How udev rules can help us to recognize a usb-to-serial device over /dev/tty interface
Tags: linux, go, tutorial
Published: 2020-01-22 22:52:13


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() {
    ...
}
```

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
```

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"
    ....
    ....
``` 

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
```
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
```
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
```

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
```

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