76

I have an Arduino which sometimes gets bound to /dev/ttyUSB0 and other times to /dev/ttyUSB1, making my script fail.

I do not want to enumerate all the possibilities of where my device could be, but I'd rather have it be bound somewhere static, e.g. /dev/arduino.

How do I achieve that?

3

9 Answers 9

76

As suggested, you can add some udev rules. I edited the /etc/udev/rules.d/10-local.rules to contain:

ACTION=="add", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="my_uart"

You can check for the variables of your device by running

udevadm info -a -p  $(udevadm info -q path -n /dev/ttyUSB0)

There is a more in depth guide you can read on http://www.reactivated.net/writing_udev_rules.html

8
  • 1
    Worked like a charm. One question: How to exit udevam? And it is important to note that my_uart creates the symlink under /dev/my_uart. I first wrote /dev/arduino the first time and it failed whilst arduino is sufficient.
    – k0pernikus
    Mar 7, 2013 at 19:11
  • 1
    udevadm should exit by itself when it's done.
    – Kotte
    Mar 8, 2013 at 7:31
  • Then for some unknown reason it froze the terminal session to my Raspberry Pi while generating the report.
    – k0pernikus
    Mar 8, 2013 at 12:19
  • What's the benefit of doing your command vs. just udevadm info -a -n /dev/ttyUSB0
    – Pylinux
    Mar 30, 2020 at 10:05
  • 1
    Is ACTION=="add" required? What does it do? Other examples do not have it; it is also not explained in the link to the guide. Also, what would be the difference between SYMLINK+="my_uart" and NAME="my_uart"?
    – domsson
    Jul 20, 2021 at 13:28
51

The rule syntax above may work on some distributions, but did not work on mine (Raspbian). Since I never found a single document that explains all the ins and outs, I wrote my own, to be found here. This is what it boils down to.

  1. Find out what's on ttyUSB:

    dmesg | grep ttyUSB  
    
  2. List all attributes of the device:

    udevadm info --name=/dev/ttyUSBx --attribute-walk
    

    (with your device number(s) instead of x, of course). Pick out a unique identifier set, eg idVendor + idProduct. You may also need SerialNumber if you have more than one device with the same idVendor and idProduct. SerialNumbers ought to be unique for each device.

  3. Create a file /etc/udev/rules.d/99-usb-serial.rules with something like this line in it:

    SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", SYMLINK+="your_device_name" 
    

    assuming you don't need a serial number there, and of course with the numbers for idVendor and idProduct that you found in step 2.

  4. Load the new rule:

    sudo udevadm trigger
    

    Edit 2021, july 6: While this worked fine on said distribution at the time (2015), on Archlinux, and perhaps other distributions, you may need to first do sudo udevadm control --reload, if automatic loading fails.

  5. Verify what happened:

    ls -l /dev/your_device_name  
    

    will show what ttyUSB number the symlink went to. If it's /dev/ttyUSB1, then verify who owns that and to which group it belongs:

    ls -l /dev/ttyUSB1   
    
  6. Then just for the fun of it:

    udevadm test -a -p  $(udevadm info -q path -n /dev/your_device_name)
    
9
  • 1
    So is there any solution if the idVendor and idProduct are exactly the same? (two sensors attached on identical model USB to UART modules)
    – Steven Lu
    Jul 22, 2015 at 2:00
  • @StevenLu Yes, see step 2, do udevadm info --name=/dev/ttyUSB1 --attribute-walk for both devices and look for serial numbers, they should be unique for each device. If your sensors have no serial number, can you specify what they are?
    – RolfBly
    Jul 22, 2015 at 9:31
  • that's awesome, i will report back when i try this out
    – Steven Lu
    Jul 22, 2015 at 19:43
  • My $2 USB to UART dongles have serial number 0001. Can't say I'm surprised. Looks like I have to identify the sensors based on their output protocol.
    – Steven Lu
    Aug 8, 2015 at 20:18
  • @StevenLu Bad luck. FTDI USB-UART converters do have a unique serial number, AFAIK. A few extra bucks, but less time to develop.
    – RolfBly
    Aug 10, 2015 at 20:41
