You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

300 lines
11KB

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