Add native USB HID boot setup and status display

This commit is contained in:
2026-08-26 14:10:31 +02:00
parent d0c926abd6
commit 0e9637c474
5 changed files with 218 additions and 40 deletions
+49 -31
View File
@@ -1,8 +1,7 @@
"""Minimal composite keyboard/mouse USB HID device for MicroPython ESP32-S2/S3.
"""Composite keyboard, mouse, and media-key USB HID device for MicroPython.
Requires a MicroPython build exposing `machine.USBDevice` (native USB device
support). The report descriptor uses report ID 1 for a boot-style keyboard,
report ID 2 for a relative three-button mouse, and report ID 3 for media keys.
The interface must be initialised by ``boot.py`` so it is configured before the
ESP32-S3 native USB subsystem starts.
"""
from machine import USBDevice
@@ -27,7 +26,16 @@ REPORT_DESCRIPTOR = bytes((
0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0xC0,
))
# Configuration descriptor: one HID interface, one interrupt-IN endpoint.
# This is a HID-only USB device. 0xCAFE/0x4001 are development identifiers,
# not allocated USB identifiers; replace them with assigned values for any
# product distribution.
DEVICE_DESCRIPTOR = bytes((
0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40,
0xFE, 0xCA, 0x01, 0x40, 0x00, 0x01, 0x01, 0x02,
0x03, 0x01,
))
# One HID interface and one interrupt-IN endpoint.
CONFIG_DESCRIPTOR = bytes((
0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,
0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00,
@@ -36,13 +44,14 @@ CONFIG_DESCRIPTOR = bytes((
0x07, 0x05, 0x81, 0x03, 0x10, 0x00, 0x0A,
))
# HID class requests used by common desktop hosts.
STRINGS = [None, "PolterHID", "Awareness trainer", "PHID-001"]
_GET_DESCRIPTOR = 0x06
_DESCRIPTOR_TYPE_REPORT = 0x22
_INSTANCE = None
class HID:
"""Queues one USB report at a time, avoiding writes to a busy endpoint."""
"""Queue reports while the USB interrupt endpoint is busy."""
def __init__(self):
self._usb = USBDevice()
@@ -52,22 +61,20 @@ class HID:
self._configure()
def _configure(self):
# USBDevice's custom-device API was introduced with a positional
# BUILTIN_NONE argument. The fallback supports earlier preview builds.
kwargs = {
"strs": [None, "PolterHID", "Awareness trainer", "PHID-001"],
"open_itf_cb": self._open_interface,
"reset_cb": self._reset,
"control_xfer_cb": self._control_transfer,
"xfer_cb": self._transfer_done,
}
builtin_none = getattr(USBDevice, "BUILTIN_NONE", None)
if builtin_none is None:
# Current stable custom-device API.
self._usb.config(CONFIG_DESCRIPTOR, **kwargs)
else:
# API variant that requires an explicit built-in driver selection.
self._usb.config(builtin_none, CONFIG_DESCRIPTOR, **kwargs)
# A custom descriptor replaces the built-in USB CDC configuration.
# This must run from boot.py, before native USB is made visible.
self._usb.active(False)
self._usb.builtin_driver = USBDevice.BUILTIN_NONE
self._usb.config(
DEVICE_DESCRIPTOR,
CONFIG_DESCRIPTOR,
STRINGS,
self._open_interface,
self._reset,
self._control_transfer,
self._transfer_done,
)
self._usb.active(True)
def _open_interface(self, interface_descriptor):
# Endpoint descriptors follow the nine-byte interface descriptor.
@@ -88,9 +95,8 @@ class HID:
self._pending = []
def _control_transfer(self, stage, request):
# Return our report descriptor during the setup/data stage. HID has no
# feature reports or output reports in this intentionally small design.
if stage == 1 and request[1] == _GET_DESCRIPTOR and request[3] == _DESCRIPTOR_TYPE_REPORT:
if (stage == 1 and request[1] == _GET_DESCRIPTOR and
request[3] == _DESCRIPTOR_TYPE_REPORT):
return REPORT_DESCRIPTOR
return False
@@ -104,11 +110,9 @@ class HID:
return
report = self._pending.pop(0)
try:
self._usb.submit_xfer(0x81, report)
self._busy = True
self._busy = self._usb.submit_xfer(0x81, report)
except OSError:
# A host may disconnect between scheduling and transfer. Dropping
# an old movement/key event is safer than replaying it on reconnect.
# Drop the event if the host disconnects between scheduling and IO.
self._busy = False
def _send(self, report):
@@ -116,7 +120,6 @@ class HID:
self._flush()
def press_return(self):
# Report ID, modifiers, reserved, six key slots. 0x28 is Enter/Return.
self._send(bytes((1, 0, 0, 0x28, 0, 0, 0, 0, 0)))
def release_keys(self):
@@ -135,3 +138,18 @@ class HID:
def release_consumer(self):
self._send(bytes((3, 0)))
def initialise():
"""Configure and retain the singleton HID device from boot.py."""
global _INSTANCE
if _INSTANCE is None:
_INSTANCE = HID()
return _INSTANCE
def get_hid():
"""Return the HID interface configured by boot.py."""
if _INSTANCE is None:
raise RuntimeError("HID is not initialised; install boot.py before main.py")
return _INSTANCE