Skip to content

Commit f2ae73a

Browse files
committed
HID Get Report
1 parent cc1ae68 commit f2ae73a

File tree

1 file changed

+167
-47
lines changed

1 file changed

+167
-47
lines changed

pyocd/probe/pydapaccess/interface/hidapi_backend.py

Lines changed: 167 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@
2020
import platform
2121
import threading
2222
import queue
23-
from typing import Optional
23+
import struct
24+
import array
25+
from typing import Optional, Tuple
2426

2527
from .interface import Interface
2628
from .common import (
27-
USB_CLASS_HID,
2829
filter_device_by_usage_page,
2930
generate_device_unique_id,
3031
is_known_cmsis_dap_vid_pid,
3132
)
3233
from ..dap_access_api import DAPAccessIntf
33-
from .pyusb_backend import MatchCmsisDapv1Interface
3434
from ....utility.compatibility import to_str_safe
3535
from ....utility.timeout import Timeout
3636

@@ -40,11 +40,6 @@
4040

4141
try:
4242
import hid
43-
import usb.util
44-
try:
45-
from libusb_package import find as usb_find
46-
except ImportError:
47-
from usb.core import find as usb_find
4843
except ImportError:
4944
IS_AVAILABLE = False
5045
else:
@@ -54,6 +49,97 @@
5449
_IS_DARWIN = (platform.system() == 'Darwin')
5550
_IS_WINDOWS = (platform.system() == 'Windows')
5651

52+
if _IS_WINDOWS:
53+
import ctypes
54+
from ctypes import wintypes
55+
56+
# Define necessary Windows structures and functions for HID
57+
class HIDP_CAPS(ctypes.Structure):
58+
_fields_ = [
59+
("Usage", wintypes.USHORT),
60+
("UsagePage", wintypes.USHORT),
61+
("InputReportByteLength", wintypes.USHORT),
62+
("OutputReportByteLength", wintypes.USHORT),
63+
("FeatureReportByteLength", wintypes.USHORT),
64+
("Reserved", wintypes.USHORT * 17),
65+
("NumberLinkCollectionNodes", wintypes.USHORT),
66+
("NumberInputButtonCaps", wintypes.USHORT),
67+
("NumberInputValueCaps", wintypes.USHORT),
68+
("NumberInputDataIndices", wintypes.USHORT),
69+
("NumberOutputButtonCaps", wintypes.USHORT),
70+
("NumberOutputValueCaps", wintypes.USHORT),
71+
("NumberOutputDataIndices", wintypes.USHORT),
72+
("NumberFeatureButtonCaps", wintypes.USHORT),
73+
("NumberFeatureValueCaps", wintypes.USHORT),
74+
("NumberFeatureDataIndices", wintypes.USHORT),
75+
]
76+
77+
hid_dll = ctypes.windll.hid
78+
kernel32_dll = ctypes.windll.kernel32
79+
80+
# HidD_GetPreparsedData
81+
hid_dll.HidD_GetPreparsedData.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.HANDLE)]
82+
hid_dll.HidD_GetPreparsedData.restype = wintypes.BOOLEAN
83+
84+
# HidP_GetCaps
85+
hid_dll.HidP_GetCaps.argtypes = [wintypes.HANDLE, ctypes.POINTER(HIDP_CAPS)]
86+
hid_dll.HidP_GetCaps.restype = ctypes.c_long # NTSTATUS
87+
88+
# HidD_FreePreparsedData
89+
hid_dll.HidD_FreePreparsedData.argtypes = [wintypes.HANDLE]
90+
hid_dll.HidD_FreePreparsedData.restype = wintypes.BOOLEAN
91+
92+
# CreateFileW
93+
kernel32_dll.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
94+
kernel32_dll.CreateFileW.restype = wintypes.HANDLE
95+
96+
# CloseHandle
97+
kernel32_dll.CloseHandle.argtypes = [wintypes.HANDLE]
98+
kernel32_dll.CloseHandle.restype = wintypes.BOOL
99+
100+
INVALID_HANDLE_VALUE = -1
101+
GENERIC_READ = 0x80000000
102+
GENERIC_WRITE = 0x40000000
103+
FILE_SHARE_READ = 1
104+
FILE_SHARE_WRITE = 2
105+
OPEN_EXISTING = 3
106+
107+
elif _IS_DARWIN:
108+
import ctypes
109+
import ctypes.util
110+
111+
iokit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('IOKit'))
112+
113+
kCFStringEncodingUTF8 = 0x08000100
114+
115+
# Define CFString functions
116+
CFStringCreateWithCString = iokit.CFStringCreateWithCString
117+
CFStringCreateWithCString.restype = ctypes.c_void_p
118+
CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32]
119+
120+
CFRelease = iokit.CFRelease
121+
CFRelease.restype = None
122+
CFRelease.argtypes = [ctypes.c_void_p]
123+
124+
# Define IOHIDDevice functions
125+
IOHIDDeviceGetProperty = iokit.IOHIDDeviceGetProperty
126+
IOHIDDeviceGetProperty.restype = ctypes.c_void_p
127+
IOHIDDeviceGetProperty.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
128+
129+
# Define CFNumber functions
130+
CFNumberGetValue = iokit.CFNumberGetValue
131+
CFNumberGetValue.restype = ctypes.c_bool
132+
CFNumberGetValue.argtypes = [ctypes.c_void_p, ctypes.c_long, ctypes.c_void_p]
133+
kCFNumberSInt32Type = 3
134+
135+
# Keys for IOHIDDeviceGetProperty
136+
def _create_cfstring(s):
137+
return CFStringCreateWithCString(None, s.encode('utf-8'), kCFStringEncodingUTF8)
138+
139+
kIOHIDMaxInputReportSizeKey = _create_cfstring("MaxInputReportSize")
140+
kIOHIDMaxOutputReportSizeKey = _create_cfstring("MaxOutputReportSize")
141+
142+
57143
class HidApiUSB(Interface):
58144
"""@brief CMSIS-DAP USB interface class using hidapi backend."""
59145

