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:
@@ -0,0 +1,157 @@
|
||||
// tests for the emulated mouse: tests/test_mouse.cpp
|
||||
//
|
||||
// The two things worth proving about a relative mouse are that a frame which changes
|
||||
// nothing sends nothing, and that a button release is never conditional on movement.
|
||||
// Everything else about it is arithmetic.
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include "mouse.h"
|
||||
#include "test.h"
|
||||
|
||||
using kr::mouse::Button;
|
||||
using kr::mouse::Frame;
|
||||
using kr::mouse::MouseReport;
|
||||
using kr::mouse::MouseState;
|
||||
|
||||
namespace {
|
||||
|
||||
bool allZero(const MouseReport &r) {
|
||||
const uint8_t *b = reinterpret_cast<const uint8_t *>(&r);
|
||||
for (size_t i = 0; i < sizeof(MouseReport); i++) {
|
||||
if (b[i] != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
CASE("the report is the five bytes the descriptor promises");
|
||||
{
|
||||
CHECK_EQ(sizeof(MouseReport), 5);
|
||||
// The layout is asserted, not assumed: buttons, X, Y, wheel, pan. A struct that
|
||||
// reordered itself would still compile and would move the target's cursor with the
|
||||
// wheel byte.
|
||||
CHECK_EQ(offsetof(MouseReport, buttons), 0);
|
||||
CHECK_EQ(offsetof(MouseReport, dx), 1);
|
||||
CHECK_EQ(offsetof(MouseReport, dy), 2);
|
||||
CHECK_EQ(offsetof(MouseReport, wheel), 3);
|
||||
CHECK_EQ(offsetof(MouseReport, pan), 4);
|
||||
}
|
||||
|
||||
CASE("button bits are the report's order, not the DOM's");
|
||||
{
|
||||
CHECK_EQ(kr::mouse::kBtnLeft, 0x01);
|
||||
CHECK_EQ(kr::mouse::kBtnRight, 0x02);
|
||||
CHECK_EQ(kr::mouse::kBtnMiddle, 0x04);
|
||||
CHECK_EQ(kr::mouse::kBtnBackward, 0x08);
|
||||
CHECK_EQ(kr::mouse::kBtnForward, 0x10);
|
||||
CHECK_EQ(kr::mouse::kBtnAll, 0x1F);
|
||||
}
|
||||
|
||||
CASE("movement produces a report, nothing produces nothing");
|
||||
{
|
||||
MouseState m;
|
||||
MouseReport r;
|
||||
CHECK(m.apply(Frame{0, 30, -4, 0, 0}, r));
|
||||
CHECK_EQ(r.buttons, 0);
|
||||
CHECK_EQ(r.dx, 30);
|
||||
CHECK_EQ(r.dy, -4);
|
||||
CHECK_EQ(r.wheel, 0);
|
||||
CHECK_EQ(r.pan, 0);
|
||||
|
||||
// An idle frame is the whole endpoint for that frame and says nothing, so it is
|
||||
// dropped rather than sent -- this is what stops a chatty controller keeping a
|
||||
// target busy at 125 Hz with a pointer that is not moving.
|
||||
CHECK(!m.apply(Frame{0, 0, 0, 0, 0}, r));
|
||||
CHECK(!m.apply(Frame{0, 0, 0, 0, 0}, r));
|
||||
}
|
||||
|
||||
CASE("a button change is a report even with no movement at all");
|
||||
{
|
||||
MouseState m;
|
||||
MouseReport r;
|
||||
CHECK(m.apply(Frame{kr::mouse::kBtnLeft, 0, 0, 0, 0}, r));
|
||||
CHECK_EQ(r.buttons, kr::mouse::kBtnLeft);
|
||||
CHECK(allZero(kr::mouse::kReportClear)); // the report a fail-safe sends
|
||||
|
||||
// Repeating the same button byte with no movement is not a new state.
|
||||
CHECK(!m.apply(Frame{kr::mouse::kBtnLeft, 0, 0, 0, 0}, r));
|
||||
CHECK(m.apply(Frame{0, 0, 0, 0, 0}, r)); // ...but letting go is
|
||||
CHECK_EQ(r.buttons, 0);
|
||||
}
|
||||
|
||||
CASE("buttons are restated, not diffed");
|
||||
{
|
||||
MouseState m;
|
||||
MouseReport r;
|
||||
m.apply(Frame{kr::mouse::kBtnLeft, 0, 0, 0, 0}, r);
|
||||
m.apply(Frame{kr::mouse::kBtnLeft | kr::mouse::kBtnRight, 0, 0, 0, 0}, r);
|
||||
CHECK_EQ(r.buttons, kr::mouse::kBtnLeft | kr::mouse::kBtnRight);
|
||||
// Letting go of one reports the state that remains, which is the whole reason a
|
||||
// frame lost in the network cannot strand a button: the next frame is the truth.
|
||||
m.apply(Frame{kr::mouse::kBtnRight, 5, 0, 0, 0}, r);
|
||||
CHECK_EQ(r.buttons, kr::mouse::kBtnRight);
|
||||
CHECK_EQ(r.dx, 5);
|
||||
CHECK_EQ(m.buttons(), kr::mouse::kBtnRight);
|
||||
CHECK(m.down());
|
||||
}
|
||||
|
||||
CASE("only the five buttons the report has");
|
||||
{
|
||||
MouseState m;
|
||||
MouseReport r;
|
||||
// The parser refuses a sixth bit, so this is a second line of defence rather than
|
||||
// the behaviour anyone relies on: whatever arrives cannot set a padding bit.
|
||||
m.apply(Frame{0xFF, 1, 0, 0, 0}, r);
|
||||
CHECK_EQ(r.buttons, 0x1F);
|
||||
CHECK_EQ(m.buttons(), 0x1F);
|
||||
}
|
||||
|
||||
CASE("releaseAll lets go, from any state, and is idempotent");
|
||||
{
|
||||
MouseState m;
|
||||
MouseReport r;
|
||||
// Nothing was ever down; letting go is still well-defined and leaves the same state.
|
||||
m.releaseAll();
|
||||
CHECK_EQ(m.buttons(), 0);
|
||||
CHECK(!m.down());
|
||||
|
||||
m.apply(Frame{kr::mouse::kBtnMiddle, 0, 0, 0, 0}, r);
|
||||
m.releaseAll();
|
||||
CHECK_EQ(m.buttons(), 0);
|
||||
CHECK(!m.down());
|
||||
m.releaseAll();
|
||||
CHECK_EQ(m.buttons(), 0);
|
||||
|
||||
// And after letting go, the next movement frame restates the empty button byte -- so
|
||||
// a release that never reached the target is corrected by the next one regardless.
|
||||
CHECK(m.apply(Frame{0, 4, 0, 0, 0}, r));
|
||||
CHECK_EQ(r.buttons, 0);
|
||||
}
|
||||
|
||||
CASE("the full range of one signed byte survives");
|
||||
{
|
||||
MouseState m;
|
||||
MouseReport r;
|
||||
CHECK(m.apply(Frame{0, 127, -127, 127, -127}, r));
|
||||
CHECK_EQ(r.dx, 127);
|
||||
CHECK_EQ(r.dy, -127);
|
||||
CHECK_EQ(r.wheel, 127);
|
||||
CHECK_EQ(r.pan, -127);
|
||||
}
|
||||
|
||||
CASE("report equality is byte equality");
|
||||
{
|
||||
const MouseReport a{1, 2, 3, 4, 5};
|
||||
const MouseReport b{1, 2, 3, 4, 5};
|
||||
const MouseReport c{1, 2, 3, 4, 6};
|
||||
const MouseReport d{0, 2, 3, 4, 5};
|
||||
CHECK(kr::mouse::reportsEqual(a, b));
|
||||
CHECK(!kr::mouse::reportsEqual(a, c)); // pan differs: still a different report
|
||||
CHECK(!kr::mouse::reportsEqual(a, d));
|
||||
}
|
||||
|
||||
return report("mouse");
|
||||
}
|
||||
Reference in New Issue
Block a user