|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- #!/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:
- # 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
-
- 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 informations 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))
|