Deterministic USB Devices on Linux

I had to whip up a test fixture this last week where I had to control two UART connections simultaneously through FTDI cables. Easy thing to prompt, so I asked ChatGPT to whip me out ~150 lines of Python to do what I want. The slop machine ended up teaching me something that's vexed me for years: how to deterministically access USB ports.

I'm used to simply grabbing /dev/ttyUSB0 or /dev/ttyUSB1 on my Linux machine to talk USB CDC to some embedded UART endpoint. Most of the time, that's just fine - I just grab the highest numbered ttyUSB device, because it's typically the one I plugged in last.

However, for this little fixture, it wasn't so easy: it mattered to keep track of which FTDI when to which UART, and it was something I'd need to keep track of later for future use. This is where the robot gave me a helpful trick - it showed me two folders of symlinks that the kernel automatically creates under /dev/ in these scenarios:

/dev/serial/by-id/
/dev/serial/by-path/

/by-id/ has just what I need in it:

nash@nash-Precision-5560:~$ ls /dev/serial/by-id/
usb-FTDI_FT232R_USB_UART_AQ01DEYN-if00-port0  
usb-STMicroelectronics_STLINK-V3_005600343235510D37333439-if01
usb-FTDI_TTL-234X-3V3_FT9IDJE3-if00-port0
nash@nash-Precision-5560:~$ 

Convenient, descriptive links to specific serial devices! This was there the whole time and I had no idea.

Though it's not immediately useful to me, I think /by-path/ is maybe even cooler:

nash@nash-Precision-5560:~$ ls /dev/serial/by-path/
pci-0000:00:14.0-usb-0:4.1.2:1.1          pci-0000:00:14.0-usbv2-0:4.1.2:1.1
pci-0000:00:14.0-usb-0:4.1.3.1:1.0-port0  pci-0000:00:14.0-usbv2-0:4.1.3.1:1.0-port0
pci-0000:00:14.0-usb-0:4.1.3.3:1.0-port0  pci-0000:00:14.0-usbv2-0:4.1.3.3:1.0-port0
nash@nash-Precision-5560:~$ 

A list of USB links, by port on the host computer. It even accounts for the ones that are through PCIe bridges! Would be nice if it differentiated ones that were on my hub vs my laptop, but even still - a very cool trick.

What's even neater is that these are all just symlinks. It's trivial to see which kernel filehandles they link back to:

nash@nash-Precision-5560:~$ tree /dev/serial/
/dev/serial/
├── by-id
│   ├── usb-FTDI_FT232R_USB_UART_AQ01DEYN-if00-port0 -> ../../ttyUSB1
│   ├── usb-FTDI_TTL-234X-3V3_FT9IDJE3-if00-port0 -> ../../ttyUSB0
│   └── usb-STMicroelectronics_STLINK-V3_005600343235510D37333439-if01 -> ../../ttyACM0
└── by-path
    ├── pci-0000:00:14.0-usb-0:4.1.2:1.1 -> ../../ttyACM0
    ├── pci-0000:00:14.0-usb-0:4.1.3.1:1.0-port0 -> ../../ttyUSB0
    ├── pci-0000:00:14.0-usb-0:4.1.3.3:1.0-port0 -> ../../ttyUSB1
    ├── pci-0000:00:14.0-usbv2-0:4.1.2:1.1 -> ../../ttyACM0
    ├── pci-0000:00:14.0-usbv2-0:4.1.3.1:1.0-port0 -> ../../ttyUSB0
    └── pci-0000:00:14.0-usbv2-0:4.1.3.3:1.0-port0 -> ../../ttyUSB1

3 directories, 9 files
nash@nash-Precision-5560:~$ 

It's always a pleasant surprise to learn a fun new trick about something you've been using for years. Saving this one for future use, without a doubt.