Надійна фільтрація послідовних портів Python у Linux за назвою та виробником за допомогою udev

udev_info_example.py
    {
        '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)'
    }

    ```

або просто жорстко задати порт, наприклад `/dev/ttyACM0`.

Має бути досить очевидно, чому це не є надійним:
* Послідовних портів може бути кілька, а їхній порядок невідомий
* Ваш код може потребувати роботи на різних комп'ютерах, що призводить до призначення різних портів
* Коли ви перепідключаєте пристрій, йому може бути призначено інший номер послідовного порту (наприклад, `/dev/ttyACM1`)


Наведений нижче код використовує лише вбудований у Linux інструмент `udevadm` для запиту інформації та вбудовані бібліотеки. **Наразі він працює лише на Linux.**

```python {filename="udev_serial_utils.py"}
import glob
import subprocess
import re

def get_serial_ports():
    # Отримати список пристроїв /dev/ttyACM* та /dev/ttyUSB*
    ports = glob.glob('/dev/ttyACM*') + glob.glob('/dev/ttyUSB*')
    return ports

def get_udev_info(port):
    # Виконати команду udevadm та отримати вивід
    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):
    # Відповідність зручних для читання назв ключам udevadm
    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 = {}
    for key, value in locals().items():
        if value and key in attribute_mapping:
            filters[attribute_mapping[key]] = value

    # Знайти та відфільтрувати порти
    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):
    # Відповідність ключів udevadm зручним для читання назвам
    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",
    }

    # Виконати команду udevadm та отримати вивід
    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:
                # Декодувати escape-послідовності, як-от \x20, у пробіл.
                # ПРИМІТКА: Оскільки лише \x20 є поширеним, наразі ми замінюємо лише її
                port_info[attribute_mapping[key]] = value.replace('\\x20', ' ')

    return port_info

def find_serial_port(**kwargs):
    """
    Знайти один послідовний порт, що відповідає наданим фільтрам.
    """
    ports = find_serial_ports(**kwargs)
    if len(ports) > 1:
        raise ValueError("Знайдено кілька портів, що відповідають фільтрам: " + str(kwargs))
    elif len(ports) == 0:
        raise ValueError("Не знайдено портів, що відповідають фільтрам: " + str(kwargs))
    else:
        return ports[0]

Приклад: Знайти послідовний порт

find_serial_example.py
# Приклад використання: Знайти послідовний порт
matching_ports = find_serial_ports(vendor="OpenMoko, Inc.")
print(matching_ports) # Виводить, наприклад, ['/dev/ttyACM0']

Приклад: Вивести інформацію про заданий порт

Зазвичай це використовується, щоб ви знали, які фільтри додати для find_serial_ports().

print_port_info_example.py
# Приклад використання: Вивести інформацію про послідовний порт
serial_port_info = get_serial_port_info('/dev/ttyACM0')
print(serial_port_info)

Це виводить, наприклад:

serial_port_info_output.py
{
    '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)'
}

Дивіться схожі статті за категоріями: Electronics Linux