"""Composite keyboard, mouse, and media-key USB HID device for MicroPython. The interface must be initialised by ``boot.py`` so it is configured before the ESP32-S3 native USB subsystem starts. """ 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, )) # 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, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, len(REPORT_DESCRIPTOR) & 0xFF, len(REPORT_DESCRIPTOR) >> 8, 0x07, 0x05, 0x81, 0x03, 0x10, 0x00, 0x0A, )) STRINGS = [None, "PolterHID", "Awareness trainer", "PHID-001"] _GET_DESCRIPTOR = 0x06 _DESCRIPTOR_TYPE_REPORT = 0x22 _MAX_PENDING_REPORTS = 8 _INSTANCE = None class HID: """Queue reports while the USB interrupt endpoint is busy.""" def __init__(self): self._usb = USBDevice() self._ready = False self._busy = False self._pending = [] self._configure() def _configure(self): # 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): # MicroPython opens the endpoints described by this accepted interface; # USBDevice 1.29 does not expose an ep_open() method to Python. self._ready = True def _reset(self): self._ready = False self._busy = False self._pending = [] def _control_transfer(self, stage, request): # The report descriptor buffer is supplied at SETUP, but the same # request must be accepted through its DATA and ACK stages. if (request[1] == _GET_DESCRIPTOR and request[3] == _DESCRIPTOR_TYPE_REPORT): return REPORT_DESCRIPTOR if stage == 1 else True # Desktop HID drivers commonly issue these class requests while # initialising. There is no protocol/idle state to maintain here, but # they must not be stalled. if request[1] in (0x0A, 0x0B): # SET_IDLE, SET_PROTOCOL return True 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._busy = self._usb.submit_xfer(0x81, report) except OSError: # Drop the event if the host disconnects between scheduling and IO. self._busy = False def _send(self, report): if not self._ready: return report_id = report[0] for index, pending in enumerate(self._pending): if pending[0] == report_id: self._pending[index] = report self._flush() return if len(self._pending) >= _MAX_PENDING_REPORTS: self._pending.pop(0) self._pending.append(report) self._flush() def press_return(self): 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(-127, min(127, int(x))) y = max(-127, min(127, 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))) 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