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
124 lines
6.3 KiB
C++
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
|