1 import ctypes 2 import re 3 4 def ValidHandle(value, func, arguments): 5 if value == 0: 6 raise ctypes.WinError() 7 return value 8 9 import serial 10 from serial.win32 import ULONG_PTR, is_64bit 11 from ctypes.wintypes import HANDLE 12 from ctypes.wintypes import BOOL 13 from ctypes.wintypes import HWND 14 from ctypes.wintypes import DWORD 15 from ctypes.wintypes import WORD 16 from ctypes.wintypes import LONG 17 from ctypes.wintypes import ULONG 18 from ctypes.wintypes import LPCSTR 19 from ctypes.wintypes import HKEY 20 from ctypes.wintypes import BYTE 21 22 NULL = 0 23 HDEVINFO = ctypes.c_void_p 24 PCTSTR = ctypes.c_char_p 25 PTSTR = ctypes.c_void_p 26 CHAR = ctypes.c_char 27 LPDWORD = PDWORD = ctypes.POINTER(DWORD) 28 #~ LPBYTE = PBYTE = ctypes.POINTER(BYTE) 29 LPBYTE = PBYTE = ctypes.c_void_p # XXX avoids error about types 30 31 ACCESS_MASK = DWORD 32 REGSAM = ACCESS_MASK 33 34 35 def byte_buffer(length): 36 """Get a buffer for a string""" 37 return (BYTE*length)() 38 39 def string(buffer): 40 s = [] 41 for c in buffer: 42 if c == 0: break 43 s.append(chr(c & 0xff)) # "& 0xff": hack to convert signed to unsigned 44 return ''.join(s) 45 46 47 class GUID(ctypes.Structure): 48 _fields_ = [ 49 ('Data1', DWORD), 50 ('Data2', WORD), 51 ('Data3', WORD), 52 ('Data4', BYTE*8), 53 ] 54 def __str__(self): 55 return "{%08x-%04x-%04x-%s-%s}" % ( 56 self.Data1, 57 self.Data2, 58 self.Data3, 59 ''.join(["%02x" % d for d in self.Data4[:2]]), 60 ''.join(["%02x" % d for d in self.Data4[2:]]), 61 ) 62 63 class SP_DEVINFO_DATA(ctypes.Structure): 64 _fields_ = [ 65 ('cbSize', DWORD), 66 ('ClassGuid', GUID), 67 ('DevInst', DWORD), 68 ('Reserved', ULONG_PTR), 69 ] 70 def __str__(self): 71 return "ClassGuid:%s DevInst:%s" % (self.ClassGuid, self.DevInst) 72 PSP_DEVINFO_DATA = ctypes.POINTER(SP_DEVINFO_DATA) 73 74 PSP_DEVICE_INTERFACE_DETAIL_DATA = ctypes.c_void_p 75 76 setupapi = ctypes.windll.LoadLibrary("setupapi") 77 SetupDiDestroyDeviceInfoList = setupapi.SetupDiDestroyDeviceInfoList 78 SetupDiDestroyDeviceInfoList.argtypes = [HDEVINFO] 79 SetupDiDestroyDeviceInfoList.restype = BOOL 80 81 SetupDiClassGuidsFromName = setupapi.SetupDiClassGuidsFromNameA 82 SetupDiClassGuidsFromName.argtypes = [PCTSTR, ctypes.POINTER(GUID), DWORD, PDWORD] 83 SetupDiClassGuidsFromName.restype = BOOL 84 85 SetupDiEnumDeviceInfo = setupapi.SetupDiEnumDeviceInfo 86 SetupDiEnumDeviceInfo.argtypes = [HDEVINFO, DWORD, PSP_DEVINFO_DATA] 87 SetupDiEnumDeviceInfo.restype = BOOL 88 89 SetupDiGetClassDevs = setupapi.SetupDiGetClassDevsA 90 SetupDiGetClassDevs.argtypes = [ctypes.POINTER(GUID), PCTSTR, HWND, DWORD] 91 SetupDiGetClassDevs.restype = HDEVINFO 92 SetupDiGetClassDevs.errcheck = ValidHandle 93 94 SetupDiGetDeviceRegistryProperty = setupapi.SetupDiGetDeviceRegistryPropertyA 95 SetupDiGetDeviceRegistryProperty.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, PDWORD, PBYTE, DWORD, PDWORD] 96 SetupDiGetDeviceRegistryProperty.restype = BOOL 97 98 SetupDiGetDeviceInstanceId = setupapi.SetupDiGetDeviceInstanceIdA 99 SetupDiGetDeviceInstanceId.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, PTSTR, DWORD, PDWORD] 100 SetupDiGetDeviceInstanceId.restype = BOOL 101 102 SetupDiOpenDevRegKey = setupapi.SetupDiOpenDevRegKey 103 SetupDiOpenDevRegKey.argtypes = [HDEVINFO, PSP_DEVINFO_DATA, DWORD, DWORD, DWORD, REGSAM] 104 SetupDiOpenDevRegKey.restype = HKEY 105 106 advapi32 = ctypes.windll.LoadLibrary("Advapi32") 107 RegCloseKey = advapi32.RegCloseKey 108 RegCloseKey.argtypes = [HKEY] 109 RegCloseKey.restype = LONG 110 111 RegQueryValueEx = advapi32.RegQueryValueExA 112 RegQueryValueEx.argtypes = [HKEY, LPCSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD] 113 RegQueryValueEx.restype = LONG 114 115 116 DIGCF_PRESENT = 2 117 DIGCF_DEVICEINTERFACE = 16 118 INVALID_HANDLE_VALUE = 0 119 ERROR_INSUFFICIENT_BUFFER = 122 120 SPDRP_HARDWAREID = 1 121 SPDRP_FRIENDLYNAME = 12 122 DICS_FLAG_GLOBAL = 1 123 DIREG_DEV = 0x00000001 124 KEY_READ = 0x20019 125 126 # workaround for compatibility between Python 2.x and 3.x 127 Ports = serial.to_bytes([80, 111, 114, 116, 115]) # "Ports" 128 PortName = serial.to_bytes([80, 111, 114, 116, 78, 97, 109, 101]) # "PortName" 129 130 def comports(): 131 GUIDs = (GUID*8)() # so far only seen one used, so hope 8 are enough... 132 guids_size = DWORD() 133 if not SetupDiClassGuidsFromName( 134 Ports, 135 GUIDs, 136 ctypes.sizeof(GUIDs), 137 ctypes.byref(guids_size)): 138 raise ctypes.WinError() 139 140 # repeat for all possible GUIDs 141 for index in range(guids_size.value): 142 g_hdi = SetupDiGetClassDevs( 143 ctypes.byref(GUIDs[index]), 144 None, 145 NULL, 146 DIGCF_PRESENT) # was DIGCF_PRESENT|DIGCF_DEVICEINTERFACE which misses CDC ports 147 148 devinfo = SP_DEVINFO_DATA() 149 devinfo.cbSize = ctypes.sizeof(devinfo) 150 index = 0 151 while SetupDiEnumDeviceInfo(g_hdi, index, ctypes.byref(devinfo)): 152 index += 1 153 154 # get the real com port name 155 hkey = SetupDiOpenDevRegKey( 156 g_hdi, 157 ctypes.byref(devinfo), 158 DICS_FLAG_GLOBAL, 159 0, 160 DIREG_DEV, # DIREG_DRV for SW info 161 KEY_READ) 162 port_name_buffer = byte_buffer(250) 163 port_name_length = ULONG(ctypes.sizeof(port_name_buffer)) 164 RegQueryValueEx( 165 hkey, 166 PortName, 167 None, 168 None, 169 ctypes.byref(port_name_buffer), 170 ctypes.byref(port_name_length)) 171 RegCloseKey(hkey) 172 173 # unfortunately does this method also include parallel ports. 174 # we could check for names starting with COM or just exclude LPT 175 # and hope that other "unknown" names are serial ports... 176 if string(port_name_buffer).startswith('LPT'): 177 continue 178 179 # hardware ID 180 szHardwareID = byte_buffer(250) 181 # try to get ID that includes serial number 182 if not SetupDiGetDeviceInstanceId( 183 g_hdi, 184 ctypes.byref(devinfo), 185 ctypes.byref(szHardwareID), 186 ctypes.sizeof(szHardwareID) - 1, 187 None): 188 # fall back to more generic hardware ID if that would fail 189 if not SetupDiGetDeviceRegistryProperty( 190 g_hdi, 191 ctypes.byref(devinfo), 192 SPDRP_HARDWAREID, 193 None, 194 ctypes.byref(szHardwareID), 195 ctypes.sizeof(szHardwareID) - 1, 196 None): 197 # Ignore ERROR_INSUFFICIENT_BUFFER 198 if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: 199 raise ctypes.WinError() 200 # stringify 201 szHardwareID_str = string(szHardwareID) 202 203 # in case of USB, make a more readable string, similar to that form 204 # that we also generate on other platforms 205 if szHardwareID_str.startswith('USB'): 206 m = re.search(r'VID_([0-9a-f]{4})&PID_([0-9a-f]{4})(\\(\w+))?', szHardwareID_str, re.I) 207 if m: 208 if m.group(4): 209 szHardwareID_str = 'USB VID:PID=%s:%s SNR=%s' % (m.group(1), m.group(2), m.group(4)) 210 else: 211 szHardwareID_str = 'USB VID:PID=%s:%s' % (m.group(1), m.group(2)) 212 213 # friendly name 214 szFriendlyName = byte_buffer(250) 215 if not SetupDiGetDeviceRegistryProperty( 216 g_hdi, 217 ctypes.byref(devinfo), 218 SPDRP_FRIENDLYNAME, 219 #~ SPDRP_DEVICEDESC, 220 None, 221 ctypes.byref(szFriendlyName), 222 ctypes.sizeof(szFriendlyName) - 1, 223 None): 224 # Ignore ERROR_INSUFFICIENT_BUFFER 225 #~ if ctypes.GetLastError() != ERROR_INSUFFICIENT_BUFFER: 226 #~ raise IOError("failed to get details for %s (%s)" % (devinfo, szHardwareID.value)) 227 # ignore errors and still include the port in the list, friendly name will be same as port name 228 yield string(port_name_buffer), 'n/a', szHardwareID_str 229 else: 230 yield string(port_name_buffer), string(szFriendlyName), szHardwareID_str 231 232 SetupDiDestroyDeviceInfoList(g_hdi) 233 234 # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 235 # test 236 if __name__ == '__main__': 237 import serial 238 239 for port, desc, hwid in sorted(comports()): 240 print "%s: %s [%s]" % (port, desc, hwid) 241