Add native USB HID boot setup and status display
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user