feat: relative mouse on the HID interface, and the verifier to prove it

The board now enumerates as a boot keyboard plus a relative mouse on the same
HID interface. The pointer is captured, never grabbed: Pointer Lock in the
page, so Esc (the browser's own, never forwarded) always frees it, and every
frame restates the whole button byte so a frame lost in the WiFi cannot leave
a button held. Losing the lock, the tab or the link runs the same release
funnel as a stuck key, and boot protocol or `mouse off` refuses mouse frames
with E11 rather than sending bytes a boot interface would misparse as keys.

src/mouse.h owns the report, src/output.h the queues and the funnel; the page,
relayctl.py and watch_target.py carry the button bits and the evdev decoding.
The verifier's own tables are pinned now — REL_WHEEL is 8, not 2, and the
first version guessed from the report's field order, so a working wheel looked
dead while the summary said no scroll happened.

LLM-Generated: true
This commit is contained in:
2026-11-08 23:27:42 -05:00
parent 9bc8d5213c
commit ece0313d02
19 changed files with 3135 additions and 324 deletions
+12 -2
View File
@@ -83,8 +83,18 @@ def main() -> int:
sys.stdout.flush()
return 0
finally:
termios.tcsetattr(fd, termios.TCSAFLUSH, saved)
os.close(fd)
# `reboot` makes the device disappear underneath us: the CDC node is gone the
# instant the board resets, so restoring the port settings can legitimately fail.
# The command already reached the board -- do not turn a successful `reboot` into
# a Python traceback and a non-zero exit.
try:
termios.tcsetattr(fd, termios.TCSAFLUSH, saved)
except (OSError, termios.error): # termios.error is NOT an OSError here
pass
try:
os.close(fd)
except OSError:
pass
if __name__ == "__main__":