Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 122 additions & 4 deletions adafruit_usb_host_descriptors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import struct

import usb
from micropython import const

try:
Expand All @@ -34,18 +35,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_RESERVED = 0x00
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."""
Expand Down Expand Up @@ -83,32 +102,121 @@ def get_configuration_descriptor(device, index):
return full_buf


def _find_boot_endpoint(device, protocol_type: Literal[PROTOCOL_MOUSE, PROTOCOL_KEYBOARD]):
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
# 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 usb.core.USBError 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, 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
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_RESERVED
):
candidate_found = True
mouse_interface_index = interface_number

# 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

elif candidate_found:
rep_desc = get_report_descriptor(device, mouse_interface_index, hid_desc_len)
if _is_confirmed_mouse(rep_desc):
return mouse_interface_index, endpoint_address

candidate_found = False # Stop looking at this interface

i += descriptor_len
return None, None

Expand All @@ -120,7 +228,17 @@ 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, PROTOCOL_MOUSE, SUBCLASS_BOOT)


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, PROTOCOL_MOUSE, SUBCLASS_RESERVED)


def find_boot_keyboard_endpoint(device):
Expand All @@ -130,4 +248,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_boot_endpoint(device, PROTOCOL_KEYBOARD)
return _find_endpoint(device, PROTOCOL_KEYBOARD, SUBCLASS_BOOT)