From bbba127ee62add555be9a5292f11c0e97578bc66 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Sun, 23 Nov 2025 20:47:40 -0500 Subject: [PATCH 1/8] Add support for composite HID devices --- adafruit_usb_host_descriptors.py | 143 +++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 6 deletions(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index c02c1ba..358a502 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -34,18 +34,36 @@ _REQ_GET_DESCRIPTOR = const(6) +_RECIP_INTERFACE = const(1) + # No const because these are public DESC_DEVICE = 0x01 DESC_CONFIGURATION = 0x02 DESC_STRING = 0x03 DESC_INTERFACE = 0x04 DESC_ENDPOINT = 0x05 +DESC_HID = 0x21 +DESC_REPORT = 0X22 INTERFACE_HID = 0x03 SUBCLASS_BOOT = 0x01 +SUBCLASS_REPORT = None PROTOCOL_MOUSE = 0x02 PROTOCOL_KEYBOARD = 0x01 +# --- HID Report Descriptor Item Tags (The "Command") --- +HID_TAG_USAGE_PAGE = 0x05 # Defines the category (e.g., Generic Desktop, Game Controls) +HID_TAG_USAGE = 0x09 # Defines the specific item (e.g., Mouse, Joystick) + +# --- Usage Page IDs (Values for 0x05) --- +USAGE_PAGE_GENERIC_DESKTOP = 0x01 + +# --- Usage IDs (Values for 0x09, inside Generic Desktop) --- +USAGE_MOUSE = 0x02 +USAGE_JOYSTICK = 0x04 +USAGE_GAMEPAD = 0x05 +USAGE_KEYBOARD = 0x06 + def get_descriptor(device, desc_type, index, buf, language_id=0): """Fetch the descriptor from the device into buf.""" @@ -82,35 +100,139 @@ def get_configuration_descriptor(device, index): get_descriptor(device, DESC_CONFIGURATION, index, full_buf) return full_buf +def get_report_descriptor(device, interface_num, length): + """ + Fetches the HID Report Descriptor. + This tells us what the device actually IS (Mouse vs Joystick). + """ + buf = bytearray(length) + try: + # 0x81 = Dir: IN | Type: Standard | Recipient: Interface + # wValue = 0x2200 (Report Descriptor) + device.ctrl_transfer( + _RECIP_INTERFACE | _REQ_TYPE_STANDARD | _DIR_IN, + _REQ_GET_DESCRIPTOR, + DESC_REPORT << 8, + interface_num, + buf + ) + return buf + except Exception as e: + print(f"Failed to read Report Descriptor: {e}") + return None + +def _is_confirmed_mouse(report_desc): + """ + Scans the raw descriptor bytes for: + Usage Page (Generic Desktop) = 0x05, 0x01 + Usage (Mouse) = 0x09, 0x02 + """ + if not report_desc: + return False + + # Simple byte scan check + # We look for Usage Page Generic Desktop (0x05 0x01) + has_generic_desktop = False + for i in range(len(report_desc) - 1): + if report_desc[i] == HID_TAG_USAGE_PAGE and report_desc[i+1] == USAGE_PAGE_GENERIC_DESKTOP: + has_generic_desktop = True + + # We look for Usage Mouse (0x09 0x02) + has_mouse_usage = False + for i in range(len(report_desc) - 1): + if report_desc[i] == HID_TAG_USAGE and report_desc[i+1] == USAGE_MOUSE: + has_mouse_usage = True + + return has_generic_desktop and has_mouse_usage + + +def _find_endpoint(device, count, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD], subclass): + # pass a count of <= 0 to return all HID interfaces/endpoints of selected protocol_type on the device -def _find_boot_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD]): config_descriptor = get_configuration_descriptor(device, 0) i = 0 mouse_interface_index = None found_mouse = False + candidate_found = False + hid_desc_len = 0 + endpoints = [] while i < len(config_descriptor): descriptor_len = config_descriptor[i] descriptor_type = config_descriptor[i + 1] + + # Found Interface if descriptor_type == DESC_INTERFACE: interface_number = config_descriptor[i + 2] interface_class = config_descriptor[i + 5] interface_subclass = config_descriptor[i + 6] interface_protocol = config_descriptor[i + 7] + + # Reset checks + candidate_found = False + hid_desc_len = 0 + + # Found mouse or keyboard interface depending on what was requested if ( interface_class == INTERFACE_HID - and interface_subclass == SUBCLASS_BOOT and interface_protocol == protocol_type + and interface_subclass == SUBCLASS_BOOT + and subclass == SUBCLASS_BOOT ): found_mouse = True mouse_interface_index = interface_number + + # May be trackpad interface if it's not a keyboard and looking for mouse + elif ( + interface_class == INTERFACE_HID + and interface_protocol != PROTOCOL_KEYBOARD + and protocol_type == PROTOCOL_MOUSE + and subclass != SUBCLASS_BOOT + ): + candidate_found = True + + # Found HID Descriptor (Contains Report Length) + elif descriptor_type == DESC_HID and candidate_found: + # The HID descriptor stores the Report Descriptor length at offset 7 + # Bytes: [Length, Type, BCD, BCD, Country, Count, ReportType, ReportLenL, ReportLenH] + if descriptor_len >= 9: + hid_desc_len = config_descriptor[i+7] + (config_descriptor[i+8] << 8) elif descriptor_type == DESC_ENDPOINT: endpoint_address = config_descriptor[i + 2] if endpoint_address & _DIR_IN: if found_mouse: - return mouse_interface_index, endpoint_address + endpoints.append((mouse_interface_index, endpoint_address)) + if len(endpoints) == count: + return endpoints + + elif candidate_found: + print(f"Checking Interface {interface_number}...") + + # If it's Protocol 2, it's definitely a mouse (Standard). + # If it's Protocol 0, we must check the descriptor. + is_mouse = False + + if hid_desc_len > 0: + rep_desc = get_report_descriptor(device, interface_number, hid_desc_len) + if _is_confirmed_mouse(rep_desc): + is_mouse = True + print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") + else: + print(f" -> REJECTED: Generic HID, but not a mouse (Joystick/Ups?)") + else: + # Fallback if we missed the HID descriptor, assume no if Candidate + print(" -> Warning: Could not verify Usage, assuming no.") + is_mouse = False + + if is_mouse: + endpoints.append((interface_number, endpoint_address)) + if len(endpoints) == count: + return endpoints + else: + candidate_found = False # Stop looking at this interface + i += descriptor_len - return None, None + return [(None, None)] def find_boot_mouse_endpoint(device): @@ -120,8 +242,16 @@ def find_boot_mouse_endpoint(device): :param device: The device to search within :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise """ - return _find_boot_endpoint(device, PROTOCOL_MOUSE) + return _find_endpoint(device, 1, PROTOCOL_MOUSE, SUBCLASS_BOOT)[0] +def find_report_mouse_endpoint(device): + """ + Try to find a report mouse endpoint in the device and return its + interface index, and endpoint address. + :param device: The device to search within + :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise + """ + return _find_endpoint(device, 1, PROTOCOL_MOUSE, SUBCLASS_REPORT)[0] def find_boot_keyboard_endpoint(device): """ @@ -130,4 +260,5 @@ def find_boot_keyboard_endpoint(device): :param device: The device to search within :return: keyboard_interface_index, keyboard_endpoint_address if found, or None, None otherwise """ - return _find_boot_endpoint(device, PROTOCOL_KEYBOARD) + return _find_endpoint(device, 1, PROTOCOL_KEYBOARD, SUBCLASS_BOOT)[0] + From 2b6c1c79f16c865832cf9c568d094dfd3b028dc8 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Sun, 23 Nov 2025 21:58:40 -0500 Subject: [PATCH 2/8] Ruff updates --- adafruit_usb_host_descriptors.py | 83 ++++++++++++++------------------ 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index 358a502..a91a27b 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -43,7 +43,7 @@ DESC_INTERFACE = 0x04 DESC_ENDPOINT = 0x05 DESC_HID = 0x21 -DESC_REPORT = 0X22 +DESC_REPORT = 0x22 INTERFACE_HID = 0x03 SUBCLASS_BOOT = 0x01 @@ -52,16 +52,16 @@ PROTOCOL_KEYBOARD = 0x01 # --- HID Report Descriptor Item Tags (The "Command") --- -HID_TAG_USAGE_PAGE = 0x05 # Defines the category (e.g., Generic Desktop, Game Controls) -HID_TAG_USAGE = 0x09 # Defines the specific item (e.g., Mouse, Joystick) +HID_TAG_USAGE_PAGE = 0x05 # Defines the category (e.g., Generic Desktop, Game Controls) +HID_TAG_USAGE = 0x09 # Defines the specific item (e.g., Mouse, Joystick) # --- Usage Page IDs (Values for 0x05) --- USAGE_PAGE_GENERIC_DESKTOP = 0x01 # --- Usage IDs (Values for 0x09, inside Generic Desktop) --- -USAGE_MOUSE = 0x02 +USAGE_MOUSE = 0x02 USAGE_JOYSTICK = 0x04 -USAGE_GAMEPAD = 0x05 +USAGE_GAMEPAD = 0x05 USAGE_KEYBOARD = 0x06 @@ -100,11 +100,15 @@ def get_configuration_descriptor(device, index): get_descriptor(device, DESC_CONFIGURATION, index, full_buf) return full_buf + def get_report_descriptor(device, interface_num, length): """ Fetches the HID Report Descriptor. This tells us what the device actually IS (Mouse vs Joystick). """ + if length < 1: + return None + buf = bytearray(length) try: # 0x81 = Dir: IN | Type: Standard | Recipient: Interface @@ -114,13 +118,14 @@ def get_report_descriptor(device, interface_num, length): _REQ_GET_DESCRIPTOR, DESC_REPORT << 8, interface_num, - buf + buf, ) return buf except Exception as e: print(f"Failed to read Report Descriptor: {e}") return None + def _is_confirmed_mouse(report_desc): """ Scans the raw descriptor bytes for: @@ -129,33 +134,33 @@ def _is_confirmed_mouse(report_desc): """ if not report_desc: return False - + # Simple byte scan check # We look for Usage Page Generic Desktop (0x05 0x01) has_generic_desktop = False for i in range(len(report_desc) - 1): - if report_desc[i] == HID_TAG_USAGE_PAGE and report_desc[i+1] == USAGE_PAGE_GENERIC_DESKTOP: + if ( + report_desc[i] == HID_TAG_USAGE_PAGE + and report_desc[i + 1] == USAGE_PAGE_GENERIC_DESKTOP + ): has_generic_desktop = True - + # We look for Usage Mouse (0x09 0x02) has_mouse_usage = False for i in range(len(report_desc) - 1): - if report_desc[i] == HID_TAG_USAGE and report_desc[i+1] == USAGE_MOUSE: + if report_desc[i] == HID_TAG_USAGE and report_desc[i + 1] == USAGE_MOUSE: has_mouse_usage = True - - return has_generic_desktop and has_mouse_usage + return has_generic_desktop and has_mouse_usage -def _find_endpoint(device, count, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD], subclass): - # pass a count of <= 0 to return all HID interfaces/endpoints of selected protocol_type on the device +def _find_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD], subclass): config_descriptor = get_configuration_descriptor(device, 0) i = 0 mouse_interface_index = None found_mouse = False candidate_found = False hid_desc_len = 0 - endpoints = [] while i < len(config_descriptor): descriptor_len = config_descriptor[i] descriptor_type = config_descriptor[i + 1] @@ -180,7 +185,7 @@ def _find_endpoint(device, count, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCO ): found_mouse = True mouse_interface_index = interface_number - + # May be trackpad interface if it's not a keyboard and looking for mouse elif ( interface_class == INTERFACE_HID @@ -195,44 +200,29 @@ def _find_endpoint(device, count, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCO # The HID descriptor stores the Report Descriptor length at offset 7 # Bytes: [Length, Type, BCD, BCD, Country, Count, ReportType, ReportLenL, ReportLenH] if descriptor_len >= 9: - hid_desc_len = config_descriptor[i+7] + (config_descriptor[i+8] << 8) + hid_desc_len = config_descriptor[i + 7] + (config_descriptor[i + 8] << 8) elif descriptor_type == DESC_ENDPOINT: endpoint_address = config_descriptor[i + 2] if endpoint_address & _DIR_IN: if found_mouse: - endpoints.append((mouse_interface_index, endpoint_address)) - if len(endpoints) == count: - return endpoints + return mouse_interface_index, endpoint_address elif candidate_found: print(f"Checking Interface {interface_number}...") - + # If it's Protocol 2, it's definitely a mouse (Standard). # If it's Protocol 0, we must check the descriptor. - is_mouse = False - - if hid_desc_len > 0: - rep_desc = get_report_descriptor(device, interface_number, hid_desc_len) - if _is_confirmed_mouse(rep_desc): - is_mouse = True - print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") - else: - print(f" -> REJECTED: Generic HID, but not a mouse (Joystick/Ups?)") - else: - # Fallback if we missed the HID descriptor, assume no if Candidate - print(" -> Warning: Could not verify Usage, assuming no.") - is_mouse = False - - if is_mouse: - endpoints.append((interface_number, endpoint_address)) - if len(endpoints) == count: - return endpoints - else: - candidate_found = False # Stop looking at this interface + + rep_desc = get_report_descriptor(device, interface_number, hid_desc_len) + if _is_confirmed_mouse(rep_desc): + print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") + return interface_number, endpoint_address + + candidate_found = False # Stop looking at this interface i += descriptor_len - return [(None, None)] + return None, None def find_boot_mouse_endpoint(device): @@ -242,7 +232,8 @@ def find_boot_mouse_endpoint(device): :param device: The device to search within :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise """ - return _find_endpoint(device, 1, PROTOCOL_MOUSE, SUBCLASS_BOOT)[0] + return _find_endpoint(device, PROTOCOL_MOUSE, SUBCLASS_BOOT) + def find_report_mouse_endpoint(device): """ @@ -251,7 +242,8 @@ def find_report_mouse_endpoint(device): :param device: The device to search within :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise """ - return _find_endpoint(device, 1, PROTOCOL_MOUSE, SUBCLASS_REPORT)[0] + return _find_endpoint(device, PROTOCOL_MOUSE, SUBCLASS_REPORT) + def find_boot_keyboard_endpoint(device): """ @@ -260,5 +252,4 @@ def find_boot_keyboard_endpoint(device): :param device: The device to search within :return: keyboard_interface_index, keyboard_endpoint_address if found, or None, None otherwise """ - return _find_endpoint(device, 1, PROTOCOL_KEYBOARD, SUBCLASS_BOOT)[0] - + return _find_endpoint(device, PROTOCOL_KEYBOARD, SUBCLASS_BOOT) From cceedb54b0610e5be20b3b1906be478e5fee2849 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Sun, 23 Nov 2025 22:19:02 -0500 Subject: [PATCH 3/8] Unuseful comments --- adafruit_usb_host_descriptors.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index a91a27b..646d19b 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -210,10 +210,6 @@ def _find_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBO elif candidate_found: print(f"Checking Interface {interface_number}...") - - # If it's Protocol 2, it's definitely a mouse (Standard). - # If it's Protocol 0, we must check the descriptor. - rep_desc = get_report_descriptor(device, interface_number, hid_desc_len) if _is_confirmed_mouse(rep_desc): print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") From 0c27ba3746761230a3d3e089c663bb61fa1cc95b Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Mon, 1 Dec 2025 14:33:35 -0500 Subject: [PATCH 4/8] Fix "No Subclass" (Reserved) constant definition --- adafruit_usb_host_descriptors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index 646d19b..209498e 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -47,7 +47,7 @@ INTERFACE_HID = 0x03 SUBCLASS_BOOT = 0x01 -SUBCLASS_REPORT = None +SUBCLASS_RESERVED = 0x00 PROTOCOL_MOUSE = 0x02 PROTOCOL_KEYBOARD = 0x01 @@ -238,7 +238,7 @@ def find_report_mouse_endpoint(device): :param device: The device to search within :return: mouse_interface_index, mouse_endpoint_address if found, or None, None otherwise """ - return _find_endpoint(device, PROTOCOL_MOUSE, SUBCLASS_REPORT) + return _find_endpoint(device, PROTOCOL_MOUSE, SUBCLASS_RESERVED) def find_boot_keyboard_endpoint(device): From 52f6909f59139ae996a0b615f6ed4f57c0fa153e Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Tue, 2 Dec 2025 01:39:36 -0500 Subject: [PATCH 5/8] Additional information prints --- adafruit_usb_host_descriptors.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index 209498e..1e875c6 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -191,9 +191,10 @@ def _find_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBO interface_class == INTERFACE_HID and interface_protocol != PROTOCOL_KEYBOARD and protocol_type == PROTOCOL_MOUSE - and subclass != SUBCLASS_BOOT + and subclass == SUBCLASS_RESERVED ): candidate_found = True + mouse_interface_index = interface_number # Found HID Descriptor (Contains Report Length) elif descriptor_type == DESC_HID and candidate_found: @@ -209,11 +210,11 @@ def _find_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBO return mouse_interface_index, endpoint_address elif candidate_found: - print(f"Checking Interface {interface_number}...") - rep_desc = get_report_descriptor(device, interface_number, hid_desc_len) + print(f"Checking Interface {mouse_interface_index}...") + rep_desc = get_report_descriptor(device, mouse_interface_index, hid_desc_len) if _is_confirmed_mouse(rep_desc): - print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") - return interface_number, endpoint_address + print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02) index:{mouse_interface_index}, end:{endpoint_address}") + return mouse_interface_index, endpoint_address candidate_found = False # Stop looking at this interface From 24bfac4ad404f737cfbd7ee841cf29456c51a5a4 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Tue, 2 Dec 2025 01:47:10 -0500 Subject: [PATCH 6/8] ruff: reduce line length --- adafruit_usb_host_descriptors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index 1e875c6..3260c4f 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -213,7 +213,7 @@ def _find_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBO print(f"Checking Interface {mouse_interface_index}...") rep_desc = get_report_descriptor(device, mouse_interface_index, hid_desc_len) if _is_confirmed_mouse(rep_desc): - print(f" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02) index:{mouse_interface_index}, end:{endpoint_address}") + print(" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") return mouse_interface_index, endpoint_address candidate_found = False # Stop looking at this interface From d1a8a81ac2bfa6e72df92056279426de3976a1c1 Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Tue, 2 Dec 2025 17:38:05 -0500 Subject: [PATCH 7/8] remove print and narrow try/except --- adafruit_usb_host_descriptors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index 3260c4f..345d444 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -11,6 +11,7 @@ """ import struct +import usb from micropython import const @@ -121,7 +122,7 @@ def get_report_descriptor(device, interface_num, length): buf, ) return buf - except Exception as e: + except usb.core.USBError as e: print(f"Failed to read Report Descriptor: {e}") return None @@ -210,10 +211,8 @@ def _find_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBO return mouse_interface_index, endpoint_address elif candidate_found: - print(f"Checking Interface {mouse_interface_index}...") rep_desc = get_report_descriptor(device, mouse_interface_index, hid_desc_len) if _is_confirmed_mouse(rep_desc): - print(" -> CONFIRMED: It is a Mouse/Trackpad (Usage 0x09 0x02)") return mouse_interface_index, endpoint_address candidate_found = False # Stop looking at this interface From e2e6652e9dd06b0e615078b3ad2d7e62c8e1ed8a Mon Sep 17 00:00:00 2001 From: RetiredWizard Date: Tue, 2 Dec 2025 18:48:27 -0500 Subject: [PATCH 8/8] run pre-commit --- adafruit_usb_host_descriptors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_usb_host_descriptors.py b/adafruit_usb_host_descriptors.py index 345d444..f6e74ef 100644 --- a/adafruit_usb_host_descriptors.py +++ b/adafruit_usb_host_descriptors.py @@ -11,8 +11,8 @@ """ import struct -import usb +import usb from micropython import const try: