Browser or terminal controller drives an ESP32-S3 that acts as a Logitech G815 into a target machine running no software. Disarmed by default; arming requires the passphrase, the board button, or the serial console, and every fail-safe path cancels in-flight work. - src/: firmware with pure, host-testable modules (keystate, proto, text, ws_codec, sha1), the WebSocket control server, and the served page - tests/: host suites for the pure modules, relayctl parity, and page JS - tools/: relayctl terminal controller, serial console, bootloader helper, and an evdev target watcher - platformio.ini: bridge (USB HID) and console (no-HID) environments - README.md: single source of truth for behaviour, protocol and safety LLM-Generated: true
203 lines
5.9 KiB
C++
203 lines
5.9 KiB
C++
// tests for the key-state tracker: tests/test_keystate.cpp
|
|
//
|
|
// The properties under test are the ones that decide whether the target machine
|
|
// ends up with a key stuck down: idempotence, transition preservation, rollover,
|
|
// and that clear() really clears.
|
|
#include <cstring>
|
|
|
|
#include "keystate.h"
|
|
#include "test.h"
|
|
|
|
using kr::KeyboardReport;
|
|
using kr::KeyboardState;
|
|
|
|
namespace {
|
|
|
|
// A report as 8 hex-byte triples, for readable assertions.
|
|
void hexreport(const KeyboardReport &r, char out[32]) {
|
|
static const char kHex[] = "0123456789abcdef";
|
|
const uint8_t bytes[8] = {r.modifiers, r.reserved, r.keys[0], r.keys[1], r.keys[2],
|
|
r.keys[3], r.keys[4], r.keys[5]};
|
|
for (int i = 0; i < 8; i++) {
|
|
out[3 * i] = kHex[bytes[i] >> 4];
|
|
out[3 * i + 1] = kHex[bytes[i] & 0xF];
|
|
out[3 * i + 2] = i == 7 ? '\0' : ' ';
|
|
}
|
|
out[23] = '\0';
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
char h1[32], h2[32];
|
|
|
|
CASE("empty state is all zeroes");
|
|
{
|
|
KeyboardState s;
|
|
CHECK(!s.dirty());
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 00 00 00 00 00 00");
|
|
}
|
|
|
|
CASE("reserved byte is always zero");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {kr::kUsageA};
|
|
s.apply(kr::kModLctrl | kr::kModRgui, keys, 1);
|
|
CHECK_EQ(s.report().reserved, 0);
|
|
CHECK_EQ(s.report().modifiers, kr::kModLctrl | kr::kModRgui);
|
|
}
|
|
|
|
CASE("apply is idempotent -- same state twice dirties nothing");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {kr::kUsageA, kr::kUsageZ};
|
|
s.apply(kr::kModLshift, keys, 2);
|
|
CHECK(s.dirty());
|
|
s.ackDirty();
|
|
s.apply(kr::kModLshift, keys, 2);
|
|
CHECK(!s.dirty());
|
|
s.apply(kr::kModLshift, keys, 2);
|
|
CHECK(!s.dirty());
|
|
}
|
|
|
|
CASE("duplicates collapse, order is preserved");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {kr::kUsageA, kr::kUsageA, kr::kUsageZ};
|
|
s.apply(0, keys, 3);
|
|
CHECK_EQ(s.held(), 2u);
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 04 1d 00 00 00 00");
|
|
CHECK(s.isDown(kr::kUsageA));
|
|
CHECK(s.isDown(kr::kUsageZ));
|
|
}
|
|
|
|
CASE("usage 0x00 is not a key");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {0x00, kr::kUsageEnter, 0x00};
|
|
s.apply(0, keys, 3);
|
|
CHECK_EQ(s.held(), 1u);
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 28 00 00 00 00 00");
|
|
}
|
|
|
|
CASE("a distinct transition always produces a distinct report");
|
|
{
|
|
// A fast tap: press then release. Both states have to be reportable, or the
|
|
// keypress vanishes and the target never sees the character.
|
|
KeyboardState s;
|
|
const uint8_t down[] = {kr::kUsageA};
|
|
s.apply(0, down, 1);
|
|
CHECK(s.dirty());
|
|
s.ackDirty();
|
|
const uint8_t none = 0;
|
|
s.apply(0, &none, 0);
|
|
CHECK(s.dirty());
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 00 00 00 00 00 00");
|
|
}
|
|
|
|
CASE("modifiers alone are a reportable change");
|
|
{
|
|
KeyboardState s;
|
|
s.apply(kr::kModLalt, nullptr, 0);
|
|
CHECK(s.dirty());
|
|
CHECK_EQ(s.modifiers(), kr::kModLalt);
|
|
s.apply(0, nullptr, 0);
|
|
CHECK(s.dirty());
|
|
}
|
|
|
|
CASE("six keys is exactly the limit");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
|
|
s.apply(0, keys, 6);
|
|
CHECK(!s.rollover());
|
|
CHECK_EQ(s.held(), 6u);
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 04 05 06 07 08 09");
|
|
}
|
|
|
|
CASE("a seventh key reports ErrorRollOver in every slot");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
|
|
s.apply(0, keys, 7);
|
|
CHECK(s.rollover());
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 01 01 01 01 01 01");
|
|
// ... and releasing back under the limit reports the real keys again, so the
|
|
// host is never left with a phantom key held down.
|
|
const uint8_t six[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09};
|
|
s.apply(0, six, 6);
|
|
CHECK(!s.rollover());
|
|
hexreport(s.report(), h2);
|
|
CHECK_STR(h2, "00 00 04 05 06 07 08 09");
|
|
}
|
|
|
|
CASE("more keys than we track still cannot wedge the state");
|
|
{
|
|
KeyboardState s;
|
|
uint8_t keys[40];
|
|
for (size_t i = 0; i < sizeof(keys); i++) keys[i] = static_cast<uint8_t>(0x04 + (i % 0x1A));
|
|
s.apply(0xFF, keys, sizeof(keys));
|
|
CHECK(s.rollover());
|
|
CHECK(s.held() <= kr::kMaxTrackedKeys);
|
|
s.clear();
|
|
CHECK_EQ(s.held(), 0u);
|
|
CHECK_EQ(s.modifiers(), 0);
|
|
}
|
|
|
|
CASE("clear releases everything; a repeat clear adds nothing");
|
|
{
|
|
KeyboardState s;
|
|
const uint8_t keys[] = {kr::kUsageA, kr::kUsageSpace};
|
|
s.apply(kr::kModLctrl | kr::kModLshift, keys, 2);
|
|
s.ackDirty();
|
|
s.clear();
|
|
CHECK(s.dirty()); // the all-keys-up report still has to go out
|
|
hexreport(s.report(), h1);
|
|
CHECK_STR(h1, "00 00 00 00 00 00 00 00");
|
|
s.ackDirty();
|
|
s.clear();
|
|
CHECK(!s.dirty()); // nothing changed, so no pointless second report
|
|
s.apply(kr::kModLctrl, nullptr, 0);
|
|
CHECK(s.dirty()); // ... and a real change after a clear is still seen
|
|
}
|
|
|
|
CASE("makeReport builds the Ctrl+Alt+Del chord byte-exactly");
|
|
{
|
|
const uint8_t del = kr::kUsageDelForward;
|
|
const KeyboardReport r =
|
|
kr::makeReport(kr::kModLctrl | kr::kModLalt, &del, 1);
|
|
hexreport(r, h1);
|
|
// 05 = LCtrl|LAlt, 4C = "Delete Forward". This is the exact sequence Windows
|
|
// accepts as the secure attention sequence and logind sees as CAD.
|
|
CHECK_STR(h1, "05 00 4c 00 00 00 00 00");
|
|
}
|
|
|
|
CASE("makeReport rolls over on more than six keys");
|
|
{
|
|
const uint8_t keys[] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
|
|
const KeyboardReport r = kr::makeReport(0, keys, 7);
|
|
CHECK_EQ(r.keys[0], kr::kUsageErrorRollover);
|
|
CHECK_EQ(r.keys[5], kr::kUsageErrorRollover);
|
|
}
|
|
|
|
CASE("reportsEqual distinguishes modifiers and slots");
|
|
{
|
|
KeyboardReport a = {0, 0, {4, 5, 0, 0, 0, 0}};
|
|
KeyboardReport b = a;
|
|
CHECK(kr::reportsEqual(a, b));
|
|
b.modifiers = kr::kModLshift;
|
|
CHECK(!kr::reportsEqual(a, b));
|
|
b = a;
|
|
b.keys[2] = 9;
|
|
CHECK(!kr::reportsEqual(a, b));
|
|
}
|
|
|
|
return report("keystate");
|
|
}
|