@@ -91,48 +177,14 @@ def set_packet_count(self, count):
91177
self.packet_count = count
92178

93179
def open(self):
180+
probe_id = self.serial_number or f"VID={self.vid:#06x}:PID={self.pid:#06x}"
94181

95-
# Use pyUSB to get HID Interrupt EP wMaxPacketSize, since hidapi is not reliable
96-
97-
# Get device handle.
98-
# If multiple identical (same PID & VID) probes without serial number are connected,
99-
# assume they share the same wMaxPacketSize.
100-
101-
usb_serial = self.device_info['serial_number']
102-
103-
kwargs = {'idVendor': self.vid, 'idProduct': self.pid}
104-
if usb_serial: # only pass a real USB serial
105-
kwargs['serial_number'] = usb_serial
106-
107-
probe_id = usb_serial or f"VID={self.vid:#06x}:PID={self.pid:#06x}"
108-
109-
dev = usb_find(**kwargs)
110-
if dev is None:
111-
raise DAPAccessIntf.DeviceError(f"Probe {probe_id} not found")
112-
113-
# Get active config
114-
config = dev.get_active_configuration()
115-
116-
# Get count of HID interfaces and create the matcher object
117-
hid_interface_count = len(list(usb.util.find_descriptor(config, find_all=True, bInterfaceClass=USB_CLASS_HID)))
118-
matcher = MatchCmsisDapv1Interface(hid_interface_count)
119-
120-
# Get CMSIS-DAPv1 interface
121-
interface = usb.util.find_descriptor(config, custom_match=matcher)
122-
if interface is None:
123-
raise DAPAccessIntf.DeviceError(f"Probe {probe_id} has no CMSIS-DAPv1 interface")
124-
125-
# Set report sizes, assuming HID report size matches endpoint wMaxPacketSize.
126-
for endpoint in interface:
127-
if usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_INTR:
128-
if endpoint.bEndpointAddress & usb.util.ENDPOINT_IN:
129-
self.report_in_size = endpoint.wMaxPacketSize
130-
else:
131-
self.report_out_size = endpoint.wMaxPacketSize
182+
self.report_in_size, self.report_out_size = self._get_hid_report_sizes()
132183

