DEV Community

Cover image for How udev rules can help us to recognize a usb-to-serial device over /dev/tty interface
enbis
enbis

Posted on

How udev rules can help us to recognize a usb-to-serial device over /dev/tty interface

This is the story of how I found an easy solution to recognize two identical devices connected to the pc via usb-to-serial interface. At first I tried to understand how the pc allocates the /dev/ttyUSB interfaces, searching a reason beyond its incremental allocation. In particular, I had some problems when the pc turns on with both usb ports connected. In this situation figure out which ttyUSB interface the device is referring could be tricky. Luckily the udev rules can help me to break the deadlock, and now I'll try to explain how starting at the beginning.

I was working on a service dedicated to process input data coming from two identical usb-to-serial devices. Code side was a very simple service developed in Go, whose purpose is open two goroutines and manage the data streams.

var reader *bufio.Reader
c := &serial.Config{Name: "COM_name", Baud: 115200}
s, err := serial.OpenPort(c)
if err != nil {
    fmt.Printf("Open port error: %s\n", err)
    os.Exit(1)
}
reader = bufio.NewReader(s)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
    ...
}
Enter fullscreen mode Exit fullscreen mode

These few lines of code are for open a serial communication via COM port, and start reading the stream coming from the connected device. Here there is a parameter that could be troublesome to manage at runtime, especially with more than one device to be acknowledged: I'm talking about the COM_name.

At the boot time, the machine assigns random numbers for each ttyUSB devices plugged, apparently without following logical process. In my case, the two devices were generally assigned the values 0 and 1, without being able to control the order of assignment. This means that for some switching-on the ttyUSB0 port was associated to device A and ttyUSB1 to device B, some other vice versa. To avoid this problem I had to refer to a symbolic link to these devices, which should remain valid between reboots. The udev rules can do that for me.

checklist of the process

  1. identify the environment variable required to distinguish the devices, using udevadm command
  2. write the udev rule that uses the environment variable to create the specific SYMLINK
  3. reload the rules in order to apply the changes

Conventions:

  • # Linux command to be executed with root privileges.
  • $ Linux command to be executed as a regular non-privileged user

1. udevadm

udev is a device manager for the Linux kernel, able to manage the device nodes in the /dev directory.
So, first of all you need to find out which /dev/tty interface the machine assigned to the devices. In my case, I already know that Linux grouped my devices under default UART-over-USB name, so ttyUSBx ( x stands for the incremental index ). Other possibilities could be ttyS or ttyACM.

To list the assigned interfaces, just run:

$ ls -al /dev/ttyUSB*

crw-rw---- 1 root dialout 188, 0 gen 21 12:24 /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 1 gen 21 12:24 /dev/ttyUSB1
Enter fullscreen mode Exit fullscreen mode

Now you have the devpath to deepen the knowledge of your devices. So it's time to execute a udevadm info command.

$ udevadm info -a -n /dev/ttyUSB0

Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.

looking at device 
'/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.1/1-8.1:1.0/ttyUSB0/tty/ttyUSB0':
    KERNEL=="ttyUSB0"
    SUBSYSTEM=="tty"
    DRIVER==""

looking at parent device 
'/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.1/1-8.1:1.0/ttyUSB0':
    KERNELS=="ttyUSB0"
    SUBSYSTEMS=="usb-serial"
    DRIVERS=="ftdi_sio"
    ATTRS{latency_timer}=="16"
    ATTRS{port_number}=="0"

looking at parent device 
'/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.1/1-8.1:1.0':
    KERNELS=="1-8.1:1.0"
    SUBSYSTEMS=="usb"
    DRIVERS=="ftdi_sio"
    ATTRS{authorized}=="1"
    ATTRS{bAlternateSetting}==" 0"
    ATTRS{bInterfaceClass}=="ff"
    ATTRS{bInterfaceNumber}=="00"
    ATTRS{bInterfaceProtocol}=="ff"
    ATTRS{bInterfaceSubClass}=="ff"
    ATTRS{bNumEndpoints}=="02"
    ATTRS{interface}=="brd3"
    ATTRS{supports_autosuspend}=="1"
    ....
    ....
Enter fullscreen mode Exit fullscreen mode

On top, that command prints the attributes of the specified device and then walks up the chain of parent devices. Scrolling through the information, you can find some difference between two identical devices ( comparing differen ATTRS{...} values ). Unfortunately, that attributes are not available for udev matching rules, you have to extract the environment variables. So taking note of the device devpath ( be careful the first devpath, not the parent device ) you can extract the differences between the environment variables with the udevadm test command.

