From 9950074fc29131d3111d5cef4e8759457670425d Mon Sep 17 00:00:00 2001 From: cliechti Date: Sat, 12 Oct 2013 03:50:15 +0000 Subject: OSX: [Patch pyserial:27] Scan by VendorID/Product ID for USB Serial devices also refactor list_ports_posix and take out the linux implementation into a separate file git-svn-id: http://svn.code.sf.net/p/pyserial/code/trunk/pyserial@472 f19166aa-fa4f-0410-85c2-fa1106f25c8a --- CHANGES.txt | 1 + serial/tools/list_ports.py | 4 +- serial/tools/list_ports_linux.py | 142 ++++++++++++++++++++++++++ serial/tools/list_ports_osx.py | 208 +++++++++++++++++++++++++++++++++++++++ serial/tools/list_ports_posix.py | 147 +++++---------------------- 5 files changed, 376 insertions(+), 126 deletions(-) create mode 100644 serial/tools/list_ports_linux.py create mode 100644 serial/tools/list_ports_osx.py diff --git a/CHANGES.txt b/CHANGES.txt index 756fa00..7091cc1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -445,6 +445,7 @@ Version 2.7 2012-nn-nn Lundh) - Posix: [Patch pyserial:28] Accept any speed on Linux - Posix: [Patch pyserial:29] PosixSerial.read() should "ignore" errno.EINTR +- OSX: [Patch pyserial:27] Scan by VendorID/Product ID for USB Serial devices Bugfixes: diff --git a/serial/tools/list_ports.py b/serial/tools/list_ports.py index c115441..6231d7d 100644 --- a/serial/tools/list_ports.py +++ b/serial/tools/list_ports.py @@ -4,7 +4,7 @@ # this is a wrapper module for different platform implementations of the # port enumeration feature # -# (C) 2011 Chris Liechti +# (C) 2011-2013 Chris Liechti # this is distributed under a free software license, see license.txt """\ @@ -50,7 +50,7 @@ def main(): usage = "%prog [options] []", description = "Miniterm - A simple terminal program for the serial port." ) - + parser.add_option("--debug", help="print debug messages and tracebacks (development mode)", dest="debug", diff --git a/serial/tools/list_ports_linux.py b/serial/tools/list_ports_linux.py new file mode 100644 index 0000000..eecbf41 --- /dev/null +++ b/serial/tools/list_ports_linux.py @@ -0,0 +1,142 @@ +#!/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 +# +# (C) 2011-2013 Chris Liechti +# this is distributed under a free software license, see license.txt + +import glob +import sys +import os +import re + +try: + import subprocess +except ImportError: + def popen(argv): + try: + si, so = os.popen4(' '.join(argv)) + return so.read().strip() + except: + raise IOError('lsusb failed') +else: + def popen(argv): + try: + return subprocess.check_output(argv, stderr=subprocess.STDOUT).strip() + except: + raise IOError('lsusb failed') + + +# The comports function is expected to return an iterable that yields tuples of +# 3 strings: port name, human readable description and a hardware ID. +# +# as currently no method is known to get the second two strings easily, they +# are currently just identical to the port name. + +# try to detect the OS so that a device can be selected... +plat = sys.platform.lower() + +def read_line(filename): + """help function to read a single line from a file. returns none""" + try: + f = open(filename) + line = f.readline().strip() + f.close() + return line + except IOError: + return None + +def re_group(regexp, text): + """search for regexp in text, return 1st group on match""" + if sys.version < '3': + m = re.search(regexp, text) + else: + # text is bytes-like + m = re.search(regexp, text.decode('ascii', 'replace')) + if m: return m.group(1) + + +# 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: + 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(open(os.path.join(sysfs_path, 'devnum')).readline().strip()) + desc = popen(['lsusb', '-v', '-s', '%s:%s' % (bus, dev)]) + # 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) + except IOError: + return base + +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) + 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*') + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') + 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) diff --git a/serial/tools/list_ports_osx.py b/serial/tools/list_ports_osx.py new file mode 100644 index 0000000..c9ed615 --- /dev/null +++ b/serial/tools/list_ports_osx.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python + +# portable serial port access with python +# +# This is a module that gathers a list of serial ports including details on OSX +# +# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools +# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston +# and modifications by cliechti +# +# this is distributed under a free software license, see license.txt + + + +# List all of the callout devices in OS/X by querying IOKit. + +# See the following for a reference of how to do this: +# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD + +# More help from darwin_hid.py + +# Also see the 'IORegistryExplorer' for an idea of what we are actually searching + +import ctypes +from ctypes import util +import re + +iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit')) +cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) + +kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") +kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault") + +kCFStringEncodingMacRoman = 0 + +iokit.IOServiceMatching.restype = ctypes.c_void_p + +iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + +iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32] +iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +iokit.IORegistryEntryGetPath.restype = ctypes.c_void_p + +iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +iokit.IORegistryEntryGetName.restype = ctypes.c_void_p + +iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +iokit.IOObjectGetClass.restype = ctypes.c_void_p + +iokit.IOObjectRelease.argtypes = [ctypes.c_void_p] + + +cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32] +cf.CFStringCreateWithCString.restype = ctypes.c_void_p + +cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32] +cf.CFStringGetCStringPtr.restype = ctypes.c_char_p + +cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p] +cf.CFNumberGetValue.restype = ctypes.c_void_p + +def get_string_property(device_t, property): + """ Search the given device for the specified string property + + @param device_t Device to search + @param property String to search for. + @return Python string containing the value, or None if not found. + """ + key = cf.CFStringCreateWithCString( + kCFAllocatorDefault, + property.encode("mac_roman"), + kCFStringEncodingMacRoman + ) + + CFContainer = iokit.IORegistryEntryCreateCFProperty( + device_t, + key, + kCFAllocatorDefault, + 0 + ); + + output = None + + if CFContainer: + output = cf.CFStringGetCStringPtr(CFContainer, 0) + + return output + +def get_int_property(device_t, property): + """ Search the given device for the specified string property + + @param device_t Device to search + @param property String to search for. + @return Python string containing the value, or None if not found. + """ + key = cf.CFStringCreateWithCString( + kCFAllocatorDefault, + property.encode("mac_roman"), + kCFStringEncodingMacRoman + ) + + CFContainer = iokit.IORegistryEntryCreateCFProperty( + device_t, + key, + kCFAllocatorDefault, + 0 + ); + + number = ctypes.c_uint16() + + if CFContainer: + output = cf.CFNumberGetValue(CFContainer, 2, ctypes.byref(number)) + + return number.value + +def IORegistryEntryGetName(device): + pathname = ctypes.create_string_buffer(100) # TODO: Is this ok? + iokit.IOObjectGetClass( + device, + ctypes.byref(pathname) + ) + + return pathname.value + +def GetParentDeviceByType(device, parent_type): + """ Find the first parent of a device that implements the parent_type + @param IOService Service to inspect + @return Pointer to the parent type, or None if it was not found. + """ + # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice. + while IORegistryEntryGetName(device) != parent_type: + parent = ctypes.c_void_p() + response = iokit.IORegistryEntryGetParentEntry( + device, + "IOService".encode("mac_roman"), + ctypes.byref(parent) + ) + + # If we weren't able to find a parent for the device, we're done. + if response != 0: + return None + + device = parent + + return device + +def GetIOServicesByType(service_type): + """ + """ + serial_port_iterator = ctypes.c_void_p() + + response = iokit.IOServiceGetMatchingServices( + kIOMasterPortDefault, + iokit.IOServiceMatching(service_type), + ctypes.byref(serial_port_iterator) + ) + + services = [] + while iokit.IOIteratorIsValid(serial_port_iterator): + service = iokit.IOIteratorNext(serial_port_iterator) + if not service: + break + services.append(service) + + iokit.IOObjectRelease(serial_port_iterator) + + return services + +def comports(): + # Scan for all iokit serial ports + services = GetIOServicesByType('IOSerialBSDClient') + + ports = [] + for service in services: + info = [] + + # First, add the callout device file. + info.append(get_string_property(service, "IOCalloutDevice")) + + # If the serial port is implemented by a + usb_device = GetParentDeviceByType(service, "IOUSBDevice") + if usb_device != None: + info.append(get_string_property(usb_device, "USB Product Name")) + + info.append( + "USB VID:PID=%x:%x SNR=%s"%( + get_int_property(usb_device, "idVendor"), + get_int_property(usb_device, "idProduct"), + get_string_property(usb_device, "USB Serial Number")) + ) + else: + info.append('n/a') + info.append('n/a') + + ports.append(info) + + return ports + +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print "%s: %s [%s]" % (port, desc, hwid) + diff --git a/serial/tools/list_ports_posix.py b/serial/tools/list_ports_posix.py index 3e2da72..9d96e93 100644 --- a/serial/tools/list_ports_posix.py +++ b/serial/tools/list_ports_posix.py @@ -1,131 +1,33 @@ -import glob -import sys -import os -import re - -try: - import subprocess -except ImportError: - def popen(argv): - try: - si, so = os.popen4(' '.join(argv)) - return so.read().strip() - except: - raise IOError('lsusb failed') -else: - def popen(argv): - try: - return subprocess.check_output(argv, stderr=subprocess.STDOUT).strip() - except: - raise IOError('lsusb failed') +#!/usr/bin/env python +# portable serial port access with python -# The comports function is expected to return an iterable that yields tuples of -# 3 strings: port name, human readable description and a hardware ID. +# This is a module that gathers a list of serial ports on POSIXy systems. +# For some specific implementations, see also list_ports_linux, list_ports_osx +# +# this is a wrapper module for different platform implementations of the +# port enumeration feature # -# as currently no method is known to get the second two strings easily, they -# are currently just identical to the port name. +# (C) 2011-2013 Chris Liechti +# this is distributed under a free software license, see license.txt -# try to detect the OS so that a device can be selected... -plat = sys.platform.lower() +"""\ +The ``comports`` function is expected to return an iterable that yields tuples +of 3 strings: port name, human readable description and a hardware ID. -def read_line(filename): - """help function to read a single line from a file. returns none""" - try: - f = open(filename) - line = f.readline().strip() - f.close() - return line - except IOError: - return None - -def re_group(regexp, text): - """search for regexp in text, return 1st group on match""" - if sys.version < '3': - m = re.search(regexp, text) - else: - # text is bytes-like - m = re.search(regexp, text.decode('ascii', 'replace')) - if m: return m.group(1) +As currently no method is known to get the second two strings easily, they are +currently just identical to the port name. +""" + +import glob +import sys +import os +# try to detect the OS so that a device can be selected... +plat = sys.platform.lower() if plat[:5] == 'linux': # Linux (confirmed) - # 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: - 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(open(os.path.join(sysfs_path, 'devnum')).readline().strip()) - desc = popen(['lsusb', '-v', '-s', '%s:%s' % (bus, dev)]) - # 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) - except IOError: - return base - - 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) - 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*') + glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') - return [(d, describe(d), hwinfo(d)) for d in devices] + from serial.tools.list_ports_linux import comports elif plat == 'cygwin': # cygwin/win32 def comports(): @@ -145,10 +47,7 @@ elif plat[:3] == 'bsd' or \ return [(d, d, d) for d in devices] elif plat[:6] == 'darwin': # OS X (confirmed) - def comports(): - """scan for available ports. return a list of device names.""" - devices = glob.glob('/dev/tty.*') - return [(d, d, d) for d in devices] + from serial.tools.list_ports_osx import comports elif plat[:6] == 'netbsd': # NetBSD def comports(): -- cgit v1.2.1