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