Files
PolterHID/hid.py
T

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