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
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
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
Fix all readability-identifier-naming findings and the heal-surfaced defects
Rename every identifier flagged by the .clang-tidy naming policy: file-scope
and function-local statics gain the s_ prefix, static class members of
BleHidManager and AdminPortal move from bare/member-style names to s_ camelBack,
static/constexpr constants take k + PascalCase (kDummies, kNames, kDemos,
kEventNames, kAdvHidUuid, the generated AaFont atlas symbols), snake_case
parameters/locals in secure_allocator become camelBack, the OKLab intermediates
in gradient.cpp get real names, and the mbedtls_aes_context compat alias is
retired in favor of VaultAesContext at every call site. The AaFont symbol
rename is applied in the generator (scripts/fonts/convert_fonts.py) and
mirrored into the generated files. External ABI contracts that cannot be
renamed (app_main, NimBLE C symbols, the __wrap_ linker prefix, the
std-container/std::string API spellings of InplaceVector/InplaceString) are
NOLINT-suppressed with rationale.
Also fix what the healed parse newly surfaced: brace-init the va_list locals
flagged by cppcoreguidelines-init-variables and use auto for the two
static_cast initializers in totp_screen_controller, and rename the s_-prefixed
range-for loop variables in bond_store, name_store_task, and ble_event_bus.
clang-tidy: 239 -> 117 findings; identifier-naming, init-variables, and
use-auto are now at zero.
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
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
docs: restore the Doxygen gate to exit 0 (pages workflow red since the KDF merge)
Doc-comment-only changes, no code touched. `doxygen Doxyfile` (1.17.0, the
version CI pins) has been failing on main with 42 errors, keeping the
GitHub Pages deploy red. Two error classes:
- Unresolvable @ref targets (file-level-block refs to members/forward
declarations, .cpp-internal constants, enum-class enumerators) replaced
with @c in pin_policy.h, export_passphrase.h, secure_allocator.h,
vault_import.h and vault_meta.h.
- Missing @param/@return blocks completed: SecureAllocator::deallocate /
::reallocate, web::adminsd::handleSdVaultImport, web::isCsvTrimChar,
and CsvRow::appendChar / nextField / field.
doxygen Doxyfile now exits 0 with zero warnings and
scripts/check_doc_coverage.py reports full coverage.
Claude-Session: https://claude.ai/code/session_01JW42m5zhzTUT8eapHvSvPB
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 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
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
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