summaryrefslogtreecommitdiff
path: root/serial/tools/list_ports_linux.py
blob: f7491e57af273eca714fded6b86420cb938f00be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python
#
# portable serial port access with python
#
# This is a module that gathers a list of serial ports including details on
# GNU/Linux systems.
# The comports function is expected to return an iterable that yields tuples of
# 3 strings: port name, human readable description and a hardware ID.
#
# (C) 2011-2015 Chris Liechti <cliechti@gmx.net>
#
# SPDX-License-Identifier:    BSD-3-Clause

import glob
import os
import re
import subprocess
import sys

def read_command(argv):
    """run a command and return its output"""
    try:
        return subprocess.check_output(argv, stderr=subprocess.STDOUT).strip().decode('ascii', 'replace')
    except subprocess.SubprocessError as e:
        raise IOError('command %r failed: %s' % (argv, e))

def read_line(*args):
    """\
    Helper function to read a single line from a file.
    One or more parameters are allowed, they are joined with os.path.join.
    Returns None on errors..
    """
    try:
        with open(os.path.join(*args)) as f:
            line = f.readline().strip()
        return line
    except IOError:
        return None

def re_group(regexp, text):
    """search for regexp in text, return 1st group on match"""
    m = re.search(regexp, text)
    if m:
        return m.group(1)
    else:
        return None


# try to extract descriptions from sysfs. this was done by experimenting,
# no guarantee that it works for all devices or in the future...

def usb_sysfs_hw_string(sysfs_path):
    """given a path to a usb device in sysfs, return a string describing it"""
    bus, dev = os.path.basename(os.path.realpath(sysfs_path)).split('-')
    snr = read_line(sysfs_path, 'serial')
    if snr is not None:
        snr_txt = ' SNR=%s' % (snr,)
    else:
        snr_txt = ''
    return 'USB VID:PID=%s:%s%s' % (
            read_line(sysfs_path, 'idVendor'),
            read_line(sysfs_path, 'idProduct'),
            snr_txt
            )

def usb_lsusb_string(sysfs_path):
    base = os.path.basename(os.path.realpath(sysfs_path))
    bus = base.split('-')[0]
    try:
        dev = int(read_line(sysfs_path, 'devnum'))
        desc = read_command(['lsusb', '-v', '-s', '%s:%s' % (bus, dev)])
    except IOError:
        return base
    else:
        # descriptions from device
        iManufacturer = re_group('iManufacturer\s+\w+ (.+)', desc)
        iProduct = re_group('iProduct\s+\w+ (.+)', desc)
        iSerial = re_group('iSerial\s+\w+ (.+)', desc) or ''
        # descriptions from kernel
        idVendor = re_group('idVendor\s+0x\w+ (.+)', desc)
        idProduct = re_group('idProduct\s+0x\w+ (.+)', desc)
        # create descriptions. prefer text from device, fall back to the others
        return '%s %s %s' % (iManufacturer or idVendor, iProduct or idProduct, iSerial)

def describe(device):
    """\
    Get a human readable description.
    For USB-Serial devices try to run lsusb to get a human readable description.
    For USB-CDC devices read the description from sysfs.
    """
    base = os.path.basename(device)
    # USB-Serial devices
    sys_dev_path = '/sys/class/tty/%s/device/driver/%s' % (base, base)
    if os.path.exists(sys_dev_path):
        sys_usb = os.path.dirname(os.path.dirname(os.path.realpath(sys_dev_path)))
        return usb_lsusb_string(sys_usb)
    # USB-CDC devices
    sys_dev_path = '/sys/class/tty/%s/device/interface' % (base,)
    if os.path.exists(sys_dev_path):
        return read_line(sys_dev_path)
    # USB Product Information
    sys_dev_path = '/sys/class/tty/%s/device' % (base,)
    if os.path.exists(sys_dev_path):
        product_name_file = os.path.dirname(os.path.realpath(sys_dev_path)) + "/product"
        if os.path.exists(product_name_file):
            return read_line(product_name_file)
    return base

def hwinfo(device):
    """Try to get a HW identification using sysfs"""
    base = os.path.basename(device)
    if os.path.exists('/sys/class/tty/%s/device' % (base,)):
        # PCI based devices
        sys_id_path = '/sys/class/tty/%s/device/id' % (base,)
        if os.path.exists(sys_id_path):
            return read_line(sys_id_path)
        # USB-Serial devices
        sys_dev_path = '/sys/class/tty/%s/device/driver/%s' % (base, base)
        if os.path.exists(sys_dev_path):
            sys_usb = os.path.dirname(os.path.dirname(os.path.realpath(sys_dev_path)))
            return usb_sysfs_hw_string(sys_usb)
        # USB-CDC devices
        if base.startswith('ttyACM'):
            sys_dev_path = '/sys/class/tty/%s/device' % (base,)
            if os.path.exists(sys_dev_path):
                return usb_sysfs_hw_string(sys_dev_path + '/..')
    return 'n/a'    # XXX directly remove these from the list?

def comports():
    devices = glob.glob('/dev/ttyS*')           # built-in serial ports
    devices.extend(glob.glob('/dev/ttyUSB*'))   # usb-serial with own driver
    devices.extend(glob.glob('/dev/ttyACM*'))   # usb-serial with CDC-ACM profile
    devices.extend(glob.glob('/dev/rfcomm*'))   # BT serial devices
    return [(d, describe(d), hwinfo(d)) for d in devices]

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# test
if __name__ == '__main__':
    for port, desc, hwid in sorted(comports()):
        print("%s: %s [%s]" % (port, desc, hwid))