Files
keyrelay/src/config.h
T
ducoterra ece0313d02 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
2026-11-08 23:27:42 -05:00

124 lines
6.3 KiB
C++

/**
* keyrelay -- build-time defaults.
*
* Everything here is a DEFAULT. The values stored in NVS win at runtime, so the
* network and the control passphrase can be changed from the serial console
* without reflashing (`wifi`, `ap`, `token` -- see README "First bring-up").
*
* Nothing secret belongs in this file. The default passphrase is deliberately
* weak and loudly announced as such on the serial console and in the web page,
* because a keyboard that anyone on the LAN can arm is an open door.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
namespace kr {
constexpr const char kFirmwareName[] = "keyrelay";
constexpr const char kFirmwareVersion[] = "1.0.0";
// ---------------------------------------------------------------------------
// Network
// ---------------------------------------------------------------------------
// Control plane only: the keystrokes travel WiFi -> board -> USB. The target
// machine provides nothing but 5 V and a USB port, and never sees a network.
constexpr uint16_t kHttpPort = 80;
constexpr char kHostname[] = "keyrelay"; // http://keyrelay.local
// Fallback access point, started whenever the station interface is not up, so
// the board is reachable with no infrastructure at all (air-gapped target).
constexpr char kApSsid[] = "keyrelay-ctl";
constexpr char kApPass[] = "keyrelay-ctl-pass"; // >= 8 chars, WPA2. CHANGE IT.
constexpr char kApGw[] = "192.168.4.1";
// Build-time station credentials (optional, still only a default). Override with
// pio run -e bridge -D KR_STA_SSID='"MyNet"' -D KR_STA_PASS='"hunter2"'
// without ever committing them.
#ifndef KR_STA_SSID
#define KR_STA_SSID ""
#endif
#ifndef KR_STA_PASS
#define KR_STA_PASS ""
#endif
constexpr uint32_t kStaTimeoutMs = 8000; // give the association attempt this long at boot
constexpr uint32_t kStaRetryMs = 20000; // ... then retry in the background at this rate
// ---------------------------------------------------------------------------
// Control passphrase
// ---------------------------------------------------------------------------
// Checked on every WebSocket connection and on /status. The page cannot arm the
// bridge without it, and nothing reaches the target until the bridge is armed.
constexpr char kDefaultToken[] = "changeme";
constexpr size_t kTokenMax = 32;
constexpr uint8_t kTokenMaxFails = 5; // then stop evaluating the token ...
constexpr uint32_t kTokenLockMs = 30000; // ... for this long
// ---------------------------------------------------------------------------
// Keystroke path
// ---------------------------------------------------------------------------
constexpr size_t kMaxKeysPerMessage = 8; // protocol cap (HID carries 6)
constexpr uint32_t kCadHoldMs = 60; // how long Ctrl+Alt+Del is held down
constexpr uint32_t kChordHoldMs = 40; // ditto for the other special chords
constexpr uint32_t kMinReportGapUs = 1000; // 1 kHz floor: the host polls at 1 kHz
constexpr size_t kReportQueueDepth = 48; // bursts from a fast typist
// ---------------------------------------------------------------------------
// Pasted text
// ---------------------------------------------------------------------------
// `P=<ms>:<hex>` (src/text.h) types a run of characters that a human pasted and
// then deliberately sent. The board owns the character map and the pacing: a
// controller's clock, arriving over WiFi in whatever order the network delivers
// it, is not a keyboard timing source. The controller asks for a pace; the board
// keeps it.
//
// One controller frame is still one command, so a frame carries at most
// kTextFrameMax characters and a long paste arrives as several frames. The board
// holds whatever it has been given and says so in `typing` in the status frame,
// which is how a controller knows it may send the next piece.
constexpr size_t kTextFrameMax = 120; // characters per P= frame
constexpr size_t kTextJobCap = 2048; // characters held for one paste job
constexpr uint16_t kTextPeriodMsDefault = 12; // ms per character when asked for 0
constexpr uint16_t kTextPeriodMaxMs = 250; // slowest pace a controller may ask for
constexpr uint16_t kTextHoldMaxMs = 6; // how long one character is held down
// ---------------------------------------------------------------------------
// Mouse
// ---------------------------------------------------------------------------
// The controller captures the pointer with the browser's own Pointer Lock and
// forwards it as relative HID mouse reports. One M= frame is one report: the button
// byte is restated every frame, and a movement delta is self-contained, so a frame
// that never arrives moves the cursor a little less and strands nothing. See mouse.h.
//
// The deltas are capped at one report's worth (a signed byte) because one frame is
// one command: splitting a large movement into several frames is the controller's
// job, exactly like splitting a long paste. The board does not split -- it would turn
// one command into a queue depth nobody asked for.
constexpr int8_t kMouseDeltaMax = 127; // one signed byte, the report's range
constexpr uint8_t kMouseButtonMask = 0x1F; // 5 buttons: all the report can carry
constexpr size_t kMouseReportQueueDepth = 32; // movement bursts, same fail-safe
// Safety: a bridge that has heard nothing from its controller for this long is
// a bridge that should not be typing. Arming never survives it (laptop slept,
// cable unplugged, browser killed).
constexpr uint32_t kNoClientTimeoutMs = 8000;
// Status heartbeat to the controller, and the interval of the held-key watchdog.
constexpr uint32_t kStatusIntervalMs = 1000;
// ---------------------------------------------------------------------------
// Control-server buffers
// ---------------------------------------------------------------------------
// The status JSON, sized for every field in StatusSnapshot plus headroom. Sized in
// one place because three call sites build it and a truncated status lies about state,
// which is worse than no status.
constexpr size_t kStatusJsonCap = 384;
constexpr size_t kHttpHeaderCap = 1536; // reject a request line bigger than this
constexpr size_t kWsMessageCap = 256; // one controller frame is a command, not a payload
constexpr uint32_t kAuthTimeoutMs = 5000; // a new socket must authenticate this fast
} // namespace kr