1import ctypes
2import re
3
4def ValidHandle(value, func, arguments):
5    if value == 0:
6        raise ctypes.WinError()
7    return value
8
9import serial
10from serial.win32 import ULONG_PTR, is_64bit
11from ctypes.wintypes import HANDLE
12from ctypes.wintypes import BOOL
13from ctypes.wintypes import HWND
14from ctypes.wintypes import DWORD
15from ctypes.wintypes import WORD
16from ctypes.wintypes import LONG
17from ctypes.wintypes import ULONG
18from ctypes.wintypes import LPCSTR
19from ctypes.wintypes import HKEY
20from ctypes.wintypes import BYTE
21
22NULL = 0
23HDEVINFO = ctypes.c_void_p
24PCTSTR = ctypes.c_char_p
25PTSTR = ctypes.c_void_p
26CHAR = ctypes.c_char
27LPDWORD = PDWORD = ctypes.POINTER(DWORD)
28#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE)
29LPBYTE = PBYTE = ctypes.c_void_p        # XXX avoids error about types
30
31ACCESS_MASK = DWORD
32REGSAM = ACCESS_MASK
33
34
35def byte_buffer(length):
36    """Get a buffer for a string"""
37    return (BYTE*length)()
38
39def string(buffer):
40    s = []
41    for c in buffer:
42        if c == 0: break
43        s.append(chr(c & 0xff)) # "& 0xff": hack to convert signed to unsigned
44    return ''.join(s)
45
46
47class GUID(ctypes.Structure):
48    _fields_ = [
49        ('Data1', DWORD),
50        ('Data2', WORD),
51        ('Data3', WORD),
52        ('Data4', BYTE*8),
53    ]
54    def __str__(self):
55        return "{%08x-%04x-%04x-%s-%s}" % (
56            self.Data1,
57            self.Data2,
58            self.Data3,
59            ''.join(["%02x" % d for d in self.Data4[:2]]),
60            ''.join(["%02x" % d for d in self.Data4[2:]]),
61        )
62
63class SP_DEVINFO_DATA(ctypes.Structure):
64    _fields_ = [
65        ('cbSize', DWORD),
66        ('ClassGuid', GUID),
67        ('DevInst', DWORD),
68        ('Reserved', ULONG_PTR),
69    ]
70    def __str__(self):
71        return "ClassGuid:%s DevInst:%s" % (self.ClassGuid, self.DevInst)
72PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)
73
74PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p
75
76setupapi = ctypes.windll.LoadLibrary("setupapi")
77SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList
78SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
79SetupDiDestroyDeviceInfoList.restype = BOOL
80
81SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameA
82SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD]
83SetupDiClassGuidsFromName.restype = BOOL
84
85SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo
86SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA]
87SetupDiEnumDeviceInfo.restype = BOOL
88
89SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsA
90SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
91SetupDiGetClassDevs.restype = HDEVINFO
92SetupDiGetClassDevs.errcheck = ValidHandle
93
94SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyA
95SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD]
96SetupDiGetDeviceRegistryProperty.restype = BOOL
97
98SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdA
99SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD]
100SetupDiGetDeviceInstanceId.restype = BOOL
101
102SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey
103SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM]
104SetupDiOpenDevRegKey.restype = HKEY
105
106advapi32 = ctypes.windll.LoadLibrary("Advapi32")
107RegCloseKey = advapi32.RegCloseKey
108RegCloseKey.argtypes = [HKEY]
109RegCloseKey.restype = LONG
110
111RegQueryValueEx = advapi32.RegQueryValueExA
112RegQueryValueEx.argtypes = [HKEY, LPCSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
113RegQueryValueEx.restype = LONG
114
115
116DIGCF_PRESENT = 2
117DIGCF_DEVICEINTERFACE = 16
118INVALID_HANDLE_VALUE = 0
119ERROR_INSUFFICIENT_BUFFER = 122
120SPDRP_HARDWAREID = 1
121SPDRP_FRIENDLYNAME = 12
122DICS_FLAG_GLOBAL = 1
123DIREG_DEV = 0x00000001
124KEY_READ = 0x20019
125
126# workaround for compatibility between Python 2.x and 3.x
127Ports = serial.to_bytes([80, 111, 114, 116, 115]) # "Ports"
128PortName = serial.to_bytes([80, 111, 114, 116, 78, 97, 109, 101]) # "PortName"
129
130def comports():
131    GUIDs = (GUID*8)() # so far only seen one used, so hope 8 are enough...
132    guids_size = DWORD()
133    if not SetupDiClassGuidsFromName(
134            Ports,
135            GUIDs,
136            ctypes.sizeof(GUIDs),
137            ctypes.byref(guids_size)):
138        raise ctypes.WinError()
139
140    # repeat for all possible GUIDs
141    for index in range(guids_size.value):
142        g_hdi = SetupDiGetClassDevs(
143                ctypes.byref(GUIDs[index]),
144                None,
145                NULL,
146                DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports
147
148        devinfo = SP_DEVINFO_DATA()
149        devinfo.cbSize = ctypes.sizeof(devinfo)
150        index = 0
151        while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)):
152            index += 1
153
154            # get the real com port name
155            hkey = SetupDiOpenDevRegKey(
156                    g_hdi,
157                    ctypes.byref(devinfo),
158                    DICS_FLAG_GLOBAL,
159                    0,
160                    DIREG_DEV,  # DIREG_DRV for SW info
161                    KEY_READ)
162            port_name_buffer = byte_buffer(250)
163            port_name_length = ULONG(ctypes.sizeof(port_name_buffer))
164            RegQueryValueEx(
165                    hkey,
166                    PortName,
167                    None,
168                    None,
169                    ctypes.byref(port_name_buffer),
170                    ctypes.byref(port_name_length))
171            RegCloseKey(hkey)
172
173            # unfortunately does this method also include parallel ports.
174            # we could check for names starting with COM or just exclude LPT
175            # and hope that other "unknown" names are serial ports...
176            if string(port_name_buffer).startswith('LPT'):
177                continue
178
179            # hardware ID
180            szHardwareID = byte_buffer(250)
181            # try to get ID that includes serial number
182            if not SetupDiGetDeviceInstanceId(
183                    g_hdi,
184                    ctypes.byref(devinfo),
185                    ctypes.byref(szHardwareID),
186                    ctypes.sizeof(szHardwareID) - 1,
187                    None):
188                # fall back to more generic hardware ID if that would fail
189                if not SetupDiGetDeviceRegistryProperty(
190                        g_hdi,
191                        ctypes.byref(devinfo),
192                        SPDRP_HARDWAREID,
193                        None,
194                        ctypes.byref(szHardwareID),
195                        ctypes.sizeof(szHardwareID) - 1,
196                        None):
197                    # Ignore ERROR_INSUFFICIENT_BUFFER
198                    if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
199                        raise ctypes.WinError()
200            # stringify
201            szHardwareID_str = string(szHardwareID)
202
203            # in case of USB, make a more readable string, similar to that form
204            # that we also generate on other platforms
205            if szHardwareID_str.startswith('USB'):
206                m = re.search(r'VID_([0-9a-f]{4})&PID_([0-9a-f]{4})(\\(\w+))?', szHardwareID_str, re.I)
207                if m:
208                    if m.group(4):
209                        szHardwareID_str = 'USB VID:PID=%s:%s SNR=%s' % (m.group(1), m.group(2), m.group(4))
210                    else:
211                        szHardwareID_str = 'USB VID:PID=%s:%s' % (m.group(1), m.group(2))
212
213            # friendly name
214            szFriendlyName = byte_buffer(250)
215            if not SetupDiGetDeviceRegistryProperty(
216                    g_hdi,
217                    ctypes.byref(devinfo),
218                    SPDRP_FRIENDLYNAME,
219                    #~ SPDRP_DEVICEDESC,
220                    None,
221                    ctypes.byref(szFriendlyName),
222                    ctypes.sizeof(szFriendlyName) - 1,
223                    None):
224                # Ignore ERROR_INSUFFICIENT_BUFFER
225                #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
226                    #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value))
227                # ignore errors and still include the port in the list, friendly name will be same as port name
228                yield string(port_name_buffer), 'n/a', szHardwareID_str
229            else:
230                yield string(port_name_buffer), string(szFriendlyName), szHardwareID_str
231
232        SetupDiDestroyDeviceInfoList(g_hdi)
233
234# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
235# test
236if __name__ == '__main__':
237    import serial
238
239    for port, desc, hwid in sorted(comports()):
240        print "%s: %s [%s]" % (port, desc, hwid)
241