14

The multiple-identical-USB-device problem

I have a Rasperry Pi with four cameras. I take pix with fswebcam which identifies the cameras as /dev/video0 .. video3. Sometimes the camera is video0, vide02, video4 and video6 but we can forget about that for now.

I need a persistent ID to identify a camera number so that, e.g. video0 is always the same camera because I caption the pictures. Unfortunately this doesn’t happen reliably - on boot, the cameras get enumerated as video0..video3 but not always the same way.

The cameras all have the same ID and serial number.

The solution to this problem involves udev rules, but there's a lot of fishhooks there as well.

If you issue the command

udevadm info –attribute-walk –path=/dev/video0

you get a screed of output but the salient bits are

KERNEL=”video0”, SUBSYSTEM=”video4linux” and KERNELS=”1:1.2.4:1.0”.

The KERNELS bit is a USB hub port. With four cameras there are four of these - they do not change on reboot , but the video{x} associated with a port may change.

So we need a udev rule to tie a video number to a USB hub port - something like:

KERNEL==”video0”,SUBSYSTEM=”video4linux”,KERNELS==”1:1.2.4:1.0”,SYMLINK+=”camera0” 

Looks simple – access the camera with

fswebcam –d  $realpath /dev/camera0

Except it doesn’t work – if you put this in a udev rule and the system has allocated video0 (on boot) to a different port, the udev rule is ignored. The symlink to /dev/camera0 basically says no such device. Square one.

What we want is to bind a symlink to a USB hub address, not a video{x} number. It took a Python program.

First step was to run

fswebcam –d /dev/video${x}  tst.jpg

for x between 1 and 8. The existence of tst.jpg after each call identifies whether there is a camera on this video number. From this make a list of active video numbers. My experience has been that it is either 0,1,2,3 or 0,2,4,6 for cameras I have used.

Others may of course build this list using a different process.

Then for each video number in the list run

udevadm info –attribute-walk –path=/dev/videox > dd

and extract the KERNELS= line from dd. From this process you end up with a list of the USB port addresses for the cameras. Sort this list so that at the next step, you always process it in the same order. Call this the "address list".

Run the udevadm … > dd thing again and make a list that looks like

KERNEL==”video0”, SUBSYSTEM=”video4linux”,KERNELS==”1:1.2.4:1.0 ”,SYMLINK+=”camerax”. Call this the “video list”.

Now step through the address list - for each entry find the corresponding entry from the video list. Create a new list that looks like a collection of lines like

KERNEL==”video0”, SUBSYSTEM=”video4linux”,KERNELS==”1:1.2.4:1.0 ”,SYMLINK+=”camera2”

The x (symlink number) is replaced by the sequence number in the address list.

Now you have a udev rule that works. A symlink that is tied to a USB hub address no matter what video number is allocated to that port at boot.

Write the final list into a file /etc/udev/rules.d/cam.rules. Run udevadm trigger to activate it and the job is done. /dev/camera2 will be the same camera (USB port) regardless of its video number.

3
  • Welcome on unix stackexchange. Please format your answer using markdown. I just did it for you. Also keep in mind that we want answers to be to the point. This reads more like a blog entry (which is not entirely bad) yet it's not that helpful to first read about approaches that didn't work. You may scrap that part.
    – k0pernikus
    Nov 29, 2016 at 9:48
  • Sorry. I'm new here. I have researched this problem for months. I did find others struggling with the same problem and I did not find an answer that worked for me. Just so I know, where would you advise that I post something like this? I did restrain myself and not include the Python source :-)
    – Ian Boag
    Nov 29, 2016 at 22:44
  • I think this answer is great. :-) It shows an approach to take when the serial number answer is not an option. It outlines the thought process behind the answer and why the first more obvious option doesn't work. Sep 15, 2020 at 4:13
