Identifying USB serial devices

19.06.2015 20:04

Many devices use a simulated serial line to talk over the USB port. This is by far the simplest way of adding USB connectivity to an embedded system - either by adding a dedicated RS232-to-USB hardware converter or by implementing USB CDC ACM protocol on a microcontroller with an USB peripheral. For instance, Arduino boards use one of these approaches depending on the model.

On Linux systems, such devices typically get device files like /dev/ttyUSBx or /dev/ttyACMx associated with them. For example, if you want to talk to a device using pySerial, you have to provide the path to such a device file to the library.

A common problem is how to match such a device file to a device that has been plugged into an USB port. My laptop for instance, already has /dev/ttyACM0 through /dev/ttyACM2 on boot with nothing connected externally. USB devices conveniently identify themselves using bus addresses, product and vendor identifiers, serial number and so on. However these back end identifiers are not accessible in any way through /dev/tty* device files, which are merely front ends to the Linux kernel terminal subsystem.

Surprisingly, this appears to be quite a complicated task to do programmatically. For instance in Python, PyUSB is of no assistance in this matter as far as I can see. Browsing the web you can find plenty of recipes that all appear overly convoluted (running a regexp over dmesg wins in this respect). You can crawl through the sysfs, but this seems like a brittle solution.

The best (as in, simplest to implement and reasonably future-proof) solution I found so far is to ask udev. udev takes care of various tasks related to hot-plugging devices and hence knows about many aspects of how a physical device is registered in the kernel. It also seems to be pretty universal these days, at least on Linux. So I guess it's not a terribly bad dependency to add. pyudev (apt-get install python-pyudev on Debian) provides a nice Python interface. udevadm gets you much of the same information from a command line.

For instance, to list all terminal devices connected over the USB bus and pretty print their properties:

import pyudev
import pprint

context = pyudev.Context()

for device in context.list_devices(subsystem='tty', ID_BUS='usb'):
	pprint.pprint(dict(device))

The device file path is in the DEVNAME property, as you can see below. Other identificators from the USB bus are available in other properties. You can use them to find the specific device you need (list_devices() arguments work as a filter):

{u'DEVLINKS': u'/dev/serial/by-id/usb-Olimex_Olimex_OpenOCD_JTAG-if01-port0 /dev/serial/by-path/pci-0000:00:1d.0-usb-0:1.1:1.1-port0',
 u'DEVNAME': u'/dev/ttyUSB1',
 u'DEVPATH': u'/devices/pci0000:00/0000:00:1d.0/usb4/4-1/4-1.1/4-1.1:1.1/ttyUSB1/tty/ttyUSB1',
 u'ID_BUS': u'usb',
 u'ID_MM_CANDIDATE': u'1',
 u'ID_MODEL': u'Olimex_OpenOCD_JTAG',
 u'ID_MODEL_ENC': u'Olimex\\x20OpenOCD\\x20JTAG',
 u'ID_MODEL_FROM_DATABASE': u'OpenOCD JTAG',
 u'ID_MODEL_ID': u'0003',
 u'ID_PATH': u'pci-0000:00:1d.0-usb-0:1.1:1.1',
 u'ID_PATH_TAG': u'pci-0000_00_1d_0-usb-0_1_1_1_1',
 u'ID_REVISION': u'0500',
 u'ID_SERIAL': u'Olimex_Olimex_OpenOCD_JTAG',
 u'ID_TYPE': u'generic',
 u'ID_USB_DRIVER': u'ftdi_sio',
 u'ID_USB_INTERFACES': u':ffffff:',
 u'ID_USB_INTERFACE_NUM': u'01',
 u'ID_VENDOR': u'Olimex',
 u'ID_VENDOR_ENC': u'Olimex',
 u'ID_VENDOR_FROM_DATABASE': u'Olimex Ltd.',
 u'ID_VENDOR_ID': u'15ba',
 u'MAJOR': u'188',
 u'MINOR': u'1',
 u'SUBSYSTEM': u'tty',
 u'UDEV_LOG': u'3',
 u'USEC_INITIALIZED': u'220805300741'}
{u'DEVLINKS': u'/dev/serial/by-id/usb-STMicroelectronics_VESNA_SpectrumWars_radio_32A155593935-if00 /dev/serial/by-path/pci-0000:00:1d.0-usb-0:1.2:1.0',
 u'DEVNAME': u'/dev/ttyACM3',
 u'DEVPATH': u'/devices/pci0000:00/0000:00:1d.0/usb4/4-1/4-1.2/4-1.2:1.0/tty/ttyACM3',
 u'ID_BUS': u'usb',
 u'ID_MM_CANDIDATE': u'1',
 u'ID_MODEL': u'VESNA_SpectrumWars_radio',
 u'ID_MODEL_ENC': u'VESNA\\x20SpectrumWars\\x20radio',
 u'ID_MODEL_ID': u'5740',
 u'ID_PATH': u'pci-0000:00:1d.0-usb-0:1.2:1.0',
 u'ID_PATH_TAG': u'pci-0000_00_1d_0-usb-0_1_2_1_0',
 u'ID_REVISION': u'0200',
 u'ID_SERIAL': u'STMicroelectronics_VESNA_SpectrumWars_radio_32A155593935',
 u'ID_SERIAL_SHORT': u'32A155593935',
 u'ID_TYPE': u'generic',
 u'ID_USB_DRIVER': u'cdc_acm',
 u'ID_USB_INTERFACES': u':020201:0a0000:',
 u'ID_USB_INTERFACE_NUM': u'00',
 u'ID_VENDOR': u'STMicroelectronics',
 u'ID_VENDOR_ENC': u'STMicroelectronics',
 u'ID_VENDOR_FROM_DATABASE': u'SGS Thomson Microelectronics',
 u'ID_VENDOR_ID': u'0483',
 u'MAJOR': u'166',
 u'MINOR': u'3',
 u'SUBSYSTEM': u'tty',
 u'UDEV_LOG': u'3',
 u'USEC_INITIALIZED': u'245185375947'}

If you intend to use a device on a certain host in a more permanent fashion, a better solution is to write a custom udev rule for it. A custom rule also allows you to set a custom name, permissions and other nice things. But the above seems to work quite nicely for software that requires a minimal amount of setup.

Posted by Tomaž | Categories: Code

Comments

Thanks for the `print(dict(device))` tip!

Posted by Lucas Magasweran

Thanks for the tip!

Posted by Me

Add a new comment


(No HTML tags allowed. Separate paragraphs with a blank line.)