# Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Implements raw HID device communication on Windows.""" from __future__ import absolute_import import ctypes from ctypes import wintypes import platform from . import base, errors # Load relevant DLLs hid = ctypes.windll.Hid setupapi = ctypes.windll.SetupAPI kernel32 = ctypes.windll.Kernel32 # Various structs that are used in the Windows APIs we call class GUID(ctypes.Structure): _fields_ = [('Data1', ctypes.c_ulong), ('Data2', ctypes.c_ushort), ('Data3', ctypes.c_ushort), ('Data4', ctypes.c_ubyte * 8)] # On Windows, SetupAPI.h packs structures differently in 64bit and # 32bit mode. In 64bit mode, thestructures are packed on 8 byte # boundaries, while in 32bit mode, they are packed on 1 byte boundaries. # This is important to get right for some API calls that fill out these # structures. if platform.architecture()[0] == '64bit': SETUPAPI_PACK = 8 elif platform.architecture()[0] == '32bit': SETUPAPI_PACK = 1 else: raise errors.HidError('Unknown architecture: %s' % platform.architecture()[0]) class DeviceInterfaceData(ctypes.Structure): _fields_ = [('cbSize', wintypes.DWORD), ('InterfaceClassGuid', GUID), ('Flags', wintypes.DWORD), ('Reserved', ctypes.POINTER(ctypes.c_ulong))] _pack_ = SETUPAPI_PACK class DeviceInterfaceDetailData(ctypes.Structure): _fields_ = [('cbSize', wintypes.DWORD), ('DevicePath', ctypes.c_byte * 1)] _pack_ = SETUPAPI_PACK class HidAttributes(ctypes.Structure): _fields_ = [('Size', ctypes.c_ulong), ('VendorID', ctypes.c_ushort), ('ProductID', ctypes.c_ushort), ('VersionNumber', ctypes.c_ushort)] class HidCapabilities(ctypes.Structure): _fields_ = [('Usage', ctypes.c_ushort), ('UsagePage', ctypes.c_ushort), ('InputReportByteLength', ctypes.c_ushort), ('OutputReportByteLength', ctypes.c_ushort), ('FeatureReportByteLength', ctypes.c_ushort), ('Reserved', ctypes.c_ushort * 17), ('NotUsed', ctypes.c_ushort * 10)] # Various void* aliases for readability. HDEVINFO = ctypes.c_void_p HANDLE = ctypes.c_void_p PHIDP_PREPARSED_DATA = ctypes.c_void_p # pylint: disable=invalid-name # This is a HANDLE. INVALID_HANDLE_VALUE = 0xffffffff # Status codes NTSTATUS = ctypes.c_long HIDP_STATUS_SUCCESS = 0x00110000 FILE_SHARE_READ = 0x00000001 FILE_SHARE_WRITE = 0x00000002 OPEN_EXISTING = 0x03 ERROR_ACCESS_DENIED = 0x05 # CreateFile Flags GENERIC_WRITE = 0x40000000 GENERIC_READ = 0x80000000 # Function signatures hid.HidD_GetHidGuid.restype = None hid.HidD_GetHidGuid.argtypes = [ctypes.POINTER(GUID)] hid.HidD_GetAttributes.restype = wintypes.BOOLEAN hid.HidD_GetAttributes.argtypes = [HANDLE, ctypes.POINTER(HidAttributes)] hid.HidD_GetPreparsedData.restype = wintypes.BOOLEAN hid.HidD_GetPreparsedData.argtypes = [HANDLE, ctypes.POINTER(PHIDP_PREPARSED_DATA)] hid.HidD_FreePreparsedData.restype = wintypes.BOOLEAN hid.HidD_FreePreparsedData.argtypes = [PHIDP_PREPARSED_DATA] hid.HidD_GetProductString.restype = wintypes.BOOLEAN hid.HidD_GetProductString.argtypes = [HANDLE, ctypes.c_void_p, ctypes.c_ulong] hid.HidP_GetCaps.restype = NTSTATUS hid.HidP_GetCaps.argtypes = [PHIDP_PREPARSED_DATA, ctypes.POINTER(HidCapabilities)] setupapi.SetupDiGetClassDevsA.argtypes = [ctypes.POINTER(GUID), ctypes.c_char_p, wintypes.HWND, wintypes.DWORD] setupapi.SetupDiGetClassDevsA.restype = HDEVINFO setupapi.SetupDiEnumDeviceInterfaces.restype = wintypes.BOOL setupapi.SetupDiEnumDeviceInterfaces.argtypes = [ HDEVINFO, ctypes.c_void_p, ctypes.POINTER(GUID), wintypes.DWORD, ctypes.POINTER(DeviceInterfaceData)] setupapi.SetupDiGetDeviceInterfaceDetailA.restype = wintypes.BOOL setupapi.SetupDiGetDeviceInterfaceDetailA.argtypes = [ HDEVINFO, ctypes.POINTER(DeviceInterfaceData), ctypes.POINTER(DeviceInterfaceDetailData), wintypes.DWORD, ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p] kernel32.CreateFileA.restype = HANDLE kernel32.CreateFileA.argtypes = [ ctypes.c_char_p, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, HANDLE] kernel32.CloseHandle.restype = wintypes.BOOL kernel32.CloseHandle.argtypes = [HANDLE] kernel32.ReadFile.restype = wintypes.BOOL kernel32.ReadFile.argtypes = [ HANDLE, ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p] kernel32.WriteFile.restype = wintypes.BOOL kernel32.WriteFile.argtypes = [ HANDLE, ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p] def FillDeviceAttributes(device, descriptor): """Fill out the attributes of the device. Fills the devices HidAttributes and product string into the descriptor. Args: device: A handle to the open device descriptor: The DeviceDescriptor to populate with the attributes. Returns: None Raises: WindowsError when unable to obtain attributes or product string. """ attributes = HidAttributes() result = hid.HidD_GetAttributes(device, ctypes.byref(attributes)) if not result: raise ctypes.WinError() buf = ctypes.create_string_buffer(1024) result = hid.HidD_GetProductString(device, buf, 1024) if not result: raise ctypes.WinError() descriptor.vendor_id = attributes.VendorID descriptor.product_id = attributes.ProductID descriptor.product_string = ctypes.wstring_at(buf) def FillDeviceCapabilities(device, descriptor): """Fill out device capabilities. Fills the HidCapabilitites of the device into descriptor. Args: device: A handle to the open device descriptor: DeviceDescriptor to populate with the capabilities Returns: none Raises: WindowsError when unable to obtain capabilitites. """ preparsed_data = PHIDP_PREPARSED_DATA(0) ret = hid.HidD_GetPreparsedData(device, ctypes.byref(preparsed_data)) if not ret: raise ctypes.WinError() try: caps = HidCapabilities() ret = hid.HidP_GetCaps(preparsed_data, ctypes.byref(caps)) if ret != HIDP_STATUS_SUCCESS: raise ctypes.WinError() descriptor.usage = caps.Usage descriptor.usage_page = caps.UsagePage descriptor.internal_max_in_report_len = caps.InputReportByteLength descriptor.internal_max_out_report_len = caps.OutputReportByteLength finally: hid.HidD_FreePreparsedData(preparsed_data) # The python os.open() implementation uses the windows libc # open() function, which writes CreateFile but does so in a way # that doesn't let us open the device with the right set of permissions. # Therefore, we have to directly use the Windows API calls. # We could use PyWin32, which provides simple wrappers. However, to avoid # requiring a PyWin32 dependency for clients, we simply also implement it # using ctypes. def OpenDevice(path, enum=False): """Open the device and return a handle to it.""" desired_access = GENERIC_WRITE | GENERIC_READ share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE if enum: desired_access = 0 h = kernel32.CreateFileA(path, desired_access, share_mode, None, OPEN_EXISTING, 0, None) if h == INVALID_HANDLE_VALUE: raise ctypes.WinError() return h class WindowsHidDevice(base.HidDevice): """Implementation of raw HID interface on Windows.""" @staticmethod def Enumerate(): """See base class.""" hid_guid = GUID() hid.HidD_GetHidGuid(ctypes.byref(hid_guid)) devices = setupapi.SetupDiGetClassDevsA( ctypes.byref(hid_guid), None, None, 0x12) index = 0 interface_info = DeviceInterfaceData() interface_info.cbSize = ctypes.sizeof(DeviceInterfaceData) # pylint: disable=invalid-name out = [] while True: result = setupapi.SetupDiEnumDeviceInterfaces( devices, 0, ctypes.byref(hid_guid), index, ctypes.byref(interface_info)) index += 1 if not result: break detail_len = wintypes.DWORD() result = setupapi.SetupDiGetDeviceInterfaceDetailA( devices, ctypes.byref(interface_info), None, 0, ctypes.byref(detail_len), None) detail_len = detail_len.value if detail_len == 0: # skip this device, some kind of error continue buf = ctypes.create_string_buffer(detail_len) interface_detail = DeviceInterfaceDetailData.from_buffer(buf) interface_detail.cbSize = ctypes.sizeof(DeviceInterfaceDetailData) result = setupapi.SetupDiGetDeviceInterfaceDetailA( devices, ctypes.byref(interface_info), ctypes.byref(interface_detail), detail_len, None, None) if not result: raise ctypes.WinError() descriptor = base.DeviceDescriptor() # This is a bit of a hack to work around a limitation of ctypes and # "header" structures that are common in windows. DevicePath is a # ctypes array of length 1, but it is backed with a buffer that is much # longer and contains a null terminated string. So, we read the null # terminated string off DevicePath here. Per the comment above, the # alignment of this struct varies depending on architecture, but # in all cases the path string starts 1 DWORD into the structure. # # The path length is: # length of detail buffer - header length (1 DWORD) path_len = detail_len - ctypes.sizeof(wintypes.DWORD) descriptor.path = ctypes.string_at( ctypes.addressof(interface_detail.DevicePath), path_len) device = None try: device = OpenDevice(descriptor.path, True) except WindowsError as e: # pylint: disable=undefined-variable if e.winerror == ERROR_ACCESS_DENIED: # Access Denied, e.g. a keyboard continue else: raise e try: FillDeviceAttributes(device, descriptor) FillDeviceCapabilities(device, descriptor) out.append(descriptor.ToPublicDict()) except Exception: continue # Try with next device finally: kernel32.CloseHandle(device) return out def __init__(self, path): """See base class.""" base.HidDevice.__init__(self, path) self.dev = OpenDevice(path) self.desc = base.DeviceDescriptor() FillDeviceCapabilities(self.dev, self.desc) def GetInReportDataLength(self): """See base class.""" return self.desc.internal_max_in_report_len - 1 def GetOutReportDataLength(self): """See base class.""" return self.desc.internal_max_out_report_len - 1 def Write(self, packet): """See base class.""" if len(packet) != self.GetOutReportDataLength(): raise errors.HidError('Packet length must match report data length.') out = bytes(bytearray([0] + packet)) # Prepend the zero-byte (report ID) num_written = wintypes.DWORD() ret = ( kernel32.WriteFile( self.dev, out, len(out), ctypes.byref(num_written), None)) if num_written.value != len(out): raise errors.HidError( 'Failed to write complete packet. ' + 'Expected %d, but got %d' % (len(out), num_written.value)) if not ret: raise ctypes.WinError() def Read(self): """See base class.""" buf = ctypes.create_string_buffer(self.desc.internal_max_in_report_len) num_read = wintypes.DWORD() ret = kernel32.ReadFile( self.dev, buf, len(buf), ctypes.byref(num_read), None) if num_read.value != self.desc.internal_max_in_report_len: raise errors.HidError('Failed to read full length report from device.') if not ret: raise ctypes.WinError() # Convert the string buffer to a list of numbers. Throw away the first # byte, which is the report id (which we don't care about). return b''.join(buf)[1:]