4

There are plenty of good answers here, but they all work for a specific VID:PID. After a lot of fiddling with udev rules for multiple brands of serial adapters, I find this simple udev rule works best for me:

ACTION=="add", SUBSYSTEM=="tty", SUBSYSTEMS=="usb", DRIVERS=="usb", SYMLINK+="tty.usb-$attr{devpath}"

It yields persistent symlinks based on the path to the device on the USB bus, for example:

$ lsusb -t # output filtered for tty devices only
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/12p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 1: Dev 5, If 0, Class=, Driver=, 480M
        |__ Port 2: Dev 6, If 0, Class=Hub, Driver=hub/4p, 480M
            |__ Port 3: Dev 20, If 1, Class=CDC Data, Driver=cdc_acm, 12M
            |__ Port 3: Dev 20, If 0, Class=Communications, Driver=cdc_acm, 12M
            |__ Port 2: Dev 7, If 0, Class=Hub, Driver=hub/5p, 480M
                |__ Port 3: Dev 17, If 0, Class=Vendor Specific Class, Driver=cp210x, 12M
                |__ Port 4: Dev 16, If 0, Class=Vendor Specific Class, Driver=cp210x, 12M
                |__ Port 5: Dev 10, If 0, Class=Hub, Driver=hub/5p, 480M
                    |__ Port 5: Dev 12, If 0, Class=Vendor Specific Class, Driver=cp210x, 12M
$ ls -alF /dev/tty.usb*
lrwxrwxrwx 1 root root 7 Aug  2 10:33 /dev/tty.usb-1.2.2.3 -> ttyUSB2
lrwxrwxrwx 1 root root 7 Aug  2 10:33 /dev/tty.usb-1.2.2.4 -> ttyUSB1
lrwxrwxrwx 1 root root 7 Aug  2 09:04 /dev/tty.usb-1.2.2.5.5 -> ttyUSB0
lrwxrwxrwx 1 root root 7 Aug  2 10:40 /dev/tty.usb-1.2.3 -> ttyACM0
1
  • On some systems ll isn't an alias of ls -alF and the path might be different, ls -alF /dev/ttyUSB*
    – Pablo A
    Nov 27, 2021 at 1:46
3

I was also able to find a unique device in /dev/serial/by-id. I haven't tried a reboot yet, but the files in that directory were just links to the appropriate device file (ttyACM[0-9]).`

I am running arch linux on Raspberry Pi, but I stumbled across them just by doing a find for filenames containing "Arduino". My python programs run fine using those files as devices to read/write data to/from my Arduinos (so far, two on a single Pi).

2

Short Answer

Serial devices are listed deterministically by path in /dev/serial/by-path with a verbose naming scheme that represents the physical device connection. This name will always be the same as long as the device is plugged into the same USB port. These are symlinks to the dynamic naming of devices in /dev/ttyUSB*

As long as you always have your arduino plugged into the same port on your machine or USB hub it will always result in the same path name in the by-path directory. You could then just symlink to the appropriate by-path filename to make sure your script always works and has a memorable name. This solution works even if you have several arduinos connected to your system.

If I plugged an arduino into the second USB2.0 port (4.2) on my system I could always access it at /dev/arduino using this symlink definition:

ln -s /dev/serial/by-path/pci-0000:00:14.0-usb-0:4.2:1.0-port0 /dev/arduino

Background Info

The ttyUSB0, ttyUSB1 naming scheme depends on the time order of connecting devices and the next available index. The same device may show up with a new index if the system was unable to free all handles to it from a previous connection of the device or even if an error caused the device to renumerate and the previous file handle was unavailable still. The by-path directory symlinks always point the constant physical port name to the latest ttyUSB# instance of a device connection.

