Add the WiFi-station store and NTP clock-sync backend
From Settings (vault unlocked) the device can join a saved WiFi network
as a station, sync the clock via one-shot SNTP against trusted public
NTP servers, and always disconnect. Servers are LITERAL IPv4 only —
never a hostname, never DNS (esp_sntp is fed parsed addresses; a strict
dotted-quad validator gates every entry). Defaults are Cloudflare
(162.159.200.1, 162.159.200.123), Google (216.239.35.0) and NIST
(132.163.97.1), tried in order; the user can persist an override list
of up to four IPs so dead defaults never require a firmware update.
Saved networks (SSID+PSK, max 8) and the NTP server list live in a new
wifi.bin vault module using the standard envelope (AES-256-CBC +
HMAC-SHA256, encrypt-then-MAC, verify-before-decrypt, type-bound), only
readable with the vault unlocked, staged through the crash-safe rekey,
and crypto-erased by wipeAll. PSKs are zeroized after use and never
logged or echoed.
The sync engine runs on a Core-0 worker: BLE off, join the strongest
saved network, query servers in order, sanity-clamp the epoch and never
step backward past an armed lockout, commit via the rtc facade with a
new display-only src=ntp tag — clock TRUST is never elevated (NTP is
unauthenticated; the forward-jump lockout fail-closed model is the
spoofing defense) — then a RAII station guard tears WiFi down and
restores BLE on every exit path. The station facade stays a generic
transport for future consumers (on-device OTA).
Debug console gains WIFISCAN / WIFIADD / WIFILIST / WIFIDEL, NTPSYNC
(GPSSYNC-style blocking flow with machine-checkable teardown acks) and
NTPSERVERS? / NTPSERVERSET / NTPSERVERCLEAR — all DEBUG_SERIAL_BUTTONS
gated. HIL-verified end to end on the StickS3: real sync against the
default servers, user-list precedence across reboot, TEST-NET failure
paths, trust unchanged, BLE restored, store left clean.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add the WiFi-station store and NTP clock-sync backend
From Settings (vault unlocked) the device can join a saved WiFi network
as a station, sync the clock via one-shot SNTP against trusted public
NTP servers, and always disconnect. Servers are LITERAL IPv4 only —
never a hostname, never DNS (esp_sntp is fed parsed addresses; a strict
dotted-quad validator gates every entry). Defaults are Cloudflare
(162.159.200.1, 162.159.200.123), Google (216.239.35.0) and NIST
(132.163.97.1), tried in order; the user can persist an override list
of up to four IPs so dead defaults never require a firmware update.
Saved networks (SSID+PSK, max 8) and the NTP server list live in a new
wifi.bin vault module using the standard envelope (AES-256-CBC +
HMAC-SHA256, encrypt-then-MAC, verify-before-decrypt, type-bound), only
readable with the vault unlocked, staged through the crash-safe rekey,
and crypto-erased by wipeAll. PSKs are zeroized after use and never
logged or echoed.
The sync engine runs on a Core-0 worker: BLE off, join the strongest
saved network, query servers in order, sanity-clamp the epoch and never
step backward past an armed lockout, commit via the rtc facade with a
new display-only src=ntp tag — clock TRUST is never elevated (NTP is
unauthenticated; the forward-jump lockout fail-closed model is the
spoofing defense) — then a RAII station guard tears WiFi down and
restores BLE on every exit path. The station facade stays a generic
transport for future consumers (on-device OTA).
Debug console gains WIFISCAN / WIFIADD / WIFILIST / WIFIDEL, NTPSYNC
(GPSSYNC-style blocking flow with machine-checkable teardown acks) and
NTPSERVERS? / NTPSERVERSET / NTPSERVERCLEAR — all DEBUG_SERIAL_BUTTONS
gated. HIL-verified end to end on the StickS3: real sync against the
default servers, user-list precedence across reboot, TEST-NET failure
paths, trust unchanged, BLE restored, store left clean.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Remediate WiFi audit findings, add resource telemetry and PMIC current
Implement the WiFi subsystem defensive-audit remediations (dossier in
docs/research/wifi-audit-dossier.md) plus a resource-consumption monitor and
per-chip power telemetry.
Security (audit findings):
- OTA: real semver anti-downgrade (running vs candidate partition), rollback
enabled on shipping open/_release builds, size/BLE-state guards.
- Lifecycle: keyboard variants run the real goToSleep() teardown+wipe (LIFEC-001);
exit() always tears down the portal (LIFEC-003); loop task subscribed to the
TWDT (LIFEC-004); symmetric BLE/WiFi start guard (LIFEC-002); master key wiped
before the handover reboot (LIFEC-008); on-device idle warning + panel dimming.
- HTTP: DNS-rebinding Host guard with captive-portal exemption (HTTP-003, incl.
the 403 reason-phrase fix found on hardware); idle-based session TTL (HTTP-001);
sliding-window rate limiter (HTTP-006, native test); /stream token no longer
logged (HTTP-005).
- Radio: H2E-only SAE, TX-power cap, stack wifi_config_t scrub, WPA2 fail-closed,
bootloader-random AP password, DHCP-DNS offer.
- Web UI: stored-XSS removal via safe DOM builders, autocomplete controls, CSP
meta, deterministic gzip; SPA gz regenerated.
- Optimization: nothrow HTTP body alloc, std::move export responses, CPU boost
around the export KDF, ~1 Hz AP snapshot poll, early never-connected AP shutdown.
Telemetry / consumption:
- Per-tick UI-loop latency gauge (loop_dt_us / loop_max_us) via diag helpers.
- RES? extended with heap fragmentation, SoC die temperature, battery mV/%/current
and VBUS (USB) mV/current.
- PowerManager / IPmic gain optional current + VBUS readouts; implemented for the
AXP192 (validated on a Plus1: batt 4.1 V, VBUS ~5.0 V, ~81 mA USB draw) and
"n/a" on the M5PM1 / AXP2101 fleet. See docs/research/pmic-consumption-telemetry.md.
QA harness (mastodon SSH rig):
- portal_checks.py adds Host-guard / security-header / idle plumbing / Core-0
stress-flood checks; wifi_net.sh requests SAE H2E; run.py forwards the stress
env; new pytest tests for admin lifecycle, WiFi stress-resource monitoring and
AXP192 current.
Verified: native 1472/1472; -Werror builds for sticks3, cardputer, sticks3_debug
and m5stickc_plus1_debug; clang-format, check_variants and check_conditionals
clean; HIL passes on sticks3 (via mastodon) and the AXP192 current path on the
Plus1. Known limitation in plan/plus1-softap-nomem.md: the admin SoftAP hits
ESP_ERR_NO_MEM on the no-PSRAM Plus1 (pre-existing DRAM exhaustion).
Claude-Session: https://claude.ai/code/session_01NYGJirRyAokBs9idYmGLxi
SonarCloud maintainability remediation: ~500 sites closed, 0 regressions, 2 bugs caught and fixed
Squashes 74 commits from chore/sonar-remediation implementing plan/SONAR_REMEDIATION_PLAN.md:
**Phase A** (mechanical, repo-wide, all 🟢):
- S1659 (17), S1905 (11), S1481 (3), S7127 (17), S7040 (10), S7035 (30),
S5827 (22), S6177 (52)
**Phase C** (review-each, repo-wide, excluding hot files):
- S6494 (snprintf → kleidos::platform::str::format, 96)
- S6004 (init-statement scope, 64)
- S1186 (empty body, 12)
- S1066 (merge collapsible if, 11)
- Plus S924/S886/S6020/S5952/S5350/S3230/S1172/S3624/S108/S954
opportunistically.
**Phase D** (structural, outside hot files):
- S3776 cognitive complexity (8/8 viable — complete)
- S134 deep nesting (7/7 viable — complete)
- S3358 nested ternary (8 committed; 22 remaining sites were stale
data, will auto-close on next scan)
**S5817 round** (3 parallel worktree agents, batched):
- 50 sites closed (drivers + HAL + Platform/FSM/UI cascades)
- 19 documented as WONTFIX inline (IPmic::readPowerButtonClick IRQ-clear,
File::available/read syscalls, AppContext::configureDFS, genuine
const-violation in HAS_BUZZER audio backend, etc.)
**Bug fixes (caught by Gemini + Copilot PR review):**
- ble_hid_manager.cpp: S134 refactor lost standby-mode re-advertise
behavior. Restored with the early-return re-ordered to the
over-budget case only.
- graphics.h: docstring claimed callers can pass `const Surface&` but
the 7 free helpers + drawString + drawGlyph still took `Surface&`.
All 9 free fns now take `const Surface&`. Closes 6 cpp:S995 sites.
**WONTFIX policy (4 rules, 995 sites, intentionally untouched):**
- cpp:S5008 (void*), cpp:S6022 (std::byte), cpp:S5421 (globals),
cpp:S5945 (C arrays). User reviews them in the SonarCloud web UI
with per-site justifications provided in plan/WONTFIX_*.md.
**Quality Gate:** Project now uses a project-local copy
'Sonar way for Agentic AI (Kleidos relaxed)' with coverage LT 0% and
duplication GT 50% (per user instruction to relax these for now).
**Deferred to Phase B (3 hot files, ~770 issues):**
vault_state.cpp, wifi_admin_portal.cpp, vault_store.cpp. Recon
plans at plan/PHASE_B_{VAULT_STATE,WIFI_ADMIN_PORTAL,VAULT_STORE}.md.
**Total:** 135 files changed, ~500 sites closed, 0 regressions
(468/468 native tests green throughout), 2 critical bugs caught and
fixed by bot review, 6 bot review comments acknowledged.
Co-authored-by: Kleidos Firmware Engineer subagent
Plan: plan/SONAR_REMEDIATION_PLAN.md
Add the WiFi-station store and NTP clock-sync backend
From Settings (vault unlocked) the device can join a saved WiFi network
as a station, sync the clock via one-shot SNTP against trusted public
NTP servers, and always disconnect. Servers are LITERAL IPv4 only —
never a hostname, never DNS (esp_sntp is fed parsed addresses; a strict
dotted-quad validator gates every entry). Defaults are Cloudflare
(162.159.200.1, 162.159.200.123), Google (216.239.35.0) and NIST
(132.163.97.1), tried in order; the user can persist an override list
of up to four IPs so dead defaults never require a firmware update.
Saved networks (SSID+PSK, max 8) and the NTP server list live in a new
wifi.bin vault module using the standard envelope (AES-256-CBC +
HMAC-SHA256, encrypt-then-MAC, verify-before-decrypt, type-bound), only
readable with the vault unlocked, staged through the crash-safe rekey,
and crypto-erased by wipeAll. PSKs are zeroized after use and never
logged or echoed.
The sync engine runs on a Core-0 worker: BLE off, join the strongest
saved network, query servers in order, sanity-clamp the epoch and never
step backward past an armed lockout, commit via the rtc facade with a
new display-only src=ntp tag — clock TRUST is never elevated (NTP is
unauthenticated; the forward-jump lockout fail-closed model is the
spoofing defense) — then a RAII station guard tears WiFi down and
restores BLE on every exit path. The station facade stays a generic
transport for future consumers (on-device OTA).
Debug console gains WIFISCAN / WIFIADD / WIFILIST / WIFIDEL, NTPSYNC
(GPSSYNC-style blocking flow with machine-checkable teardown acks) and
NTPSERVERS? / NTPSERVERSET / NTPSERVERCLEAR — all DEBUG_SERIAL_BUTTONS
gated. HIL-verified end to end on the StickS3: real sync against the
default servers, user-list precedence across reboot, TEST-NET failure
paths, trust unchanged, BLE restored, store left clean.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add the WiFi-station store and NTP clock-sync backend
From Settings (vault unlocked) the device can join a saved WiFi network
as a station, sync the clock via one-shot SNTP against trusted public
NTP servers, and always disconnect. Servers are LITERAL IPv4 only —
never a hostname, never DNS (esp_sntp is fed parsed addresses; a strict
dotted-quad validator gates every entry). Defaults are Cloudflare
(162.159.200.1, 162.159.200.123), Google (216.239.35.0) and NIST
(132.163.97.1), tried in order; the user can persist an override list
of up to four IPs so dead defaults never require a firmware update.
Saved networks (SSID+PSK, max 8) and the NTP server list live in a new
wifi.bin vault module using the standard envelope (AES-256-CBC +
HMAC-SHA256, encrypt-then-MAC, verify-before-decrypt, type-bound), only
readable with the vault unlocked, staged through the crash-safe rekey,
and crypto-erased by wipeAll. PSKs are zeroized after use and never
logged or echoed.
The sync engine runs on a Core-0 worker: BLE off, join the strongest
saved network, query servers in order, sanity-clamp the epoch and never
step backward past an armed lockout, commit via the rtc facade with a
new display-only src=ntp tag — clock TRUST is never elevated (NTP is
unauthenticated; the forward-jump lockout fail-closed model is the
spoofing defense) — then a RAII station guard tears WiFi down and
restores BLE on every exit path. The station facade stays a generic
transport for future consumers (on-device OTA).
Debug console gains WIFISCAN / WIFIADD / WIFILIST / WIFIDEL, NTPSYNC
(GPSSYNC-style blocking flow with machine-checkable teardown acks) and
NTPSERVERS? / NTPSERVERSET / NTPSERVERCLEAR — all DEBUG_SERIAL_BUTTONS
gated. HIL-verified end to end on the StickS3: real sync against the
default servers, user-list precedence across reboot, TEST-NET failure
paths, trust unchanged, BLE restored, store left clean.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add RLE compression and one-command recorder to WiFi video QA
Compress each RGB565 /stream frame with an own lossless run-length codec
(fmt=2, [u16 count][u16 value] tokens) before sending it over WiFi, falling
back to raw (fmt=1) whenever RLE would be larger so a frame never regresses.
On a typical Kleidos UI frame (flat menu surfaces) this cuts bytes/frame to
roughly 7-31% of raw, raising the effective fps the 2.4 GHz link can sustain.
- src/web/rle_codec.h: host-testable encode/decode, bit-exact wire format.
- src/web/video_stream.cpp: encode on the Core 0 snapshot path; raw fallback.
- src/web/video_frame_header.h: add kVideoFormatRgb565Rle.
- test/web/test_rle_codec: native round-trip + malformed-stream tests.
Make scripts/video_qa.py a one-command recorder: --serial reads WiFi creds
from a gitignored .env (password redacted), drives VIDEO WIFI/START, polls
VIDEO? until serving=1, captures the IP, and records. Default output is a
scrubbable MP4 at the real captured fps; decodes RLE frames; and prints a
baked-in frame-diff motion report (REAL MOTION vs MOSTLY STATIC). Manual
--host/--url mode is kept.
Docs: document the .env one-command workflow, the RLE format, MP4 output,
motion verification, and the on-HW gotchas.
Add debug-only WiFi live-video UI capture for QA
Stream the live shadow framebuffer over WiFi so QA can record the device
UI in motion (scroll tweens, tab-indicator slide, PIN arc, popup
transitions) that static screenshots cannot show. Entirely behind
DEBUG_SERIAL_BUTTONS; zero cost in release.
Firmware:
- platform/wifi_station: new STA facade (counterpart to the SoftAP
facade) that joins an existing network, waits for a DHCP lease, and
tears down via the shared wifi::stopAndDeinit path.
- platform/http_server: add sendResponseChunk() over httpd_resp_send_chunk
for streaming responses.
- web/video_frame_header.h: pure, host-testable length-framed RGB565
frame header (KVID magic, little-endian fields).
- web/video_stream: VIDEO serial commands (WIFI/START/STOP/?) and a
GET /stream endpoint that pushes the framebuffer (read via
display::readRect) throttled to fps. The httpd worker is pinned to the
services core (Core 0) so the UI core stays smooth. Full shutdown path;
also torn down before deep sleep.
Security: WiFi SSID/password are runtime-only secrets passed as serial
command arguments; never hardcoded, persisted, or logged. The password
is redacted (only pass_len printed) and the local copy is secure-wiped.
Host tool: scripts/video_qa.py connects to /stream, decodes RGB565, and
records MP4/GIF (imageio/Pillow) or a PPM frame sequence, with optional
live preview, scaling, duration/frame limits, and reconnect handling.
Tests/docs: native unit test for the frame-header encode/decode;
docs/testing/video-qa-capture.md runbook plus serial-debug-commands and
platform-abstraction updates.