133184
if self.report_in_size is None:
134-
raise DAPAccessIntf.DeviceError(
135-
f"Could not determine packet sizes for probe {probe_id}")
185+
# Fallback: set report size to 64 bytes
186+
LOG.warning("Could not determine IN report size for probe %s, defaulting to 64 bytes", probe_id)
187+
self.report_in_size = 64
136188

137189
if self.report_out_size is None:
138190
# No interrupt OUT endpoint. Out reports will be sent via control transfer.
@@ -156,6 +208,74 @@ def open(self):
156208

157209
self.closed = False
158210

211+
def _get_hid_report_sizes(self) -> Tuple[Optional[int], Optional[int]]:
212+
"""Get actual HID report sizes using platform-specific APIs."""
213+
if _IS_WINDOWS:
214+
return self._get_hid_report_sizes_windows()
215+
elif _IS_DARWIN:
216+
return self._get_hid_report_sizes_macos()
217+
else:
218+
# Linux and other OSes are not supported for this mechanism.
219+
LOG.debug("Unsupported OS for getting HID report sizes directly.")
220+
return None, None
221+
222+
def _get_hid_report_sizes_windows(self) -> Tuple[Optional[int], Optional[int]]:
223+
try:
224+
path = to_str_safe(self.device_info['path'])
225+
handle = kernel32_dll.CreateFileW(path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None)
226+
except Exception as exc:
227+
LOG.error("Exception opening device handle for HID report size query: %s", exc)
228+
return None, None
229+
230+
if handle == INVALID_HANDLE_VALUE:
231+
LOG.debug("Failed to open device handle for HID report size query.")
232+
return None, None
233+
234+
try:
235+
preparsed_data = wintypes.HANDLE()
236+
if not hid_dll.HidD_GetPreparsedData(handle, ctypes.byref(preparsed_data)):
237+
LOG.debug("HidD_GetPreparsedData failed.")
238+
return None, None
239+
240+
try:
241+
caps = HIDP_CAPS()
242+
HIDP_STATUS_SUCCESS = 0x00110000
243+
if hid_dll.HidP_GetCaps(preparsed_data, ctypes.byref(caps)) == HIDP_STATUS_SUCCESS:
244+
# The lengths include the report ID byte, which we don't use in the packet size.
245+
# However, the packet size should match the report size.
246+
return caps.InputReportByteLength, caps.OutputReportByteLength
247+
else:
248+
LOG.debug("HidP_GetCaps failed.")
249+
return None, None
250+
finally:
251+
hid_dll.HidD_FreePreparsedData(preparsed_data)
252+
finally:
253+
kernel32_dll.CloseHandle(handle)
254+
return None, None
255+
256+
def _get_hid_report_sizes_macos(self) -> Tuple[Optional[int], Optional[int]]:
257+
# The hid.device object from hidapi is what we need.
258+
# The underlying object is an IOHIDDeviceRef, which is a void*
259+
hid_device_ref = ctypes.c_void_p(self.device.dev)
260+
input_size = None
261+
output_size = None
262+
263+
# Get input report size
264+
value_ref = IOHIDDeviceGetProperty(hid_device_ref, kIOHIDMaxInputReportSizeKey)
265+
if value_ref:
266+
val = ctypes.c_int32(0)
267+
if CFNumberGetValue(value_ref, kCFNumberSInt32Type, ctypes.byref(val)):
268+
input_size = val.value
269+
270+
# Get output report size
271+
value_ref = IOHIDDeviceGetProperty(hid_device_ref, kIOHIDMaxOutputReportSizeKey)
272+
if value_ref:
273+
val = ctypes.c_int32(0)
274+
if CFNumberGetValue(value_ref, kCFNumberSInt32Type, ctypes.byref(val)):
275+
output_size = val.value
276+
277+
return input_size, output_size
278+
159279
def rx_task(self):
160280
try:
161281
while not self.closed_event.is_set():

0 commit comments

Comments
 (0)