$ udevadm test 
'/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.1/1-8.1:1.0/ttyUSB0/tty/ttyUSB0' >u0

$ udevadm test 
'/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.3/1-8.3:1.0/ttyUSB1/tty/ttyUSB1' >u1

$ diff -u u0 u1

 .ID_PORT=0
 ACTION=add
-DEVLINKS=/dev/serial/by-id/usb-LABS_brd3_A95B4RL5-if00-port0 /dev/serial/by-path/pci-0000:00:14.0-usb-0:8.3:1.0-port0
-DEVNAME=/dev/ttyUSB0
-DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.3/1-8.3:1.0/ttyUSB0/tty/ttyUSB0
+DEVLINKS=/dev/serial/by-id/usb-LABS_brd4_A93TPMCI-if00-port0 /dev/serial/by-path/pci-0000:00:14.0-usb-0:8.2:1.0-port0
+DEVNAME=/dev/ttyUSB1
+DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-8/1-8.2/1-8.2:1.0/ttyUSB1/tty/ttyUSB1
 ID_BUS=usb
 ID_MM_CANDIDATE=1
-ID_MODEL=brd3
-ID_MODEL_ENC=brd3
+ID_MODEL=brd4
+ID_MODEL_ENC=brd4
 ID_MODEL_FROM_DATABASE=FT232 Serial (UART) IC
-ID_PATH=pci-0000:00:14.0-usb-0:8.3:1.0
-ID_PATH_TAG=pci-0000_00_14_0-usb-0_8_3_1_0
+ID_PATH=pci-0000:00:14.0-usb-0:8.2:1.0
+ID_PATH_TAG=pci-0000_00_14_0-usb-0_8_2_1_0
-ID_SERIAL_SHORT=A95B4RL5
+ID_SERIAL_SHORT=A93TPMCI
Enter fullscreen mode Exit fullscreen mode

In this list I choose the ID_MODEL as variable to recognize and diversify the two devices. Now I can write the .rules file to create and persist the SYMLINK.

2. udev rules

Udev rules are defined into files with .rules extension. The location reserved for custom made rules is /etc/udev/rules.d/. The files in which the rules are defined are conventionally named with a number as prefix, then the name of the rule and the .rules extension, and are processed in lexical order. The syntax of udev rules is composed by the match section in which are defined the conditions, and the action section in which is performed the action. Since both of my devices are based on FTDI adapter, might be a good idea write the .rules starting from idVendor and idProduct attribute, which are the same for the two interfaces. The comparison of the previous point has shown that the devices differing in ID_MODEL environment variable, so I can use that value to create the symbolic link.

#cat >/etc/udev/rules.d/99-usb-serial.rules <<'EOT'
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001",
SYMLINK+="tty%E{ID_MODEL}"
EOT
Enter fullscreen mode Exit fullscreen mode

That is the rule, now I just have to reload the rules to activate it.

3. reload the rules

Reloading the rules serves to ensure that the process ends successfully.

# udevadm control --reload
# reboot
Enter fullscreen mode Exit fullscreen mode

As soon as the computer is turned on, and for all the subsequent boots, you will see the symbolic link provided by the .rules file. This means that now you will always know which COM port is related for each devices plugged to the pc since it is linked to its ID_MODEL.

$ ls -al /dev/tty* | grep USB
lrwxrwxrwx 1 root   root          7 gen 21 12:24 /dev/ttybrd3 -> ttyUSB0
lrwxrwxrwx 1 root   root          7 gen 21 12:24 /dev/ttybrd4 -> ttyUSB1
crw-rw---- 1 root   dialout 188,  0 gen 21 12:24 /dev/ttyUSB0
crw-rw---- 1 root   dialout 188,  1 gen 21 12:24 /dev/ttyUSB1
Enter fullscreen mode Exit fullscreen mode

Now it won't be a problem anymore the random index provided by the machine for the /dev/ttyUSB device interface.

Top comments (1)

Collapse
 
brianjmurrell profile image
Brian J. Murrell • Edited

The DEVLINKS= path in your udevadm test … commands provides a hint that rather than writing your own udev rules you can just use the existing /dev/serial/by-* paths to get stable names. They are rather long names though so maybe that was the main driver for you writing your own udev rules.