Initial import: keyrelay, a WiFi-controlled USB HID keyboard
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
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
// 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");
|
||||
}
|
||||
Reference in New Issue
Block a user