1#!/usr/bin/env python 2# 3# This is a module that gathers a list of serial ports including details on OSX 4# 5# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools 6# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston 7# and modifications by cliechti, hoihu, hardkrash 8# 9# This file is part of pySerial. https://github.com/pyserial/pyserial 10# (C) 2013-2020 11# 12# SPDX-License-Identifier: BSD-3-Clause 13 14 15# List all of the callout devices in OS/X by querying IOKit. 16 17# See the following for a reference of how to do this: 18# http://developer.apple.com/library/mac/#documentation/DeviceDrivers/Conceptual/WorkingWSerial/WWSerial_SerialDevs/SerialDevices.html#//apple_ref/doc/uid/TP30000384-CIHGEAFD 19 20# More help from darwin_hid.py 21 22# Also see the 'IORegistryExplorer' for an idea of what we are actually searching 23 24from __future__ import absolute_import 25 26import ctypes 27 28from serial.tools import list_ports_common 29 30iokit = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/IOKit.framework/IOKit') 31cf = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation') 32 33# kIOMasterPortDefault is no longer exported in BigSur but no biggie, using NULL works just the same 34kIOMasterPortDefault = 0 # WAS: ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault") 35kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault") 36 37kCFStringEncodingMacRoman = 0 38kCFStringEncodingUTF8 = 0x08000100 39 40# defined in `IOKit/usb/USBSpec.h` 41kUSBVendorString = 'USB Vendor Name' 42kUSBSerialNumberString = 'USB Serial Number' 43 44# `io_name_t` defined as `typedef char io_name_t[128];` 45# in `device/device_types.h` 46io_name_size = 128 47 48# defined in `mach/kern_return.h` 49KERN_SUCCESS = 0 50# kern_return_t defined as `typedef int kern_return_t;` in `mach/i386/kern_return.h` 51kern_return_t = ctypes.c_int 52 53iokit.IOServiceMatching.restype = ctypes.c_void_p 54 55iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] 56iokit.IOServiceGetMatchingServices.restype = kern_return_t 57 58iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] 59iokit.IOServiceGetMatchingServices.restype = kern_return_t 60 61iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32] 62iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p 63 64iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] 65iokit.IORegistryEntryGetPath.restype = kern_return_t 66 67iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p] 68iokit.IORegistryEntryGetName.restype = kern_return_t 69 70iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p] 71iokit.IOObjectGetClass.restype = kern_return_t 72 73iokit.IOObjectRelease.argtypes = [ctypes.c_void_p] 74 75 76cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32] 77cf.CFStringCreateWithCString.restype = ctypes.c_void_p 78 79cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32] 80cf.CFStringGetCStringPtr.restype = ctypes.c_char_p 81 82cf.CFStringGetCString.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_uint32] 83cf.CFStringGetCString.restype = ctypes.c_bool 84 85cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p] 86cf.CFNumberGetValue.restype = ctypes.c_void_p 87 88# void CFRelease ( CFTypeRef cf ); 89cf.CFRelease.argtypes = [ctypes.c_void_p] 90cf.CFRelease.restype = None 91 92# CFNumber type defines 93kCFNumberSInt8Type = 1 94kCFNumberSInt16Type = 2 95kCFNumberSInt32Type = 3 96kCFNumberSInt64Type = 4 97 98 99def get_string_property(device_type, property): 100 """ 101 Search the given device for the specified string property 102 103 @param device_type Type of Device 104 @param property String to search for 105 @return Python string containing the value, or None if not found. 106 """ 107 key = cf.CFStringCreateWithCString( 108 kCFAllocatorDefault, 109 property.encode("utf-8"), 110 kCFStringEncodingUTF8) 111 112 CFContainer = iokit.IORegistryEntryCreateCFProperty( 113 device_type, 114 key, 115 kCFAllocatorDefault, 116 0) 117 output = None 118 119 if CFContainer: 120 output = cf.CFStringGetCStringPtr(CFContainer, 0) 121 if output is not None: 122 output = output.decode('utf-8') 123 else: 124 buffer = ctypes.create_string_buffer(io_name_size); 125 success = cf.CFStringGetCString(CFContainer, ctypes.byref(buffer), io_name_size, kCFStringEncodingUTF8) 126 if success: 127 output = buffer.value.decode('utf-8') 128 cf.CFRelease(CFContainer) 129 return output 130 131 132def get_int_property(device_type, property, cf_number_type): 133 """ 134 Search the given device for the specified string property 135 136 @param device_type Device to search 137 @param property String to search for 138 @param cf_number_type CFType number 139 140 @return Python string containing the value, or None if not found. 141 """ 142 key = cf.CFStringCreateWithCString( 143 kCFAllocatorDefault, 144 property.encode("utf-8"), 145 kCFStringEncodingUTF8) 146 147 CFContainer = iokit.IORegistryEntryCreateCFProperty( 148 device_type, 149 key, 150 kCFAllocatorDefault, 151 0) 152 153 if CFContainer: 154 if (cf_number_type == kCFNumberSInt32Type): 155 number = ctypes.c_uint32() 156 elif (cf_number_type == kCFNumberSInt16Type): 157 number = ctypes.c_uint16() 158 cf.CFNumberGetValue(CFContainer, cf_number_type, ctypes.byref(number)) 159 cf.CFRelease(CFContainer) 160 return number.value 161 return None 162 163def IORegistryEntryGetName(device): 164 devicename = ctypes.create_string_buffer(io_name_size); 165 res = iokit.IORegistryEntryGetName(device, ctypes.byref(devicename)) 166 if res != KERN_SUCCESS: 167 return None 168 # this works in python2 but may not be valid. Also I don't know if 169 # this encoding is guaranteed. It may be dependent on system locale. 170 return devicename.value.decode('utf-8') 171 172def IOObjectGetClass(device): 173 classname = ctypes.create_string_buffer(io_name_size) 174 iokit.IOObjectGetClass(device, ctypes.byref(classname)) 175 return classname.value 176 177def GetParentDeviceByType(device, parent_type): 178 """ Find the first parent of a device that implements the parent_type 179 @param IOService Service to inspect 180 @return Pointer to the parent type, or None if it was not found. 181 """ 182 # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice. 183 parent_type = parent_type.encode('utf-8') 184 while IOObjectGetClass(device) != parent_type: 185 parent = ctypes.c_void_p() 186 response = iokit.IORegistryEntryGetParentEntry( 187 device, 188 "IOService".encode("utf-8"), 189 ctypes.byref(parent)) 190 # If we weren't able to find a parent for the device, we're done. 191 if response != KERN_SUCCESS: 192 return None 193 device = parent 194 return device 195 196 197def GetIOServicesByType(service_type): 198 """ 199 returns iterator over specified service_type 200 """ 201 serial_port_iterator = ctypes.c_void_p() 202 203 iokit.IOServiceGetMatchingServices( 204 kIOMasterPortDefault, 205 iokit.IOServiceMatching(service_type.encode('utf-8')), 206 ctypes.byref(serial_port_iterator)) 207 208 services = [] 209 while iokit.IOIteratorIsValid(serial_port_iterator): 210 service = iokit.IOIteratorNext(serial_port_iterator) 211 if not service: 212 break 213 services.append(service) 214 iokit.IOObjectRelease(serial_port_iterator) 215 return services 216 217 218def location_to_string(locationID): 219 """ 220 helper to calculate port and bus number from locationID 221 """ 222 loc = ['{}-'.format(locationID >> 24)] 223 while locationID & 0xf00000: 224 if len(loc) > 1: 225 loc.append('.') 226 loc.append('{}'.format((locationID >> 20) & 0xf)) 227 locationID <<= 4 228 return ''.join(loc) 229 230 231class SuitableSerialInterface(object): 232 pass 233 234 235def scan_interfaces(): 236 """ 237 helper function to scan USB interfaces 238 returns a list of SuitableSerialInterface objects with name and id attributes 239 """ 240 interfaces = [] 241 for service in GetIOServicesByType('IOSerialBSDClient'): 242 device = get_string_property(service, "IOCalloutDevice") 243 if device: 244 usb_device = GetParentDeviceByType(service, "IOUSBInterface") 245 if usb_device: 246 name = get_string_property(usb_device, "USB Interface Name") or None 247 locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) or '' 248 i = SuitableSerialInterface() 249 i.id = locationID 250 i.name = name 251 interfaces.append(i) 252 return interfaces 253 254 255def search_for_locationID_in_interfaces(serial_interfaces, locationID): 256 for interface in serial_interfaces: 257 if (interface.id == locationID): 258 return interface.name 259 return None 260 261 262def comports(include_links=False): 263 # XXX include_links is currently ignored. are links in /dev even supported here? 264 # Scan for all iokit serial ports 265 services = GetIOServicesByType('IOSerialBSDClient') 266 ports = [] 267 serial_interfaces = scan_interfaces() 268 for service in services: 269 # First, add the callout device file. 270 device = get_string_property(service, "IOCalloutDevice") 271 if device: 272 info = list_ports_common.ListPortInfo(device) 273 # If the serial port is implemented by IOUSBDevice 274 # NOTE IOUSBDevice was deprecated as of 10.11 and finally on Apple Silicon 275 # devices has been completely removed. Thanks to @oskay for this patch. 276 usb_device = GetParentDeviceByType(service, "IOUSBHostDevice") 277 if not usb_device: 278 usb_device = GetParentDeviceByType(service, "IOUSBDevice") 279 if usb_device: 280 # fetch some useful informations from properties 281 info.vid = get_int_property(usb_device, "idVendor", kCFNumberSInt16Type) 282 info.pid = get_int_property(usb_device, "idProduct", kCFNumberSInt16Type) 283 info.serial_number = get_string_property(usb_device, kUSBSerialNumberString) 284 # We know this is a usb device, so the 285 # IORegistryEntryName should always be aliased to the 286 # usb product name string descriptor. 287 info.product = IORegistryEntryGetName(usb_device) or 'n/a' 288 info.manufacturer = get_string_property(usb_device, kUSBVendorString) 289 locationID = get_int_property(usb_device, "locationID", kCFNumberSInt32Type) 290 info.location = location_to_string(locationID) 291 info.interface = search_for_locationID_in_interfaces(serial_interfaces, locationID) 292 info.apply_usb_info() 293 ports.append(info) 294 return ports 295 296# test 297if __name__ == '__main__': 298 for port, desc, hwid in sorted(comports()): 299 print("{}: {} [{}]".format(port, desc, hwid)) 300