Offload admin backup/restore KDF to an App-CPU worker (OPT-008)
The /api/backup and /api/restore envelope KDF (600k PBKDF2 iterations,
~4.9 s at 240 MHz) ran inline on the Core-0 httpd task, stalling the
WiFi/lwIP stack that shares Core 0 for the whole derivation and
disassociating the admin station mid-backup.
Add a dedicated crypto worker pinned to APP_CPU (Core 1, priority 4),
used only in ADMIN_MODE where the UI core is idle. The backup/restore
handlers marshal the whole wrapExport()/unwrapExport() call onto the
worker and the httpd task blocks on a task notification, freeing Core 0
for WiFi while PBKDF2 runs on Core 1. The unlock path (crypto_task on
Core 0, pin_state) is untouched. When the worker is not running (native
tests, spawn failure) the job runs inline, preserving pre-offload
behavior. The worker widens the task watchdog and drops idle-core
monitoring around the bounded derivation, mirroring crypto_task.
Lifecycle is tied to AdminPortal::begin()/end(); end() is on the
deep-sleep teardown path, so no worker survives into deep sleep.
Verified on tdeck (ESP32-S3): PBKDF2BENCH 175k=8.137 us/iter,
600k=8.132 us/iter (baseline 8.137, within 0.06%); CRYPTOKAT PASS 6/6;
TASKS in ADMIN_MODE shows admincrypto on core=1 and kleidos-crypto still
on core=0. Native 1721/1721, clang-tidy 0, doxygen exit 0.
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
Polish Allure, add no-hardware layer coverage + docs (Phase 5)
- Drop the unused pytest-embedded dependency. It was never imported — SerialDut
(pyserial) reimplements MAC->port, flashing, expect, and multi-DUT itself (the
plan's documented pyserial fallback). Removing it cuts Allure containers from
~47/test to ~2.6/test and eliminates the record_xml_attribute warning; keep
esptool explicit (bridge-board MAC confirm). Refresh docstrings + constraints.
- Allure: an autouse fixture tags each test with a feature (its layer) + story
(its name) so the dashboard groups by BLE / WiFi / Serial / Video / Smoke /
Native. scripts/allure_report.sh already carries history/trends forward.
- No-hardware layer coverage: tests/video/test_video_decode.py (synthetic KVID
frames: raw + RLE decode, motion score, GIF) and tests/wifi/test_wifi_parse.py,
so every layer has a mock-mode test (32 passed no-hardware).
- Docs: docs/testing/qa-system.md (operator guide — install, run, markers,
Allure, non-negotiables, topology gotchas); CLAUDE.md §2/§4/§11 point QA at qa/.
Gate: pytest -m "not needs_hardware" green with no devices; Allure results carry
per-layer feature labels + evidence; ruff + black clean.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add WiFi admin-portal + live-video capture (Phase 4)
Extend the host layer to WiFi and add DUT-serial live-video capture, validated
on sticks3 (+ mastodon for the WiFi suites).
- host/linux_ble.py: extract the shared invoke_suite() runner (env overrides).
- host/linux_wifi.py: LinuxWifiHarness wraps run.py's wifi-normal / wifi-attack;
the SoftAP PSK is registered as a secret.
- dut/video.py: KVID /stream decoder (raw RGB565-LE + RLE) ported from
scripts/video_qa.py — record_stream (stdlib http.client, no requests dep),
motion_score, frames_to_gif_bytes. Never logs WiFi credentials.
- debug_console.py: VIDEO staging (video_wifi [redacted password + registered],
video_start/status/stop). evidence.py: attach_gif.
- conftest: wifi_host fixture (SSH + WiFi-NIC preflight); admin_dut fixture
(unlock -> ADMIN_MODE with a known WIFIAPTEST password, reads the SoftAP SSID);
the dut fixture now attaches a screenshot + RES/TASKS to Allure on failure.
- tests: tests/video/test_video_capture.py (join WiFi, record /stream, verify
motion via a serial-driven ARC animation, attach the GIF), tests/wifi/
test_admin_portal.py + test_wifi_security.py.
Gate: video /stream capture green — DUT joins WiFi, 20 frames decoded, motion
verified, a 135x240 GIF attached to Allure; WiFi monitor security green on
mastodon; the admin-portal test brings the DUT SoftAP up but skips cleanly when
the host NIC is out of RF range of the DUT (topology, not a failure). WiFi PSK /
password never leak; no-hardware suite green (26 passed); ruff + black clean.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
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