Skip to content

Commit bbba127

Browse files
committed
Add support for composite HID devices
1 parent 5b7eedc commit bbba127

File tree

1 file changed

+137
-6
lines changed

1 file changed

+137
-6
lines changed

adafruit_usb_host_descriptors.py

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,36 @@
3434

3535
_REQ_GET_DESCRIPTOR = const(6)
3636

37+
_RECIP_INTERFACE = const(1)
38+
3739
# No const because these are public
3840
DESC_DEVICE = 0x01
3941
DESC_CONFIGURATION = 0x02
4042
DESC_STRING = 0x03
4143
DESC_INTERFACE = 0x04
4244
DESC_ENDPOINT = 0x05
45+
DESC_HID = 0x21
46+
DESC_REPORT = 0X22
4347

4448
INTERFACE_HID = 0x03
4549
SUBCLASS_BOOT = 0x01
50+
SUBCLASS_REPORT = None
4651
PROTOCOL_MOUSE = 0x02
4752
PROTOCOL_KEYBOARD = 0x01
4853

54+
# --- HID Report Descriptor Item Tags (The "Command") ---
55+
HID_TAG_USAGE_PAGE = 0x05 # Defines the category (e.g., Generic Desktop, Game Controls)
56+
HID_TAG_USAGE = 0x09 # Defines the specific item (e.g., Mouse, Joystick)
57+
58+
# --- Usage Page IDs (Values for 0x05) ---
59+
USAGE_PAGE_GENERIC_DESKTOP = 0x01
60+
61+
# --- Usage IDs (Values for 0x09, inside Generic Desktop) ---
62+
USAGE_MOUSE = 0x02
63+
USAGE_JOYSTICK = 0x04
64+
USAGE_GAMEPAD = 0x05
65+
USAGE_KEYBOARD = 0x06
66+
4967

5068
def get_descriptor(device, desc_type, index, buf, language_id=0):
5169
"""Fetch the descriptor from the device into buf."""
@@ -82,35 +100,139 @@ def get_configuration_descriptor(device, index):
82100
get_descriptor(device, DESC_CONFIGURATION, index, full_buf)
83101
return full_buf
84102

103+
def get_report_descriptor(device, interface_num, length):
104+
"""
105+
Fetches the HID Report Descriptor.
106+
This tells us what the device actually IS (Mouse vs Joystick).
107+
"""
108+
buf = bytearray(length)
109+
try:
110+
# 0x81 = Dir: IN | Type: Standard | Recipient: Interface
111+
# wValue = 0x2200 (Report Descriptor)
112+
device.ctrl_transfer(
113+
_RECIP_INTERFACE | _REQ_TYPE_STANDARD | _DIR_IN,
114+
_REQ_GET_DESCRIPTOR,
115+
DESC_REPORT << 8,
116+
interface_num,
117+
buf
118+
)
119+
return buf
120+
except Exception as e:
121+
print(f"Failed to read Report Descriptor: {e}")
122+
return None
123+
124+
def _is_confirmed_mouse(report_desc):
125+
"""
126+
Scans the raw descriptor bytes for:
127+
Usage Page (Generic Desktop) = 0x05, 0x01
128+
Usage (Mouse) = 0x09, 0x02
129+
"""
130+
if not report_desc:
131+
return False
132+
133+
# Simple byte scan check
134+
# We look for Usage Page Generic Desktop (0x05 0x01)
135+
has_generic_desktop = False
136+
for i in range(len(report_desc) - 1):
137+
if report_desc[i] == HID_TAG_USAGE_PAGE and report_desc[i+1] == USAGE_PAGE_GENERIC_DESKTOP:
138+
has_generic_desktop = True
139+
140+
# We look for Usage Mouse (0x09 0x02)
141+
has_mouse_usage = False
142+
for i in range(len(report_desc) - 1):
143+
if report_desc[i] == HID_TAG_USAGE and report_desc[i+1] == USAGE_MOUSE:
144+
has_mouse_usage = True
145+
146+
return has_generic_desktop and has_mouse_usage
147+
148+
149+
def _find_endpoint(device, count, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD], subclass):
150+
# pass a count of <= 0 to return all HID interfaces/endpoints of selected protocol_type on the device
85151

