"""Minimal composite keyboard/mouse USB HID device for MicroPython ESP32-S2/S3. 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. """ from machine import USBDevice # Combined keyboard, mouse, and Consumer Control HID report descriptor. REPORT_DESCRIPTOR = bytes(( 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xC0, 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x02, 0x09, 0x01, 0xA1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x03, 0x15, 0x00, 0x25, 0x01, 0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02, 0x81, 0x06, 0xC0, 0xC0, # Consumer Control: Volume Increment and Volume Decrement. 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x03, 0x15, 0x00, 0x25, 0x01, 0x09, 0xE9, 0x09, 0xEA, 0x75, 0x01, 0x95, 0x02, 0x81, 0x02, 0x75, 0x06, 0x95, 0x01, 0x81, 0x01, 0xC0, )) # Configuration descriptor: one HID interface, 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, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, len(REPORT_DESCRIPTOR) & 0xFF, len(REPORT_DESCRIPTOR) >> 8, 0x07, 0x05, 0x81, 0x03, 0x10, 0x00, 0x0A, )) # HID class requests used by common desktop hosts. _GET_DESCRIPTOR = 0x06 _DESCRIPTOR_TYPE_REPORT = 0x22 class HID: """Queues one USB report at a time, avoiding writes to a busy endpoint.""" def __init__(self): self._usb = USBDevice() self._ready = False self._busy = False self._pending = [] 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) def _open_interface(self, interface_descriptor): # Endpoint descriptors follow the nine-byte interface descriptor. offset = 9 while offset + 1 < len(interface_descriptor): length = interface_descriptor[offset] descriptor_type = interface_descriptor[offset + 1] if length < 2: break if descriptor_type == 0x05: self._usb.ep_open(interface_descriptor[offset:offset + length]) offset += length self._ready = True def _reset(self): self._ready = False self._busy = False 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: return REPORT_DESCRIPTOR return False def _transfer_done(self, endpoint, result, transferred): if endpoint == 0x81: self._busy = False self._flush() def _flush(self): if not self._ready or self._busy or not self._pending: return report = self._pending.pop(0) try: self._usb.submit_xfer(0x81, report) self._busy = True except OSError: # A host may disconnect between scheduling and transfer. Dropping # an old movement/key event is safer than replaying it on reconnect. self._busy = False def _send(self, report): self._pending.append(report) 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): self._send(bytes((1, 0, 0, 0, 0, 0, 0, 0, 0))) def move(self, x, y): x = max(-1, min(1, int(x))) y = max(-1, min(1, int(y))) self._send(bytes((2, 0, x & 0xFF, y & 0xFF, 0))) def press_volume_up(self): self._send(bytes((3, 0x01))) def press_volume_down(self): self._send(bytes((3, 0x02))) def release_consumer(self): self._send(bytes((3, 0)))