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:
@@ -45,11 +45,33 @@ struct StatusSnapshot {
|
||||
uint32_t typedChars = 0; // characters sent by pasted text (a count, never the text)
|
||||
uint16_t typing = 0; // characters still queued in the paste in progress
|
||||
bool rollover = false;
|
||||
// The mouse: whether it can report at all, which buttons the board believes are
|
||||
// down, and how many reports have gone out. `mouse` is false when the interface is
|
||||
// switched off on the board or when the target has forced boot protocol -- in both
|
||||
// cases the keyboard is untouched and M= frames are refused, not ignored.
|
||||
bool mouse = false;
|
||||
uint8_t mouseButtons = 0;
|
||||
uint32_t mouseFrames = 0;
|
||||
uint8_t ledState = 0; // the target's caps/num/scroll LEDs, per its HID reports
|
||||
bool defaultToken = false;
|
||||
bool apForced = false;
|
||||
};
|
||||
|
||||
// Error codes, named. The wire form is `E<code>:<text>`; see proto.h for the list.
|
||||
enum ErrorCode : uint8_t {
|
||||
kErrNotAuthed = 1,
|
||||
kErrLockedOut = 2,
|
||||
kErrBusy = 3,
|
||||
kErrNotArmed = 4,
|
||||
kErrNoTarget = 5,
|
||||
kErrBadMessage = 6,
|
||||
kErrQueueFull = 7,
|
||||
kErrTextFull = 8,
|
||||
kErrUntypeable = 9,
|
||||
kErrMouseDisabled = 10,
|
||||
kErrMouseBootProtocol = 11,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the status frame. Split out of the class so it is callable from the serial
|
||||
* console too, and so the field names stay in one place.
|
||||
@@ -75,6 +97,14 @@ class ControlServer {
|
||||
void sendError(uint8_t code, const char *text);
|
||||
|
||||
bool linked() const { return _phase == Phase::WebSocket && _authed; }
|
||||
/**
|
||||
* True from the first successful authentication until reboot. The silence timer needs
|
||||
* this as well as lastInboundMs(): an armed board with no controller at all (armed by
|
||||
* its own button, or by the serial console) has nothing to hear from, and must not be
|
||||
* disarmed for it -- but a board whose controller came and went must never stay armed
|
||||
* on the strength of a link that no longer exists.
|
||||
*/
|
||||
bool controllerSeen() const { return _controllerSeen; }
|
||||
uint32_t lastInboundMs() const { return _lastInboundMs; }
|
||||
uint32_t rejected() const { return _rejected; }
|
||||
uint32_t authFails() const { return _authFails; }
|
||||
@@ -111,6 +141,7 @@ class ControlServer {
|
||||
ws::Reader _reader{_wsBuf, sizeof(_wsBuf)};
|
||||
|
||||
bool _authed = false;
|
||||
bool _controllerSeen = false;
|
||||
uint32_t _connectedAtMs = 0;
|
||||
uint32_t _lastInboundMs = 0;
|
||||
uint32_t _lastStatusMs = 0;
|
||||
|
||||
Reference in New Issue
Block a user