diff options
Diffstat (limited to 'serial/tools/list_ports_windows.py')
-rw-r--r-- | serial/tools/list_ports_windows.py | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/serial/tools/list_ports_windows.py b/serial/tools/list_ports_windows.py new file mode 100644 index 0000000..0b4a5b1 --- /dev/null +++ b/serial/tools/list_ports_windows.py @@ -0,0 +1,427 @@ +#! python +# +# Enumerate serial ports on Windows including a human readable description +# and hardware information. +# +# This file is part of pySerial. https://github.com/pyserial/pyserial +# (C) 2001-2016 Chris Liechti <cliechti@gmx.net> +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import absolute_import + +# pylint: disable=invalid-name,too-few-public-methods +import re +import ctypes +from ctypes.wintypes import BOOL +from ctypes.wintypes import HWND +from ctypes.wintypes import DWORD +from ctypes.wintypes import WORD +from ctypes.wintypes import LONG +from ctypes.wintypes import ULONG +from ctypes.wintypes import HKEY +from ctypes.wintypes import BYTE +import serial +from serial.win32 import ULONG_PTR +from serial.tools import list_ports_common + + +def ValidHandle(value, func, arguments): + if value == 0: + raise ctypes.WinError() + return value + + +NULL = 0 +HDEVINFO = ctypes.c_void_p +LPCTSTR = ctypes.c_wchar_p +PCTSTR = ctypes.c_wchar_p +PTSTR = ctypes.c_wchar_p +LPDWORD = PDWORD = ctypes.POINTER(DWORD) +#~ LPBYTE = PBYTE = ctypes.POINTER(BYTE) +LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types + +ACCESS_MASK = DWORD +REGSAM = ACCESS_MASK + + +class GUID(ctypes.Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8), + ] + + def __str__(self): + return "{{{:08x}-{:04x}-{:04x}-{}-{}}}".format( + self.Data1, + self.Data2, + self.Data3, + ''.join(["{:02x}".format(d) for d in self.Data4[:2]]), + ''.join(["{:02x}".format(d) for d in self.Data4[2:]]), + ) + + +class SP_DEVINFO_DATA(ctypes.Structure): + _fields_ = [ + ('cbSize', DWORD), + ('ClassGuid', GUID), + ('DevInst', DWORD), + ('Reserved', ULONG_PTR), + ] + + def __str__(self): + return "ClassGuid:{} DevInst:{}".format(self.ClassGuid, self.DevInst) + + +PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA) + +PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p + +setupapi = ctypes.windll.LoadLibrary("setupapi") +SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList +SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO] +SetupDiDestroyDeviceInfoList.restype = BOOL + +SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameW +SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD] +SetupDiClassGuidsFromName.restype = BOOL + +SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo +SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA] +SetupDiEnumDeviceInfo.restype = BOOL + +SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsW +SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD] +SetupDiGetClassDevs.restype = HDEVINFO +SetupDiGetClassDevs.errcheck = ValidHandle + +SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyW +SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD] +SetupDiGetDeviceRegistryProperty.restype = BOOL + +SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdW +SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD] +SetupDiGetDeviceInstanceId.restype = BOOL + +SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey +SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM] +SetupDiOpenDevRegKey.restype = HKEY + +advapi32 = ctypes.windll.LoadLibrary("Advapi32") +RegCloseKey = advapi32.RegCloseKey +RegCloseKey.argtypes = [HKEY] +RegCloseKey.restype = LONG + +RegQueryValueEx = advapi32.RegQueryValueExW +RegQueryValueEx.argtypes = [HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD] +RegQueryValueEx.restype = LONG + +cfgmgr32 = ctypes.windll.LoadLibrary("Cfgmgr32") +CM_Get_Parent = cfgmgr32.CM_Get_Parent +CM_Get_Parent.argtypes = [PDWORD, DWORD, ULONG] +CM_Get_Parent.restype = LONG + +CM_Get_Device_IDW = cfgmgr32.CM_Get_Device_IDW +CM_Get_Device_IDW.argtypes = [DWORD, PTSTR, ULONG, ULONG] +CM_Get_Device_IDW.restype = LONG + +CM_MapCrToWin32Err = cfgmgr32.CM_MapCrToWin32Err +CM_MapCrToWin32Err.argtypes = [DWORD, DWORD] +CM_MapCrToWin32Err.restype = DWORD + + +DIGCF_PRESENT = 2 +DIGCF_DEVICEINTERFACE = 16 +INVALID_HANDLE_VALUE = 0 +ERROR_INSUFFICIENT_BUFFER = 122 +ERROR_NOT_FOUND = 1168 +SPDRP_HARDWAREID = 1 +SPDRP_FRIENDLYNAME = 12 +SPDRP_LOCATION_PATHS = 35 +SPDRP_MFG = 11 +DICS_FLAG_GLOBAL = 1 +DIREG_DEV = 0x00000001 +KEY_READ = 0x20019 + + +MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH = 5 + + +def get_parent_serial_number(child_devinst, child_vid, child_pid, depth=0, last_serial_number=None): + """ Get the serial number of the parent of a device. + + Args: + child_devinst: The device instance handle to get the parent serial number of. + child_vid: The vendor ID of the child device. + child_pid: The product ID of the child device. + depth: The current iteration depth of the USB device tree. + """ + + # If the traversal depth is beyond the max, abandon attempting to find the serial number. + if depth > MAX_USB_DEVICE_TREE_TRAVERSAL_DEPTH: + return '' if not last_serial_number else last_serial_number + + # Get the parent device instance. + devinst = DWORD() + ret = CM_Get_Parent(ctypes.byref(devinst), child_devinst, 0) + + if ret: + win_error = CM_MapCrToWin32Err(DWORD(ret), DWORD(0)) + + # If there is no parent available, the child was the root device. We cannot traverse + # further. + if win_error == ERROR_NOT_FOUND: + return '' if not last_serial_number else last_serial_number + + raise ctypes.WinError(win_error) + + # Get the ID of the parent device and parse it for vendor ID, product ID, and serial number. + parentHardwareID = ctypes.create_unicode_buffer(250) + + ret = CM_Get_Device_IDW( + devinst, + parentHardwareID, + ctypes.sizeof(parentHardwareID) - 1, + 0) + + if ret: + raise ctypes.WinError(CM_MapCrToWin32Err(DWORD(ret), DWORD(0))) + + parentHardwareID_str = parentHardwareID.value + m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', + parentHardwareID_str, + re.I) + + # return early if we have no matches (likely malformed serial, traversed too far) + if not m: + return '' if not last_serial_number else last_serial_number + + vid = None + pid = None + serial_number = None + if m.group(1): + vid = int(m.group(1), 16) + if m.group(3): + pid = int(m.group(3), 16) + if m.group(7): + serial_number = m.group(7) + + # store what we found as a fallback for malformed serial values up the chain + found_serial_number = serial_number + + # Check that the USB serial number only contains alpha-numeric characters. It may be a windows + # device ID (ephemeral ID). + if serial_number and not re.match(r'^\w+$', serial_number): + serial_number = None + + if not vid or not pid: + # If pid and vid are not available at this device level, continue to the parent. + return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number) + + if pid != child_pid or vid != child_vid: + # If the VID or PID has changed, we are no longer looking at the same physical device. The + # serial number is unknown. + return '' if not last_serial_number else last_serial_number + + # In this case, the vid and pid of the parent device are identical to the child. However, if + # there still isn't a serial number available, continue to the next parent. + if not serial_number: + return get_parent_serial_number(devinst, child_vid, child_pid, depth + 1, found_serial_number) + + # Finally, the VID and PID are identical to the child and a serial number is present, so return + # it. + return serial_number + + +def iterate_comports(): + """Return a generator that yields descriptions for serial ports""" + PortsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough... + ports_guids_size = DWORD() + if not SetupDiClassGuidsFromName( + "Ports", + PortsGUIDs, + ctypes.sizeof(PortsGUIDs), + ctypes.byref(ports_guids_size)): + raise ctypes.WinError() + + ModemsGUIDs = (GUID * 8)() # so far only seen one used, so hope 8 are enough... + modems_guids_size = DWORD() + if not SetupDiClassGuidsFromName( + "Modem", + ModemsGUIDs, + ctypes.sizeof(ModemsGUIDs), + ctypes.byref(modems_guids_size)): + raise ctypes.WinError() + + GUIDs = PortsGUIDs[:ports_guids_size.value] + ModemsGUIDs[:modems_guids_size.value] + + # repeat for all possible GUIDs + for index in range(len(GUIDs)): + bInterfaceNumber = None + g_hdi = SetupDiGetClassDevs( + ctypes.byref(GUIDs[index]), + None, + NULL, + DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports + + devinfo = SP_DEVINFO_DATA() + devinfo.cbSize = ctypes.sizeof(devinfo) + index = 0 + while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)): + index += 1 + + # get the real com port name + hkey = SetupDiOpenDevRegKey( + g_hdi, + ctypes.byref(devinfo), + DICS_FLAG_GLOBAL, + 0, + DIREG_DEV, # DIREG_DRV for SW info + KEY_READ) + port_name_buffer = ctypes.create_unicode_buffer(250) + port_name_length = ULONG(ctypes.sizeof(port_name_buffer)) + RegQueryValueEx( + hkey, + "PortName", + None, + None, + ctypes.byref(port_name_buffer), + ctypes.byref(port_name_length)) + RegCloseKey(hkey) + + # unfortunately does this method also include parallel ports. + # we could check for names starting with COM or just exclude LPT + # and hope that other "unknown" names are serial ports... + if port_name_buffer.value.startswith('LPT'): + continue + + # hardware ID + szHardwareID = ctypes.create_unicode_buffer(250) + # try to get ID that includes serial number + if not SetupDiGetDeviceInstanceId( + g_hdi, + ctypes.byref(devinfo), + #~ ctypes.byref(szHardwareID), + szHardwareID, + ctypes.sizeof(szHardwareID) - 1, + None): + # fall back to more generic hardware ID if that would fail + if not SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_HARDWAREID, + None, + ctypes.byref(szHardwareID), + ctypes.sizeof(szHardwareID) - 1, + None): + # Ignore ERROR_INSUFFICIENT_BUFFER + if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: + raise ctypes.WinError() + # stringify + szHardwareID_str = szHardwareID.value + + info = list_ports_common.ListPortInfo(port_name_buffer.value, skip_link_detection=True) + + # in case of USB, make a more readable string, similar to that form + # that we also generate on other platforms + if szHardwareID_str.startswith('USB'): + m = re.search(r'VID_([0-9a-f]{4})(&PID_([0-9a-f]{4}))?(&MI_(\d{2}))?(\\(.*))?', szHardwareID_str, re.I) + if m: + info.vid = int(m.group(1), 16) + if m.group(3): + info.pid = int(m.group(3), 16) + if m.group(5): + bInterfaceNumber = int(m.group(5)) + + # Check that the USB serial number only contains alpha-numeric characters. It + # may be a windows device ID (ephemeral ID) for composite devices. + if m.group(7) and re.match(r'^\w+$', m.group(7)): + info.serial_number = m.group(7) + else: + info.serial_number = get_parent_serial_number(devinfo.DevInst, info.vid, info.pid) + + # calculate a location string + loc_path_str = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_LOCATION_PATHS, + None, + ctypes.byref(loc_path_str), + ctypes.sizeof(loc_path_str) - 1, + None): + m = re.finditer(r'USBROOT\((\w+)\)|#USB\((\w+)\)', loc_path_str.value) + location = [] + for g in m: + if g.group(1): + location.append('{:d}'.format(int(g.group(1)) + 1)) + else: + if len(location) > 1: + location.append('.') + else: + location.append('-') + location.append(g.group(2)) + if bInterfaceNumber is not None: + location.append(':{}.{}'.format( + 'x', # XXX how to determine correct bConfigurationValue? + bInterfaceNumber)) + if location: + info.location = ''.join(location) + info.hwid = info.usb_info() + elif szHardwareID_str.startswith('FTDIBUS'): + m = re.search(r'VID_([0-9a-f]{4})\+PID_([0-9a-f]{4})(\+(\w+))?', szHardwareID_str, re.I) + if m: + info.vid = int(m.group(1), 16) + info.pid = int(m.group(2), 16) + if m.group(4): + info.serial_number = m.group(4) + # USB location is hidden by FDTI driver :( + info.hwid = info.usb_info() + else: + info.hwid = szHardwareID_str + + # friendly name + szFriendlyName = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_FRIENDLYNAME, + #~ SPDRP_DEVICEDESC, + None, + ctypes.byref(szFriendlyName), + ctypes.sizeof(szFriendlyName) - 1, + None): + info.description = szFriendlyName.value + #~ else: + # Ignore ERROR_INSUFFICIENT_BUFFER + #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: + #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value)) + # ignore errors and still include the port in the list, friendly name will be same as port name + + # manufacturer + szManufacturer = ctypes.create_unicode_buffer(250) + if SetupDiGetDeviceRegistryProperty( + g_hdi, + ctypes.byref(devinfo), + SPDRP_MFG, + #~ SPDRP_DEVICEDESC, + None, + ctypes.byref(szManufacturer), + ctypes.sizeof(szManufacturer) - 1, + None): + info.manufacturer = szManufacturer.value + yield info + SetupDiDestroyDeviceInfoList(g_hdi) + + +def comports(include_links=False): + """Return a list of info objects about serial ports""" + return list(iterate_comports()) + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# test +if __name__ == '__main__': + for port, desc, hwid in sorted(comports()): + print("{}: {} [{}]".format(port, desc, hwid)) |