Files
ducoterra 56aa97d60d chore(agent): phase roadmap from TODO.md, 5 phases
The four open items in TODO.md become an executable pipeline: 01 the
verification gate (host suites + sanitiser + both firmware envs, and the
hardware contract so no unit blocks on a board nobody is holding), 02 HTTPS
and wss:// with an identity the board generates itself into NVS, 03 session
takeover that always lands disarmed, 04 a responsive page whose safety
statements are pinned before they are trimmed, 05 the phone as a controller
(press-and-hold pad, soft keyboard, modifier latches over a new Q= frame).

TODO.md is cleared to its empty title; the items live in .agents/phases/ now.
AGENTS.md gains the five protocol rules and nothing else.

LLM-Generated: true
2026-11-08 23:28:25 -05:00

5.2 KiB

Task 01 — Touch pad: the capture is the finger

Phase: 05_mobile_controller · Source: TODO.md:5 — "Needs to be mobile compatible — … Mobile trackpad for touch trackpad." Story: n/a

Objective

#mPad becomes a touch pad: press and hold it and the target's pointer moves with your finger, drag two fingers to scroll, and the moment the touch ends the movement stops and every button lets go. Pointer Lock does not exist for touch, so the capture is the touch itself.

Work

  1. src/web_page.h, the mouse block:
    • touchstart on #mPad: requires armed and mouseAvailable() (s.mouse === 1) — the same two gates as takeMouse(), and the same E10/E11 refusal path if the board disagrees. Sets mTouchActive, records the anchor point, and takes no button.
    • touchmove: compute the delta from the previous sample, feed the existing movement pipeline (the code behind flushMouse()) so the ±127 cap and the multi-frame split are the ones already tested — do not write a second splitter. Coalesce per report the way mousemove does, and drop movement when there is no link rather than queueing it (the page's existing rule: a queue of pointer motion delivered after you stopped moving is the pointer version of a stuck key).
    • Two-touch drag → wheel/pan through the same quantiser the desktop wheel handler uses (notches with the remainder carried), not movement. touchmove with two fingers must not also emit dx/dy.
    • touchend/touchcancel, and blur/visibilitychange/disarm/link loss: end the capture, dropMouse() (which releases every button through the funnel), reset the anchor. touchcancel is the browser taking the gesture away — incoming call, notification shade, a palm — and it must land exactly like lifting the finger.
    • A visible button row beside the pad: Left / Right / Middle, held while pressed (touchstart down, touchend up), carrying the existing MOUSE_BTN bits. Tap on the pad itself never clicks. The row is 44 px and shows which bits are down, so "left is held" is a thing you can see before you ask what is stuck.
    • The pad's label changes with state (not captured / captured — move to move, lift to release) since a phone has no cursor to indicate either.
  2. refreshMouseUi() / paintMouse() / mouseAvailable() — teach them the touch state so the pills say what is true. mCaptured (Pointer Lock) and mTouchActive are different states that must render distinctly and never both claim the pointer.
  3. Keep the desktop path byte-for-byte identical: mousemove, the Pointer Lock request, Esc, wheel on a mouse — phase 04's assertions and today's test_page.js mouse checks must stay green without edit.
  4. tests/test_page.js — extend the shim for multi-point touch and add: capture requires armed + mouse:1; a drag produces one M= per flush with no idle frames; a large drag splits with the net displacement exact; two fingers produce wheel/pan and no dx/dy; touchend and touchcancel each release every button and stop movement; button-row bits equal MOUSE_BTN; every emitted frame parses with the firmware's own parser.
  5. README.md — Controller page: the pad row of the element table describes the touch behaviour; "What capture does and does not do" gains the touch sentence (this is elaborated properly in task 04 — here, only what this task changes).
  • ASSUMPTION: one finger moves, two fingers scroll, and clicking is an explicit held button rather than a tap. Tap-to-click on a pad whose whole job is movement produces clicks the operator did not ask for, and on a remote keyboard a click the target did not expect is the worst class of surprise.
  • ASSUMPTION: no "tap-and-hold to drag" (holding a button while moving). It needs a second simultaneous gesture and any mis-timing strands a button on the target; the button row gives the same capability with visible state.
  • ASSUMPTION: scale of finger-delta to HID delta is 1:1 (one CSS pixel → one report unit) at first. If it feels slow on hardware, the tuning knob goes in config.h-style naming on the page side with the measured value in the comment — the board must not invent a pointer speed of its own.

Testing & Quality

  • node tests/test_page.js: the new touch checks pass and the existing mouse checks are unmodified (prove it: the diff of that test file adds, it does not change, mouse assertions).
  • bash .agents/validate.sh green; test_proto.cpp, test_mouse.cpp, test_output.cpp untouched.
  • Device (best effort, one attempt): a phone on the board's AP — movement lands, lift stops it, and watch_target.py shows no stuck button across 20 press/release cycles.

Completion Criteria

  • Press-and-hold on the pad moves the target's pointer; lifting stops it and releases every button (observed, or recorded skipped (no device) with the commands).
  • Two-finger drag scrolls and never moves the pointer.
  • Left/Right/Middle are held-while-pressed and visible; tap on the pad clicks nothing.
  • Desktop Pointer Lock path unchanged and still covered by the pre-existing checks.
  • Full test suite green.