156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
"""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
|
|
_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):
|
|
# 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):
|
|
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._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):
|
|
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(-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)))
|
|
|
|
|
|
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
|