Add PolterHID awareness training firmware
This commit is contained in:
@@ -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)))
|
||||
Reference in New Issue
Block a user