$ ls -la /dev/serial/by-path/
total 0
drwxr-xr-x. 2 root root 200 Feb  8 21:46 .
drwxr-xr-x. 4 root root  80 Feb  7 22:17 ..
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:1:1.0-port0 -> ../../ttyUSB4            
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.1:1.0-port0 -> ../../ttyUSB0          
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.1:1.1-port0 -> ../../ttyUSB1          
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.1:1.2-port0 -> ../../ttyUSB2          
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.1:1.3-port0 -> ../../ttyUSB3          
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.2:1.0-port0 -> ../../ttyUSB6          
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.3.3.1.2.4:1.0-port0 -> ../../ttyUSB7  
lrwxrwxrwx. 1 root root  13 Feb  8 21:46 pci-0000:00:14.0-usb-0:4.3.3.3:1.0-port0 -> ../../ttyUSB8   

In this example I have five USB-serial devices connected as well as several hubs. The "pci-0000:00:14.0-usb-" prefix is always the same because all of my USB chips are on the same PCI bus. If you had a thunderbolt connected USB hub this number would probably change. The subsequent numbering scheme represents the path to the device on a specific USB port. For the symlink beginning 0:1 I'm not sure what the 0 is, 1 is the address of the USB3 chip on my motherboard, 4.1, 4.2, 4.3 are the three builtin USB2 ports on my system. The device connected to port 4.1 is a USB-serial adapter with four serial ports on a single device, therefore it has final values of 1.0, 1.1, 1.2, and 1.3 to signify all of the ttys on the single device.

ttyUSB7 is a USB-Serial adapter plugged into a long string of USB hubs. ttyUSB8 is plugged into the same string of hubs but only part way along the string. The ttyUSB* naming shows you what order I plugged the devices in. You can see that /dev/ttyUSB5 is missing. That is because I unplugged a device that regularly fails to get all of its file handles released so when I plugged it in again that number wasn't reused. Despite that I can still reach the device under it's new filename because I have the by-path symlink.

Alternative Naming Scheme

On my CentOS system these by-path symlinks are being created with UDEV rules from the default system ruleset in /usr/lib/udev/rules.d/60-serial.rules.

Since my system doesn't have the ability to extend the PCI bus the pci-0000:00:14.0-usb- prefix never changes and is just more typing to point to a specific port. For the sake of convenience in having a shorter naming scheme, the UDEV rule below uses a variation of the 60-serial.rules to create a symlink in /dev/ which strips pci-0000:00:14.0-usb- off the beginning of the link name and -port0 off the end.

My new rule relies on the ID_PATH environment variable having been defined (ID_PATH contains the long version device path - "pci-0000:00:14.0-usb-0:4.3.3.1.2.4:1.0-port0"). You don't have to care about how ID_PATH gets created, you just need to run your rule after it exists in order to use/manipulate it. UDEV rules are executed in numerical naming order so I just named my rule with a 90 prefix, /etc/udev/rules.d/90-usbtty.rules, rather than trying to find out when ID_PATH got defined and then it was available to modify.

SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", IMPORT{program}="/bin/sh -c 'echo USB_PATH=$( echo ${ID_PATH} | cut --delimiter=- -f 4 )'", SYMLINK+="ttyUSB.$env{USB_PATH}"

This rule creates shorter names and puts them in the /dev/ directory for my convenience.

$ ls -ld /dev/ttyUSB.*
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:1:1.0 -> ttyUSB4                           
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.1:1.0 -> ttyUSB0                         
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.1:1.1 -> ttyUSB1                         
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.1:1.2 -> ttyUSB2                         
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.1:1.3 -> ttyUSB3                         
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.2:1.0 -> ttyUSB6                         
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.3.3.1.2.4:1.0 -> ttyUSB7                 
lrwxrwxrwx. 1 root root         7 Feb  8 21:46 /dev/ttyUSB.0:4.3.3.3:1.0 -> ttyUSB8                     

More Involved Naming Code

It is nice to have a one-liner UDEV rule,  but if it were any more complex  (and arguably now) it would need a separate script file to be intelligible and functional. If you did want to create such a script the example below could be a starting point. You can substitute /bin/sh for any other executable file. 

