| #!/usr/bin/env 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, hoihu, hardkrash |
| # |
| # This file is part of pySerial. https://github.com/pyserial/pyserial |
| # (C) 2013-2020 |
| # |
| # SPDX-License-Identifier: BSD-3-Clause |
| |
| |
| # List all of the callout devices in OS/X by querying IOKit. |
| |
| # See the following for a reference of how to do this: |
| # https://developer.apple.com/library/archive/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 |
| |
| from __future__ import absolute_import |
| |
| import ctypes |
| |
| from serial.tools import list_ports_common |
| |
| iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit') |
| cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation') |
| |
| # kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same |
| kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") |
| kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault") |
| |
| kCFStringEncodingMacRoman = 0 |
| kCFStringEncodingUTF8 = 0x08000100 |
| |
| # defined in `IOKit/usb/USBSpec.h` |
| kUSBVendorString = 'USB Vendor Name' |
| kUSBSerialNumberString = 'USB Serial Number' |
| |
| # `io_name_t` defined as `typedef char io_name_t[128];` |
| # in `device/device_types.h` |
| io_name_size = 128 |
| |
| # defined in `mach/kern_return.h` |
| KERN_SUCCESS = 0 |
| # kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h` |
| kern_return_t = ctypes.c_int |
| |
| 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 = kern_return_t |
| |
| iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] |
| iokit.IOServiceGetMatchingServices.restype = kern_return_t |
| |
| 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 = kern_return_t |
| |
| iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p] |
| iokit.IORegistryEntryGetName.restype = kern_return_t |
| |
| iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p] |
| iokit.IOObjectGetClass.restype = kern_return_t |
| |
| 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.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32] |
| cf.CFStringGetCString.restype = ctypes.c_bool |
| |
| cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p] |
| cf.CFNumberGetValue.restype = ctypes.c_void_p |
| |
| # void CFRelease ( CFTypeRef cf ); |
| cf.CFRelease.argtypes = [ctypes.c_void_p] |
| cf.CFRelease.restype = None |
| |
| # CFNumber type defines |
| kCFNumberSInt8Type = 1 |
| kCFNumberSInt16Type = 2 |
| kCFNumberSInt32Type = 3 |
| kCFNumberSInt64Type = 4 |
| |
| |
| def get_string_property(device_type, property): |
| """ |
| Search the given device for the specified string property |
| |
| @param device_type Type of Device |
| @param property String to search for |
| @return Python string containing the value, or None if not found. |
| """ |
| key = cf.CFStringCreateWithCString( |
| kCFAllocatorDefault, |
| property.encode("utf-8"), |
| kCFStringEncodingUTF8) |
| |
| CFContainer = iokit.IORegistryEntryCreateCFProperty( |
| device_type, |
| key, |
| kCFAllocatorDefault, |
| 0) |
| output = None |
| |
| if CFContainer: |
| output = cf.CFStringGetCStringPtr(CFContainer, 0) |
| if output is not None: |
| output = output.decode('utf-8') |
| else: |
| buffer = ctypes.create_string_buffer(io_name_size); |
| success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8) |
| if success: |
| output = buffer.value.decode('utf-8') |
| cf.CFRelease(CFContainer) |
| return output |
| |
| |
| def get_int_property(device_type, property, cf_number_type): |
| """ |
| Search the given device for the specified string property |
| |
| @param device_type Device to search |
| @param property String to search for |
| @param cf_number_type CFType number |
| |
| @return Python string containing the value, or None if not found. |
| """ |
| key = cf.CFStringCreateWithCString( |
| kCFAllocatorDefault, |
| property.encode("utf-8"), |
| kCFStringEncodingUTF8) |
| |
| CFContainer = iokit.IORegistryEntryCreateCFProperty( |
| device_type, |
| key, |
| kCFAllocatorDefault, |
| 0) |
| |
| if CFContainer: |
| if (cf_number_type == kCFNumberSInt32Type): |
| number = ctypes.c_uint32() |
| elif (cf_number_type == kCFNumberSInt16Type): |
| number = ctypes.c_uint16() |
| cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number)) |
| cf.CFRelease(CFContainer) |
| return number.value |
| return None |
| |
| def IORegistryEntryGetName(device): |
| devicename = ctypes.create_string_buffer(io_name_size); |
| res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename)) |
| if res != KERN_SUCCESS: |
| return None |
| # this works in python2 but may not be valid. Also I don't know if |
| # this encoding is guaranteed. It may be dependent on system locale. |
| return devicename.value.decode('utf-8') |
| |
| def IOObjectGetClass(device): |
| classname = ctypes.create_string_buffer(io_name_size) |
| iokit.IOObjectGetClass(device, ctypes.byref(classname)) |
| return classname.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. |
| parent_type = parent_type.encode('utf-8') |
| while IOObjectGetClass(device) != parent_type: |
| parent = ctypes.c_void_p() |
| response = iokit.IORegistryEntryGetParentEntry( |
| device, |
| "IOService".encode("utf-8"), |
| ctypes.byref(parent)) |
| # If we weren't able to find a parent for the device, we're done. |
| if response != KERN_SUCCESS: |
| return None |
| device = parent |
| return device |
| |
| |
| def GetIOServicesByType(service_type): |
| """ |
| returns iterator over specified service_type |
| """ |
| serial_port_iterator = ctypes.c_void_p() |
| |
| iokit.IOServiceGetMatchingServices( |
| kIOMasterPortDefault, |
| iokit.IOServiceMatching(service_type.encode('utf-8')), |
| 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 location_to_string(locationID): |
| """ |
| helper to calculate port and bus number from locationID |
| """ |
| loc = ['{}-'.format(locationID >> 24)] |
| while locationID & 0xf00000: |
| if len(loc) > 1: |
| loc.append('.') |
| loc.append('{}'.format((locationID >> 20) & 0xf)) |
| locationID <<= 4 |
| return ''.join(loc) |
| |
| |
| class SuitableSerialInterface(object): |
| pass |
| |
| |
| def scan_interfaces(): |
| """ |
| helper function to scan USB interfaces |
| returns a list of SuitableSerialInterface objects with name and id attributes |
| """ |
| interfaces = [] |
| for service in GetIOServicesByType('IOSerialBSDClient'): |
| device = get_string_property(service, "IOCalloutDevice") |
| if device: |
| usb_device = GetParentDeviceByType(service, "IOUSBInterface") |
| if usb_device: |
| name = get_string_property(usb_device, "USB Interface Name") or None |
| locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or '' |
| i = SuitableSerialInterface() |
| i.id = locationID |
| i.name = name |
| interfaces.append(i) |
| return interfaces |
| |
| |
| def search_for_locationID_in_interfaces(serial_interfaces, locationID): |
| for interface in serial_interfaces: |
| if (interface.id == locationID): |
| return interface.name |
| return None |
| |
| |
| def comports(include_links=False): |
| # XXX include_links is currently ignored. are links in /dev even supported here? |
| # Scan for all iokit serial ports |
| services = GetIOServicesByType('IOSerialBSDClient') |
| ports = [] |
| serial_interfaces = scan_interfaces() |
| for service in services: |
| # First, add the callout device file. |
| device = get_string_property(service, "IOCalloutDevice") |
| if device: |
| info = list_ports_common.ListPortInfo(device) |
| # If the serial port is implemented by IOUSBDevice |
| # NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon |
| # devices has been completely removed. Thanks to @oskay for this patch. |
| usb_device = GetParentDeviceByType(service, "IOUSBHostDevice") |
| if not usb_device: |
| usb_device = GetParentDeviceByType(service, "IOUSBDevice") |
| if usb_device: |
| # fetch some useful information from properties |
| info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type) |
| info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type) |
| info.serial_number = get_string_property(usb_device, kUSBSerialNumberString) |
| # We know this is a usb device, so the |
| # IORegistryEntryName should always be aliased to the |
| # usb product name string descriptor. |
| info.product = IORegistryEntryGetName(usb_device) or 'n/a' |
| info.manufacturer = get_string_property(usb_device, kUSBVendorString) |
| locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) |
| info.location = location_to_string(locationID) |
| info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID) |
| info.apply_usb_info() |
| ports.append(info) |
| return ports |
| |
| # test |
| if __name__ == '__main__': |
| for port, desc, hwid in sorted(comports()): |
| print("{}: {} [{}]".format(port, desc, hwid)) |