1#!/usr/bin/env python
2
3# portable serial port access with python
4#
5# This is a module that gathers a list of serial ports including details on OSX
6#
7# code originally from https://github.com/makerbot/pyserial/tree/master/serial/tools
8# with contributions from cibomahto, dgs3, FarMcKon, tedbrandston
9# and modifications by cliechti
10#
11# this is distributed under a free software license, see license.txt
12
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
24import ctypes
25from ctypes import util
26import re
27
28iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit'))
29cf = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation'))
30
31kIOMasterPortDefault = ctypes.c_void_p.in_dll(iokit, "kIOMasterPortDefault")
32kCFAllocatorDefault = ctypes.c_void_p.in_dll(cf, "kCFAllocatorDefault")
33
34kCFStringEncodingMacRoman = 0
35
36iokit.IOServiceMatching.restype = ctypes.c_void_p
37
38iokit.IOServiceGetMatchingServices.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
39iokit.IOServiceGetMatchingServices.restype = ctypes.c_void_p
40
41iokit.IORegistryEntryGetParentEntry.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
42
43iokit.IORegistryEntryCreateCFProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32]
44iokit.IORegistryEntryCreateCFProperty.restype = ctypes.c_void_p
45
46iokit.IORegistryEntryGetPath.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
47iokit.IORegistryEntryGetPath.restype = ctypes.c_void_p
48
49iokit.IORegistryEntryGetName.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
50iokit.IORegistryEntryGetName.restype = ctypes.c_void_p
51
52iokit.IOObjectGetClass.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
53iokit.IOObjectGetClass.restype = ctypes.c_void_p
54
55iokit.IOObjectRelease.argtypes = [ctypes.c_void_p]
56
57
58cf.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32]
59cf.CFStringCreateWithCString.restype = ctypes.c_void_p
60
61cf.CFStringGetCStringPtr.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
62cf.CFStringGetCStringPtr.restype = ctypes.c_char_p
63
64cf.CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p]
65cf.CFNumberGetValue.restype = ctypes.c_void_p
66
67def get_string_property(device_t, property):
68    """ Search the given device for the specified string property
69
70    @param device_t Device to search
71    @param property String to search for.
72    @return Python string containing the value, or None if not found.
73    """
74    key = cf.CFStringCreateWithCString(
75        kCFAllocatorDefault,
76        property.encode("mac_roman"),
77        kCFStringEncodingMacRoman
78    )
79
80    CFContainer = iokit.IORegistryEntryCreateCFProperty(
81        device_t,
82        key,
83        kCFAllocatorDefault,
84        0
85    );
86
87    output = None
88
89    if CFContainer:
90        output = cf.CFStringGetCStringPtr(CFContainer, 0)
91
92    return output
93
94def get_int_property(device_t, property):
95    """ Search the given device for the specified string property
96
97    @param device_t Device to search
98    @param property String to search for.
99    @return Python string containing the value, or None if not found.
100    """
101    key = cf.CFStringCreateWithCString(
102        kCFAllocatorDefault,
103        property.encode("mac_roman"),
104        kCFStringEncodingMacRoman
105    )
106
107    CFContainer = iokit.IORegistryEntryCreateCFProperty(
108        device_t,
109        key,
110        kCFAllocatorDefault,
111        0
112    );
113
114    number = ctypes.c_uint16()
115
116    if CFContainer:
117        output = cf.CFNumberGetValue(CFContainer, 2, ctypes.byref(number))
118
119    return number.value
120
121def IORegistryEntryGetName(device):
122    pathname = ctypes.create_string_buffer(100) # TODO: Is this ok?
123    iokit.IOObjectGetClass(
124        device,
125        ctypes.byref(pathname)
126    )
127
128    return pathname.value
129
130def GetParentDeviceByType(device, parent_type):
131    """ Find the first parent of a device that implements the parent_type
132        @param IOService Service to inspect
133        @return Pointer to the parent type, or None if it was not found.
134    """
135    # First, try to walk up the IOService tree to find a parent of this device that is a IOUSBDevice.
136    while IORegistryEntryGetName(device) != parent_type:
137        parent = ctypes.c_void_p()
138        response = iokit.IORegistryEntryGetParentEntry(
139            device,
140            "IOService".encode("mac_roman"),
141            ctypes.byref(parent)
142        )
143
144        # If we weren't able to find a parent for the device, we're done.
145        if response != 0:
146            return None
147
148        device = parent
149
150    return device
151
152def GetIOServicesByType(service_type):
153    """
154    """
155    serial_port_iterator = ctypes.c_void_p()
156
157    response = iokit.IOServiceGetMatchingServices(
158        kIOMasterPortDefault,
159        iokit.IOServiceMatching(service_type),
160        ctypes.byref(serial_port_iterator)
161    )
162
163    services = []
164    while iokit.IOIteratorIsValid(serial_port_iterator):
165        service = iokit.IOIteratorNext(serial_port_iterator)
166        if not service:
167            break
168        services.append(service)
169
170    iokit.IOObjectRelease(serial_port_iterator)
171
172    return services
173
174def comports():
175    # Scan for all iokit serial ports
176    services = GetIOServicesByType('IOSerialBSDClient')
177
178    ports = []
179    for service in services:
180        info = []
181
182        # First, add the callout device file.
183        info.append(get_string_property(service, "IOCalloutDevice"))
184
185        # If the serial port is implemented by a
186        usb_device = GetParentDeviceByType(service, "IOUSBDevice")
187        if usb_device != None:
188            info.append(get_string_property(usb_device, "USB Product Name"))
189
190            info.append(
191                "USB VID:PID=%x:%x SNR=%s"%(
192                get_int_property(usb_device, "idVendor"),
193                get_int_property(usb_device, "idProduct"),
194                get_string_property(usb_device, "USB Serial Number"))
195            )
196        else:
197           info.append('n/a')
198           info.append('n/a')
199
200        ports.append(info)
201
202    return ports
203
204# test
205if __name__ == '__main__':
206    for port, desc, hwid in sorted(comports()):
207        print "%s: %s [%s]" % (port, desc, hwid)
208
209