You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

428 lines
16KB

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