UDEV environment variables are available in the called program and standard output (e.g. echo/print statements) of the form ENV_VAR='VALUE' cause new environment variables to become available.

SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", IMPORT{program}="/path/to/script", SYMLINK+="ttyUSB.$env{USB_PATH}"
#!/bin/sh

p=$(echo "${ID_PATH}" | cut -d '-' -f 4)
echo "USB_PATH='$p'"

One last gotcha I ran into that might force a separte script rather than the one-liner is quote escaping. You can see in the script version of the rule that the more standard -d '-' is passed to cut, whereas in the oneliner --delimiter=- was required. As far as I can tell UDEV doesn't allow quote escaping, so specifying the delimiter without a quote is necessary. If you try -d - then the dash is interpreted as a flag to cut rather than a delimeter character.

2
  • (1) You obviously put a lot of work into this. But, maybe I’m missing something — I don’t see how it answers the question. The OP wants to write a script that access his device without needing to determine its dynamic name. How can he do that? (2) What is the ID_PATH variable? (3) It’s difficult (if not impossible) to use quotes in a {program}="/bin/sh -c '…'" command because it’s already quoted twice. But what does that have to do with anything? You can say -d- or -d - without using quotes. … … Please do not respond in comments; edit your answer to make it clearer and more complete. Feb 9, 2022 at 3:22
  • Great thoughts. I've tried to clarify a few points and address each of the issues you listed. - Thanks
    – user8472
    Feb 10, 2022 at 16:06
0

Just to say that the above worked for me and also automounted the device for me after I had placed an entry in /etc/fstab (and it also calls umount after removal of the stick)

i.e.

/etc/fstab

# See /etc/udev/rules.d/5-usb-disk.rules
/dev/backup     /vol/backup     ext4    defaults,errors=remount-ro 0       1

cat /etc/udev/rules.d/5-usb-stick.rules

#
# the next line creates a symlink to this disk drive called /dev/backup 
# i.e.
#   root:# ls -la /dev/backup 
#   lrwxrwxrwx 1 root root 3 Jul 22 19:33 /dev/backup -> sg0

# Backup usb stick - create /dev/backup
# ATTRS{model}=="Cruzer Blade    "
ACTION=="add", ATTRS{model}=="Cruzer Blade    ", SYMLINK+="backup"

# Clean up after removal  
ACTION=="remove", ATTRS{model}=="Cruzer Blade    ", RUN+="/bin/umount /vol/backup"

So after inserting my usb stick I get:

root:# mount | grep sd
/dev/sda1 on /vol/backup type ext4 (rw,relatime,errors=remount-ro,data=ordered)
0
$ sudo vi /etc/udev/rules.d/99-usb-serial.rules

Enter the following line:

SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="A1111111", SYMLINK+="ttyMyUSB"

To get the required information

Vendor/product ID

$ lsusb
[...]
Bus 001 Device 007: ID 0403: 6001 Future Technology Devices International, Ltd FT232 USB-Serial (UART) IC
[...]

ATTRS{serial}

$ udevadm info -a -n /dev/ttyUSB0 | grep {serial} | head -n1
ATTRS{serial}=="A1111111"
0
0

I needed to set a permanent name for a specific port, and not for a specific device connected to the computer, so I tied to KERNELS

sudo vim /etc/udev/rules.d/10-usb1-3.rules
KERNELS=="1-3:1.0", SYMLINK+="usb_3"

(yes, after disconnecting the usb, the link is also deleted, otherwise I had doubts)

Even before I made the rule above, I couldn't figure out for a long time why my udev rule didn't work, the reason was that I was setting ATTRS from different "sections" in the output of udevadm info --name=/dev/ttyUSB1 --attribute-walk.

After I fixed it, everything started working fine

To reset the rules:

sudo udevadm control --reload-rules && sudo udevadm trigger

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .