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