Add PolterHID awareness training firmware

This commit is contained in:
2026-08-26 11:38:52 +02:00
commit d0c926abd6
6 changed files with 567 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
"""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)))