Home | History | Annotate | Download | only in tools
      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