1212
1313import struct
1414
15+ import usb
1516from micropython import const
1617
1718try :
3435
3536_REQ_GET_DESCRIPTOR = const (6 )
3637
38+ _RECIP_INTERFACE = const (1 )
39+
3740# No const because these are public
3841DESC_DEVICE = 0x01
3942DESC_CONFIGURATION = 0x02
4043DESC_STRING = 0x03
4144DESC_INTERFACE = 0x04
4245DESC_ENDPOINT = 0x05
46+ DESC_HID = 0x21
47+ DESC_REPORT = 0x22
4348
4449INTERFACE_HID = 0x03
4550SUBCLASS_BOOT = 0x01
51+ SUBCLASS_RESERVED = 0x00
4652PROTOCOL_MOUSE = 0x02
4753PROTOCOL_KEYBOARD = 0x01
4854
55+ # --- HID Report Descriptor Item Tags (The "Command") ---
56+ HID_TAG_USAGE_PAGE = 0x05 # Defines the category (e.g., Generic Desktop, Game Controls)
57+ HID_TAG_USAGE = 0x09 # Defines the specific item (e.g., Mouse, Joystick)
58+
59+ # --- Usage Page IDs (Values for 0x05) ---
60+ USAGE_PAGE_GENERIC_DESKTOP = 0x01
61+
62+ # --- Usage IDs (Values for 0x09, inside Generic Desktop) ---
63+ USAGE_MOUSE = 0x02
64+ USAGE_JOYSTICK = 0x04
65+ USAGE_GAMEPAD = 0x05
66+ USAGE_KEYBOARD = 0x06
67+
4968
5069def get_descriptor (device , desc_type , index , buf , language_id = 0 ):
5170 """Fetch the descriptor from the device into buf."""
@@ -83,32 +102,121 @@ def get_configuration_descriptor(device, index):
83102 return full_buf
84103
85104
86- def _find_boot_endpoint (device , protocol_type : Literal [PROTOCOL_MOUSE , PROTOCOL_KEYBOARD ]):
105+ def get_report_descriptor (device , interface_num , length ):
106+ """
107+ Fetches the HID Report Descriptor.
108+ This tells us what the device actually IS (Mouse vs Joystick).
109+ """
110+ if length < 1 :
111+ return None
112+
113+ buf = bytearray (length )
114+ try :
115+ # 0x81 = Dir: IN | Type: Standard | Recipient: Interface
116+ # wValue = 0x2200 (Report Descriptor)
117+ device .ctrl_transfer (
118+ _RECIP_INTERFACE | _REQ_TYPE_STANDARD | _DIR_IN ,
119+ _REQ_GET_DESCRIPTOR ,
120+ DESC_REPORT << 8 ,
121+ interface_num ,
122+ buf ,
123+ )
124+ return buf
125+ except usb .core .USBError as e :
126+ print (f"Failed to read Report Descriptor: { e } " )
127+ return None
128+
129+
130+ def _is_confirmed_mouse (report_desc ):
131+ """
132+ Scans the raw descriptor bytes for:
133+ Usage Page (Generic Desktop) = 0x05, 0x01
134+ Usage (Mouse) = 0x09, 0x02
135+ """
136+ if not report_desc :
137+ return False
138+
139+ # Simple byte scan check
140+ # We look for Usage Page Generic Desktop (0x05 0x01)
141+ has_generic_desktop = False
142+ for i in range (len (report_desc ) - 1 ):
143+ if (
144+ report_desc [i ] == HID_TAG_USAGE_PAGE
145+ and report_desc [i + 1 ] == USAGE_PAGE_GENERIC_DESKTOP
146+ ):
147+ has_generic_desktop = True
148+
149+ # We look for Usage Mouse (0x09 0x02)
150+ has_mouse_usage = False
151+ for i in range (len (report_desc ) - 1 ):
152+ if report_desc [i ] == HID_TAG_USAGE and report_desc [i + 1 ] == USAGE_MOUSE :
153+ has_mouse_usage = True
154+
155+ return has_generic_desktop and has_mouse_usage
156+
157+
158+ def _find_endpoint (device , protocol_type : Literal [PROTOCOL_MOUSE , PROTOCOL_KEYBOARD ], subclass ):
87159 config_descriptor = get_configuration_descriptor (device , 0 )
88160 i = 0
89161 mouse_interface_index = None
90162 found_mouse = False
163+ candidate_found = False
164+ hid_desc_len = 0
91165 while i < len (config_descriptor ):
92166 descriptor_len = config_descriptor [i ]
93167 descriptor_type = config_descriptor [i + 1 ]
168+
169+ # Found Interface
94170 if descriptor_type == DESC_INTERFACE :
95171 interface_number = config_descriptor [i + 2 ]
96172 interface_class = config_descriptor [i + 5 ]
97173 interface_subclass = config_descriptor [i + 6 ]
98174 interface_protocol = config_descriptor [i + 7 ]
175+
176+ # Reset checks
177+ candidate_found = False
178+ hid_desc_len = 0
179+
180+ # Found mouse or keyboard interface depending on what was requested
99181 if (
100182 interface_class == INTERFACE_HID
101- and interface_subclass == SUBCLASS_BOOT
102183 and interface_protocol == protocol_type
184+ and interface_subclass == SUBCLASS_BOOT
185+ and subclass == SUBCLASS_BOOT
103186 ):
104187 found_mouse = True
105188 mouse_interface_index = interface_number
106189
190+ # May be trackpad interface if it's not a keyboard and looking for mouse
191+ elif (
192+ interface_class == INTERFACE_HID
193+ and interface_protocol != PROTOCOL_KEYBOARD
194+ and protocol_type == PROTOCOL_MOUSE
195+ and subclass == SUBCLASS_RESERVED
196+ ):
197+ candidate_found = True
198+ mouse_interface_index = interface_number
199+
200+ # Found HID Descriptor (Contains Report Length)
201+ elif descriptor_type == DESC_HID and candidate_found :
202+ # The HID descriptor stores the Report Descriptor length at offset 7
203+ # Bytes: [Length, Type, BCD, BCD, Country, Count, ReportType, ReportLenL, ReportLenH]
204+ if descriptor_len >= 9 :
205+ hid_desc_len = config_descriptor [i + 7 ] + (config_descriptor [i + 8 ] << 8 )
206+
107207 elif descriptor_type == DESC_ENDPOINT :
108208 endpoint_address = config_descriptor [i + 2 ]
109209 if endpoint_address & _DIR_IN :
110210 if found_mouse :
111211 return mouse_interface_index , endpoint_address
212+
213+ elif candidate_found :
214+ rep_desc = get_report_descriptor (device , mouse_interface_index , hid_desc_len )
215+ if _is_confirmed_mouse (rep_desc ):
216+ return mouse_interface_index , endpoint_address
217+
218+ candidate_found = False # Stop looking at this interface
219+
112220 i += descriptor_len
113221 return None , None
114222
@@ -120,7 +228,17 @@ def find_boot_mouse_endpoint(device):
120228 :param device: The device to search within
121229 :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise
122230 """
123- return _find_boot_endpoint (device , PROTOCOL_MOUSE )
231+ return _find_endpoint (device , PROTOCOL_MOUSE , SUBCLASS_BOOT )
232+
233+
234+ def find_report_mouse_endpoint (device ):
235+ """
236+ Try to find a report mouse endpoint in the device and return its
237+ interface index, and endpoint address.
238+ :param device: The device to search within
239+ :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise
240+ """
241+ return _find_endpoint (device , PROTOCOL_MOUSE , SUBCLASS_RESERVED )
124242
125243
126244def find_boot_keyboard_endpoint (device ):
@@ -130,4 +248,4 @@ def find_boot_keyboard_endpoint(device):
130248 :param device: The device to search within
131249 :return: keyboard_interface_index, keyboard_endpoint_address if found, or None, None otherwise
132250 """
133- return _find_boot_endpoint (device , PROTOCOL_KEYBOARD )
251+ return _find_endpoint (device , PROTOCOL_KEYBOARD , SUBCLASS_BOOT )
0 commit comments