86-
def _find_boot_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD]):
87152
config_descriptor = get_configuration_descriptor(device, 0)
88153
i = 0
89154
mouse_interface_index = None
90155
found_mouse = False
156+
candidate_found = False
157+
hid_desc_len = 0
158+
endpoints = []
91159
while i < len(config_descriptor):
92160
descriptor_len = config_descriptor[i]
93161
descriptor_type = config_descriptor[i + 1]
162+
163+
# Found Interface
94164
if descriptor_type == DESC_INTERFACE:
95165
interface_number = config_descriptor[i + 2]
96166
interface_class = config_descriptor[i + 5]
97167
interface_subclass = config_descriptor[i + 6]
98168
interface_protocol = config_descriptor[i + 7]
169+
170+
# Reset checks
171+
candidate_found = False
172+
hid_desc_len = 0
173+
174+
# Found mouse or keyboard interface depending on what was requested
99175
if (
100176
interface_class == INTERFACE_HID
101-
and interface_subclass == SUBCLASS_BOOT
102177
and interface_protocol == protocol_type
178+
and interface_subclass == SUBCLASS_BOOT
179+
and subclass == SUBCLASS_BOOT
103180
):
104181
found_mouse = True
105182
mouse_interface_index = interface_number
183+
184+
# May be trackpad interface if it's not a keyboard and looking for mouse
185+
elif (
186+
interface_class == INTERFACE_HID
187+
and interface_protocol != PROTOCOL_KEYBOARD
188+
and protocol_type == PROTOCOL_MOUSE
189+
and subclass != SUBCLASS_BOOT
190+
):
191+
candidate_found = True
192+
193+
# Found HID Descriptor (Contains Report Length)
194+
elif descriptor_type == DESC_HID and candidate_found:
195+
# The HID descriptor stores the Report Descriptor length at offset 7
196+
# Bytes: [Length, Type, BCD, BCD, Country, Count, ReportType, ReportLenL, ReportLenH]
197+
if descriptor_len >= 9:
198+
hid_desc_len = config_descriptor[i+7] + (config_descriptor[i+8] << 8)
106199

107200
elif descriptor_type == DESC_ENDPOINT:
108201
endpoint_address = config_descriptor[i + 2]
109202
if endpoint_address & _DIR_IN:
110203
if found_mouse:
111-
return mouse_interface_index, endpoint_address
204+
endpoints.append((mouse_interface_index, endpoint_address))
205+
if len(endpoints) == count:
206+
return endpoints
207+
208+
elif candidate_found:
209+
print(f"Checking Interface {interface_number}...")
210+
211+
# If it's Protocol 2, it's definitely a mouse (Standard).
212+
# If it's Protocol 0, we must check the descriptor.
213+
is_mouse = False
214+
215+
if hid_desc_len > 0:
216+
rep_desc = get_report_descriptor(device, interface_number, hid_desc_len)
217+
if _is_confirmed_mouse(rep_desc):
218+
is_mouse = True
219+
print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)")
220+
else:
221+
print(f" -> REJECTED: Generic HID, but not a mouse (Joystick/Ups?)")
222+
else:
223+
# Fallback if we missed the HID descriptor, assume no if Candidate
224+
print(" -> Warning: Could not verify Usage, assuming no.")
225+
is_mouse = False
226+
227+
if is_mouse:
228+
endpoints.append((interface_number, endpoint_address))
229+
if len(endpoints) == count:
230+
return endpoints
231+
else:
232+
candidate_found = False # Stop looking at this interface
233+
112234
i += descriptor_len
113-
return None, None
235+
return [(None, None)]
114236

115237

116238
def find_boot_mouse_endpoint(device):
@@ -120,8 +242,16 @@ def find_boot_mouse_endpoint(device):
120242
:param device: The device to search within
121243
:return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise
122244
"""
123-
return _find_boot_endpoint(device, PROTOCOL_MOUSE)
245+
return _find_endpoint(device, 1, PROTOCOL_MOUSE, SUBCLASS_BOOT)[0]
124246

247+
def find_report_mouse_endpoint(device):
248+
"""
249+
Try to find a report mouse endpoint in the device and return its
250+
interface index, and endpoint address.
251+
:param device: The device to search within
252+
:return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise
253+
"""
254+
return _find_endpoint(device, 1, PROTOCOL_MOUSE, SUBCLASS_REPORT)[0]
125255

126256
def find_boot_keyboard_endpoint(device):
127257
"""
@@ -130,4 +260,5 @@ def find_boot_keyboard_endpoint(device):
130260
:param device: The device to search within
131261
:return: keyboard_interface_index, keyboard_endpoint_address if found, or None, None otherwise
132262
"""
133-
return _find_boot_endpoint(device, PROTOCOL_KEYBOARD)
263+
return _find_endpoint(device, 1, PROTOCOL_KEYBOARD, SUBCLASS_BOOT)[0]
264+

0 commit comments

Comments
 (0)