1#! python
2#
3# Enumerate serial ports on Windows including a human readable description
4# and hardware information.
5#
6# This file is part of pySerial. https://github.com/pyserial/pyserial
7# (C) 2001-2016 Chris Liechti <cliechti@gmx.net>
8#
9# SPDX-License-Identifier:    BSD-3-Clause
10
11from __future__ import absolute_import
12
13# pylint: disable=invalid-name,too-few-public-methods
14import re
15import ctypes
16from ctypes.wintypes import BOOL
17from ctypes.wintypes import HWND
18from ctypes.wintypes import DWORD
19from ctypes.wintypes import WORD
20from ctypes.wintypes import LONG
21from ctypes.wintypes import ULONG
22from ctypes.wintypes import HKEY
23from ctypes.wintypes import BYTE
24import serial
25from serial.win32 import ULONG_PTR
26from serial.tools import list_ports_common
27
28
29def ValidHandle(value, func, arguments):
30    if value == 0:
31        raise ctypes.WinError()
32    return value
33
34
35NULL = 0
36HDEVINFO = ctypes.c_void_p
37LPCTSTR = ctypes.c_wchar_p
38PCTSTR = ctypes.c_wchar_p
39PTSTR = ctypes.c_wchar_p
40LPDWORD = PDWORD = ctypes.POINTER(DWORD)
41#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE)
42LPBYTE = PBYTE = ctypes.c_void_p        # XXX avoids error about types
43
44ACCESS_MASK = DWORD
45REGSAM = ACCESS_MASK
46
47
48class GUID(ctypes.Structure):
49    _fields_ = [
50        ('Data1', DWORD),
51        ('Data2', WORD),
52        ('Data3', WORD),
53        ('Data4', BYTE * 8),
54    ]
55
56    def __str__(self):
57        return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format(
58            self.Data1,
59            self.Data2,
60            self.Data3,
61            ''.join(["{:02x}".format(d) for d in self.Data4[:2]]),
62            ''.join(["{:02x}".format(d) for d in self.Data4[2:]]),
63        )
64
65
66class SP_DEVINFO_DATA(ctypes.Structure):
67    _fields_ = [
68        ('cbSize', DWORD),
69        ('ClassGuid', GUID),
70        ('DevInst', DWORD),
71        ('Reserved', ULONG_PTR),
72    ]
73
74    def __str__(self):
75        return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst)
76
77
78PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA)
79
80PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p
81
82setupapi = ctypes.windll.LoadLibrary("setupapi")
83SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList
84SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO]
85SetupDiDestroyDeviceInfoList.restype = BOOL
86
87SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW
88SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD]
89SetupDiClassGuidsFromName.restype = BOOL
90
91SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo
92SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA]
93SetupDiEnumDeviceInfo.restype = BOOL
94
95SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW
96SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD]
97SetupDiGetClassDevs.restype = HDEVINFO
98SetupDiGetClassDevs.errcheck = ValidHandle
99
100SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW
101SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD]
102SetupDiGetDeviceRegistryProperty.restype = BOOL
103
104SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW
105SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD]
106SetupDiGetDeviceInstanceId.restype = BOOL
107
108SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey
109SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM]
110SetupDiOpenDevRegKey.restype = HKEY
111
112advapi32 = ctypes.windll.LoadLibrary("Advapi32")
113RegCloseKey = advapi32.RegCloseKey
114RegCloseKey.argtypes = [HKEY]
115RegCloseKey.restype = LONG
116
117RegQueryValueEx = advapi32.RegQueryValueExW
118RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD]
119RegQueryValueEx.restype = LONG
120
121cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32")
122CM_Get_Parent = cfgmgr32.CM_Get_Parent
123CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG]
124CM_Get_Parent.restype = LONG
125
126CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW
127CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG]
128CM_Get_Device_IDW.restype = LONG
129
130CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err
131CM_MapCrToWin32Err.argtypes = [DWORD, DWORD]
132CM_MapCrToWin32Err.restype = DWORD
133
134
135DIGCF_PRESENT = 2
136DIGCF_DEVICEINTERFACE = 16
137INVALID_HANDLE_VALUE = 0
138ERROR_INSUFFICIENT_BUFFER = 122
139ERROR_NOT_FOUND = 1168
140SPDRP_HARDWAREID = 1
141SPDRP_FRIENDLYNAME = 12
142SPDRP_LOCATION_PATHS = 35
143SPDRP_MFG = 11
144DICS_FLAG_GLOBAL = 1
145DIREG_DEV = 0x00000001
146KEY_READ = 0x20019
147
148
149MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5
150
151
152def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None):
153    """ Get the serial number of the parent of a device.
154
155    Args:
156        child_devinst: The device instance handle to get the parent serial number of.
157        child_vid: The vendor ID of the child device.
158        child_pid: The product ID of the child device.
159        depth: The current iteration depth of the USB device tree.
160    """
161
162    # If the traversal depth is beyond the max, abandon attempting to find the serial number.
163    if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH:
164        return '' if not last_serial_number else last_serial_number
165
166    # Get the parent device instance.
167    devinst = DWORD()
168    ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0)
169
170    if ret:
171        win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0))
172
173        # If there is no parent available, the child was the root device. We cannot traverse
174        # further.
175        if win_error == ERROR_NOT_FOUND:
176            return '' if not last_serial_number else last_serial_number
177
178        raise ctypes.WinError(win_error)
179
180    # Get the ID of the parent device and parse it for vendor ID, product ID, and serial number.
181    parentHardwareID = ctypes.create_unicode_buffer(250)
182
183    ret = CM_Get_Device_IDW(
184        devinst,
185        parentHardwareID,
186        ctypes.sizeof(parentHardwareID) - 1,
187        0)
188
189    if ret:
190        raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0)))
191
192    parentHardwareID_str = parentHardwareID.value
193    m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?',
194                  parentHardwareID_str,
195                  re.I)
196
197    # return early if we have no matches (likely malformed serial, traversed too far)
198    if not m:
199        return '' if not last_serial_number else last_serial_number
200
201    vid = None
202    pid = None
203    serial_number = None
204    if m.group(1):
205        vid = int(m.group(1), 16)
206    if m.group(3):
207        pid = int(m.group(3), 16)
208    if m.group(7):
209        serial_number = m.group(7)
210
211    # store what we found as a fallback for malformed serial values up the chain
212    found_serial_number = serial_number
213
214    # Check that the USB serial number only contains alpha-numeric characters. It may be a windows
215    # device ID (ephemeral ID).
216    if serial_number and not re.match(r'^\w+$', serial_number):
217        serial_number = None
218
219    if not vid or not pid:
220        # If pid and vid are not available at this device level, continue to the parent.
221        return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
222
223    if pid != child_pid or vid != child_vid:
224        # If the VID or PID has changed, we are no longer looking at the same physical device. The
225        # serial number is unknown.
226        return '' if not last_serial_number else last_serial_number
227
228    # In this case, the vid and pid of the parent device are identical to the child. However, if
229    # there still isn't a serial number available, continue to the next parent.
230    if not serial_number:
231        return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number)
232
233    # Finally, the VID and PID are identical to the child and a serial number is present, so return
234    # it.
235    return serial_number
236
237
238def iterate_comports():
239    """Return a generator that yields descriptions for serial ports"""
240    PortsGUIDs = (GUID * 8)()  # so far only seen one used, so hope 8 are enough...
241    ports_guids_size = DWORD()
242    if not SetupDiClassGuidsFromName(
243            "Ports",
244            PortsGUIDs,
245            ctypes.sizeof(PortsGUIDs),
246            ctypes.byref(ports_guids_size)):
247        raise ctypes.WinError()
248
249    ModemsGUIDs = (GUID * 8)()  # so far only seen one used, so hope 8 are enough...
250    modems_guids_size = DWORD()
251    if not SetupDiClassGuidsFromName(
252            "Modem",
253            ModemsGUIDs,
254            ctypes.sizeof(ModemsGUIDs),
255            ctypes.byref(modems_guids_size)):
256        raise ctypes.WinError()
257
258    GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value]
259
260    # repeat for all possible GUIDs
261    for index in range(len(GUIDs)):
262        bInterfaceNumber = None
263        g_hdi = SetupDiGetClassDevs(
264            ctypes.byref(GUIDs[index]),
265            None,
266            NULL,
267            DIGCF_PRESENT)  # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports
268
269        devinfo = SP_DEVINFO_DATA()
270        devinfo.cbSize = ctypes.sizeof(devinfo)
271        index = 0
272        while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)):
273            index += 1
274
275            # get the real com port name
276            hkey = SetupDiOpenDevRegKey(
277                g_hdi,
278                ctypes.byref(devinfo),
279                DICS_FLAG_GLOBAL,
280                0,
281                DIREG_DEV,  # DIREG_DRV for SW info
282                KEY_READ)
283            port_name_buffer = ctypes.create_unicode_buffer(250)
284            port_name_length = ULONG(ctypes.sizeof(port_name_buffer))
285            RegQueryValueEx(
286                hkey,
287                "PortName",
288                None,
289                None,
290                ctypes.byref(port_name_buffer),
291                ctypes.byref(port_name_length))
292            RegCloseKey(hkey)
293
294            # unfortunately does this method also include parallel ports.
295            # we could check for names starting with COM or just exclude LPT
296            # and hope that other "unknown" names are serial ports...
297            if port_name_buffer.value.startswith('LPT'):
298                continue
299
300            # hardware ID
301            szHardwareID = ctypes.create_unicode_buffer(250)
302            # try to get ID that includes serial number
303            if not SetupDiGetDeviceInstanceId(
304                    g_hdi,
305                    ctypes.byref(devinfo),
306                    #~ ctypes.byref(szHardwareID),
307                    szHardwareID,
308                    ctypes.sizeof(szHardwareID) - 1,
309                    None):
310                # fall back to more generic hardware ID if that would fail
311                if not SetupDiGetDeviceRegistryProperty(
312                        g_hdi,
313                        ctypes.byref(devinfo),
314                        SPDRP_HARDWAREID,
315                        None,
316                        ctypes.byref(szHardwareID),
317                        ctypes.sizeof(szHardwareID) - 1,
318                        None):
319                    # Ignore ERROR_INSUFFICIENT_BUFFER
320                    if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
321                        raise ctypes.WinError()
322            # stringify
323            szHardwareID_str = szHardwareID.value
324
325            info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True)
326
327            # in case of USB, make a more readable string, similar to that form
328            # that we also generate on other platforms
329            if szHardwareID_str.startswith('USB'):
330                m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I)
331                if m:
332                    info.vid = int(m.group(1), 16)
333                    if m.group(3):
334                        info.pid = int(m.group(3), 16)
335                    if m.group(5):
336                        bInterfaceNumber = int(m.group(5))
337
338                    # Check that the USB serial number only contains alpha-numeric characters. It
339                    # may be a windows device ID (ephemeral ID) for composite devices.
340                    if m.group(7) and re.match(r'^\w+$', m.group(7)):
341                        info.serial_number = m.group(7)
342                    else:
343                        info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid)
344
345                # calculate a location string
346                loc_path_str = ctypes.create_unicode_buffer(250)
347                if SetupDiGetDeviceRegistryProperty(
348                        g_hdi,
349                        ctypes.byref(devinfo),
350                        SPDRP_LOCATION_PATHS,
351                        None,
352                        ctypes.byref(loc_path_str),
353                        ctypes.sizeof(loc_path_str) - 1,
354                        None):
355                    m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value)
356                    location = []
357                    for g in m:
358                        if g.group(1):
359                            location.append('{:d}'.format(int(g.group(1)) + 1))
360                        else:
361                            if len(location) > 1:
362                                location.append('.')
363                            else:
364                                location.append('-')
365                            location.append(g.group(2))
366                    if bInterfaceNumber is not None:
367                        location.append(':{}.{}'.format(
368                            'x',  # XXX how to determine correct bConfigurationValue?
369                            bInterfaceNumber))
370                    if location:
371                        info.location = ''.join(location)
372                info.hwid = info.usb_info()
373            elif szHardwareID_str.startswith('FTDIBUS'):
374                m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I)
375                if m:
376                    info.vid = int(m.group(1), 16)
377                    info.pid = int(m.group(2), 16)
378                    if m.group(4):
379                        info.serial_number = m.group(4)
380                # USB location is hidden by FDTI driver :(
381                info.hwid = info.usb_info()
382            else:
383                info.hwid = szHardwareID_str
384
385            # friendly name
386            szFriendlyName = ctypes.create_unicode_buffer(250)
387            if SetupDiGetDeviceRegistryProperty(
388                    g_hdi,
389                    ctypes.byref(devinfo),
390                    SPDRP_FRIENDLYNAME,
391                    #~ SPDRP_DEVICEDESC,
392                    None,
393                    ctypes.byref(szFriendlyName),
394                    ctypes.sizeof(szFriendlyName) - 1,
395                    None):
396                info.description = szFriendlyName.value
397            #~ else:
398                # Ignore ERROR_INSUFFICIENT_BUFFER
399                #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER:
400                    #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value))
401                # ignore errors and still include the port in the list, friendly name will be same as port name
402
403            # manufacturer
404            szManufacturer = ctypes.create_unicode_buffer(250)
405            if SetupDiGetDeviceRegistryProperty(
406                    g_hdi,
407                    ctypes.byref(devinfo),
408                    SPDRP_MFG,
409                    #~ SPDRP_DEVICEDESC,
410                    None,
411                    ctypes.byref(szManufacturer),
412                    ctypes.sizeof(szManufacturer) - 1,
413                    None):
414                info.manufacturer = szManufacturer.value
415            yield info
416        SetupDiDestroyDeviceInfoList(g_hdi)
417
418
419def comports(include_links=False):
420    """Return a list of info objects about serial ports"""
421    return list(iterate_comports())
422
423# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
424# test
425if __name__ == '__main__':
426    for port, desc, hwid in sorted(comports()):
427        print("{}: {} [{}]".format(port, desc, hwid))
428