138 lines
5.1 KiB
Python
138 lines
5.1 KiB
Python
"""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)))
|