428 lines
16 KiB
Python
428 lines
16 KiB
Python
#! 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))
|