Important note: This code is deprecated as it has been superseded by the UliSerial library on GitHub. UIiSerial implements the serial port filtering in a platform-independent manner using pyserial built-in features.
Typically, when one intends to select a serial port in Python, you use a sequence such as
glob.glob('/dev/ttyACM*')[0]
or just go to straight hard-coding the port such as /dev/ttyACM0
.
It should be quite obvious why this isn’t robust:
- There might be multiple serial ports and the order is unknown
- You code might need to work on different computers, which lead to different ports being assigned
- When you re-plug a device, it might get assigned a differnent serial port number (e.g.
/dev/ttyACM1
)
The following code uses only the linux integrated tool udevadm
to query information and built-in libraries. Currently it does not work on anything but Linux.
import glob import subprocess import re def get_serial_ports(): # Get a list of /dev/ttyACM* and /dev/ttyUSB* devices ports = glob.glob('/dev/ttyACM*') + glob.glob('/dev/ttyUSB*') return ports def get_udev_info(port): # Run the udevadm command and get the output result = subprocess.run(['udevadm', 'info', '-q', 'all', '-r', '-n', port], capture_output=True, text=True) return result.stdout def find_serial_ports(vendor=None, model=None, usb_vendor=None, usb_model=None, usb_model_id=None, vendor_id=None): # Mapping of human-readable names to udevadm keys attribute_mapping = { "vendor": "ID_VENDOR_FROM_DATABASE", "model": "ID_MODEL_FROM_DATABASE", "usb_vendor": "ID_USB_VENDOR", "usb_model": "ID_USB_MODEL_ENC", "usb_model_id": "ID_USB_MODEL_ID", "vendor_id": "ID_VENDOR_ID", "usb_path": "ID_PATH", "serial": "ID_SERIAL_SHORT" } # Filters based on provided arguments filters = {} for key, value in locals().items(): if value and key in attribute_mapping: filters[attribute_mapping[key]] = value # Find and filter ports ports = get_serial_ports() filtered_ports = [] for port in ports: udev_info = get_udev_info(port) match = True for key, value in filters.items(): if not re.search(f"{key}={re.escape(value)}", udev_info): match = False break if match: filtered_ports.append(port) return filtered_ports def get_serial_port_info(port): # Mapping of udevadm keys to human-readable names attribute_mapping = { "ID_VENDOR_FROM_DATABASE": "vendor", "ID_MODEL_FROM_DATABASE": "model", "ID_USB_VENDOR": "usb_vendor", "ID_USB_MODEL_ENC": "usb_model", "ID_USB_MODEL_ID": "usb_model_id", "ID_VENDOR_ID": "vendor_id", "ID_SERIAL_SHORT": "serial", } # Run the udevadm command and get the output udev_info = get_udev_info(port) port_info = {} for line in udev_info.splitlines(): if line.startswith('E: '): key, value = line[3:].split('=', 1) if key in attribute_mapping: # Decode escape sequences like \x20 to a space. # NOTE: Since only \x20 is common, we currently only replace that one port_info[attribute_mapping[key]] = value.replace('\\x20', ' ') return port_info def find_serial_port(**kwargs): """ Find a single serial port matching the provided filters. """ ports = find_serial_ports(**kwargs) if len(ports) > 1: raise ValueError("Multiple matching ports found for filters: " + str(kwargs)) elif len(ports) == 0: raise ValueError("No matching ports found for filters: " + str(kwargs)) else: return ports[0]
Example: Find a serial port
# Example usage: Find a serial port matching_ports = find_serial_ports(vendor="OpenMoko, Inc.") print(matching_ports) # Prints e.g. ['/dev/ttyACM0']
Example: Print information about a given port
This is typically used so you know what filters to add for find_serial_ports()
.
# Example usage: Print info about a serial port serial_port_info = get_serial_port_info('/dev/ttyACM0') print(serial_port_info)
This prints, for example:
{ 'serial': '01010A23535223934CF29A1EF5000007', 'vendor_id': '1d50', 'usb_model': 'Marlin USB Device', 'usb_model_id': '6029', 'usb_vendor': 'marlinfw.org', 'vendor': 'OpenMoko, Inc.', 'model': 'Marlin 2.0 (Serial)' }