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
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
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
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
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
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
Style: group-open/close consistency (matching multi-line vs triple-slash)
Round 2 of the Doxygen group-marker pass (post b0dd0b0).
The previous round converted 60 /// @} -> /** @} */ but had a state-
machine bug: it converted closings whose openings were still triple-slash
(/// @name X + /// @{), leaving them mismatched. It also missed the
inverse case: /// @} closings whose openings were multi-line
(/** @name X + * @{\n */).
This pass fixes both directions using a stack-based state machine that
walks the source top-to-bottom:
- Push a frame on each @{ opening, tagged with the opening comment
style (triple-slash or multi-line) and a 'force triple-slash' flag
(true for @addtogroup / @defgroup openings).
- On each @} close, pop the top frame and rewrite the close to match
the expected style. @addtogroup / @defgroup closings are always
triple-slash, per the project rule.
Changes:
- 60 /// @} reverted to /** @} */ (round-1 over-conversions)
- 43 /// @} converted to /** @} */ (missed multi-line openings)
---
103 conversions across 29 files; idempotent on second run.
Scope: src/**/*.{h,cpp,hpp} minus brands/generated/, strings_gen*,
index_html_gz.h, and tools/. No function body, signature, include, or
Doxygen block content was touched. doxygen Doxyfile still exits 0.
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
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
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
Round 3 compliance: src/ble/ + HAL/platform incidentally touched
Agent 48 (Round 3 BLE) was killed by a token-limit error mid-pass but had
already applied 287 insertions / 72 deletions across 20 files. This commit
captures that work. The changes are all docs-only:
- src/ble/: @brief / @param[in|out] / @retval / @sa additions on the BLE facade
and internal helpers (ble_commands.h, ble_events.h, bond_store.h, name_store.h,
connection_manager.h, i_hid_sink.h, numeric_comparison.h, ble_command_queue.h,
ble_stack_task.h, nimble_host.h)
- src/hal/: display_hal.h, shake_detector.h, spi_panel_driver.h — parallel
compliance pass during the BLE agent's incidental class-headers review
- src/platform/: i2c.h, i2s.h, pwm.h, rtos.h, sleep.h, spi.h, system.h — same
Verified doxygen Doxyfile exits 0 with no new errors / warnings.
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Style: group-open/close consistency (matching multi-line vs triple-slash)
Round 2 of the Doxygen group-marker pass (post b0dd0b0).
The previous round converted 60 /// @} -> /** @} */ but had a state-
machine bug: it converted closings whose openings were still triple-slash
(/// @name X + /// @{), leaving them mismatched. It also missed the
inverse case: /// @} closings whose openings were multi-line
(/** @name X + * @{\n */).
This pass fixes both directions using a stack-based state machine that
walks the source top-to-bottom:
- Push a frame on each @{ opening, tagged with the opening comment
style (triple-slash or multi-line) and a 'force triple-slash' flag
(true for @addtogroup / @defgroup openings).
- On each @} close, pop the top frame and rewrite the close to match
the expected style. @addtogroup / @defgroup closings are always
triple-slash, per the project rule.
Changes:
- 60 /// @} reverted to /** @} */ (round-1 over-conversions)
- 43 /// @} converted to /** @} */ (missed multi-line openings)
---
103 conversions across 29 files; idempotent on second run.
Scope: src/**/*.{h,cpp,hpp} minus brands/generated/, strings_gen*,
index_html_gz.h, and tools/. No function body, signature, include, or
Doxygen block content was touched. doxygen Doxyfile still exits 0.
Style: group-open/close consistency (matching multi-line vs triple-slash)
Round 2 of the Doxygen group-marker pass (post b0dd0b0).
The previous round converted 60 /// @} -> /** @} */ but had a state-
machine bug: it converted closings whose openings were still triple-slash
(/// @name X + /// @{), leaving them mismatched. It also missed the
inverse case: /// @} closings whose openings were multi-line
(/** @name X + * @{\n */).
This pass fixes both directions using a stack-based state machine that
walks the source top-to-bottom:
- Push a frame on each @{ opening, tagged with the opening comment
style (triple-slash or multi-line) and a 'force triple-slash' flag
(true for @addtogroup / @defgroup openings).
- On each @} close, pop the top frame and rewrite the close to match
the expected style. @addtogroup / @defgroup closings are always
triple-slash, per the project rule.
Changes:
- 60 /// @} reverted to /** @} */ (round-1 over-conversions)
- 43 /// @} converted to /** @} */ (missed multi-line openings)
---
103 conversions across 29 files; idempotent on second run.
Scope: src/**/*.{h,cpp,hpp} minus brands/generated/, strings_gen*,
index_html_gz.h, and tools/. No function body, signature, include, or
Doxygen block content was touched. doxygen Doxyfile still exits 0.
Style: group-open/close consistency (matching multi-line vs triple-slash)
Round 2 of the Doxygen group-marker pass (post b0dd0b0).
The previous round converted 60 /// @} -> /** @} */ but had a state-
machine bug: it converted closings whose openings were still triple-slash
(/// @name X + /// @{), leaving them mismatched. It also missed the
inverse case: /// @} closings whose openings were multi-line
(/** @name X + * @{\n */).
This pass fixes both directions using a stack-based state machine that
walks the source top-to-bottom:
- Push a frame on each @{ opening, tagged with the opening comment
style (triple-slash or multi-line) and a 'force triple-slash' flag
(true for @addtogroup / @defgroup openings).
- On each @} close, pop the top frame and rewrite the close to match
the expected style. @addtogroup / @defgroup closings are always
triple-slash, per the project rule.
Changes:
- 60 /// @} reverted to /** @} */ (round-1 over-conversions)
- 43 /// @} converted to /** @} */ (missed multi-line openings)
---
103 conversions across 29 files; idempotent on second run.
Scope: src/**/*.{h,cpp,hpp} minus brands/generated/, strings_gen*,
index_html_gz.h, and tools/. No function body, signature, include, or
Doxygen block content was touched. doxygen Doxyfile still exits 0.
Style: group-open/close consistency (matching multi-line vs triple-slash)
Round 2 of the Doxygen group-marker pass (post b0dd0b0).
The previous round converted 60 /// @} -> /** @} */ but had a state-
machine bug: it converted closings whose openings were still triple-slash
(/// @name X + /// @{), leaving them mismatched. It also missed the
inverse case: /// @} closings whose openings were multi-line
(/** @name X + * @{\n */).
This pass fixes both directions using a stack-based state machine that
walks the source top-to-bottom:
- Push a frame on each @{ opening, tagged with the opening comment
style (triple-slash or multi-line) and a 'force triple-slash' flag
(true for @addtogroup / @defgroup openings).
- On each @} close, pop the top frame and rewrite the close to match
the expected style. @addtogroup / @defgroup closings are always
triple-slash, per the project rule.
Changes:
- 60 /// @} reverted to /** @} */ (round-1 over-conversions)
- 43 /// @} converted to /** @} */ (missed multi-line openings)
---
103 conversions across 29 files; idempotent on second run.
Scope: src/**/*.{h,cpp,hpp} minus brands/generated/, strings_gen*,
index_html_gz.h, and tools/. No function body, signature, include, or
Doxygen block content was touched. doxygen Doxyfile still exits 0.
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Style: group-open/close consistency (matching multi-line vs triple-slash)
Round 2 of the Doxygen group-marker pass (post b0dd0b0).
The previous round converted 60 /// @} -> /** @} */ but had a state-
machine bug: it converted closings whose openings were still triple-slash
(/// @name X + /// @{), leaving them mismatched. It also missed the
inverse case: /// @} closings whose openings were multi-line
(/** @name X + * @{\n */).
This pass fixes both directions using a stack-based state machine that
walks the source top-to-bottom:
- Push a frame on each @{ opening, tagged with the opening comment
style (triple-slash or multi-line) and a 'force triple-slash' flag
(true for @addtogroup / @defgroup openings).
- On each @} close, pop the top frame and rewrite the close to match
the expected style. @addtogroup / @defgroup closings are always
triple-slash, per the project rule.
Changes:
- 60 /// @} reverted to /** @} */ (round-1 over-conversions)
- 43 /// @} converted to /** @} */ (missed multi-line openings)
---
103 conversions across 29 files; idempotent on second run.
Scope: src/**/*.{h,cpp,hpp} minus brands/generated/, strings_gen*,
index_html_gz.h, and tools/. No function body, signature, include, or
Doxygen block content was touched. doxygen Doxyfile still exits 0.
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
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add SPI panel register readback and SPIRD console command
Add a genuine ST7789 controller register read over the display SPI bus,
surface it through the display HAL, and expose it as the SPIRD <cmd> [len]
serial-debug command.
The driver had no real SPI read path: readRect() serves pixels from the RAM
shadow framebuffer, not a MISO transaction, so there was no bus preamble to
factor. The Stick S3 panel also has no MISO but is 3-wire SPI (data returns on
the shared SDA line in half-duplex), so read-back needs a half-duplex read, not
the full-duplex MISO path.
- spi::readWithDummy(): read-only half-duplex primitive using
spi_transaction_ext_t + SPI_TRANS_VARIABLE_DUMMY (length 0, rxlength len,
dummy_bits N). Splitting the command write and data read into two transactions
sidesteps ESP-IDF's "no dummy bits with both phases" limit and inserts exactly
the ST7789's one dummy clock before multi-byte reads.
- SpiPanelDriver::readPanelRegister(): gated on a read-back path
(miso >= 0 || spi3Wire), reads at BusSpec::readHz (16 MHz vs the 80 MHz write
clock). Two devices on one chip-select make ESP-IDF overwrite the write
device's CS GPIO routing and wedge the panel; since the read runs in the
Core-1 loop task (sole owner of the display SPI), it briefly retires the write
device, reads on a read-clock device that solely owns CS, then restores the
write device. Factored a shared deviceConfig() builder used by begin() too.
- display::readPanelRegister() in the HAL: TFT delegates to the driver; the
e-paper backend returns false (write-only, no panel readback).
- Console SPIRD <cmd> [len] (len default 1, cap 16), Match::Prefix, no collision
with SPI?. Output SPIRD cmd=0x..: 0x.. ...; SPIRD_ERR on no-readback / usage.
No SPIWR by design: panel init already flows through the driver and a raw
panel-command writer adds brick risk with no diagnostic value; raw register
writes are covered on the I2C side (REGW/I2CWR).
On-device (bench Stick S3, sticks3_debug), no GPIO CS-conflict warning:
SPIRD 0x04 3 -> SPIRD cmd=0x04: 0x81 0x81 0x81 (RDDID, repeats)
SPIRD 0x09 4 -> SPIRD cmd=0x09: 0x84 0x84 0x53 0x24 (RDDST, repeats)
SPIRD 0x0B 1 -> SPIRD cmd=0x0b: 0x08 (RDDMADCTL: BGR bit, matches init write)
SPIRD 0x0A 1 -> SPIRD cmd=0x0a: 0x9c (RDDPM: booster/awake/display-on)
DISP? -> ready=1 readback=0xF800 (write path intact after reads)
MADCTL/PM readbacks match the values the driver programs, proving genuine,
dummy-aligned controller reads; the panel and bus stay healthy afterwards.
Claude-Session: https://claude.ai/code/session_01Ciw3Ea5Bb2YAcmuZ1dywRY
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add "GPS time sync" settings action and progress overlay (HAS_GPS boards)
Settings -> Device gains a one-shot "GPS time sync" row on HAS_GPS boards
(DEVICE reaches exactly kSettingsMaxItems there; GPS-less boards keep the
4-item layout with RESTART last). Selecting it opens GpsSyncController, a
thin overlay over the gps:: sync engine: it polls syncPoll, refreshes the
elapsed/timeout countdown once per second, and closes on fix (success
toast), timeout acknowledgement, or cancel (B-long on button boards, Esc
on keyboard boards). Every UI exit path — including overlay idle-sleep and
the parent reset — funnels through reset(), whose syncCancel powers the
receiver down. The trailing settings value surfaces the last clock-set
provenance (ClockSource).
Long-press edges are polled before short-click polls on the same button,
matching the documented ButtonInput call order.
Carried polish: the UART facade begin() now releases the TX/RX pads on its
partial-failure branch, syncCancel documents the Core-1 owning-task
contract, and the stale SyncPhase::Idle doc phrase is reworded.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add 11 system-monitor debug commands (NVS/heap/clock/chip/rng/temp/secboot/audio/part/wdt/font)
Maximize observability — each command monitors a subsystem, so a QA test failing
here flags a firmware regression to review. All read-only, DEBUG_SERIAL_BUTTONS-
gated, KEY=value; every raw esp_*/heap_caps_*/nvs_*/partition/chip/secure-boot/
temp call is wrapped in a small src/platform/ facade (abstraction-first).
Firmware: 11 commands (NVS?/HEAP?/CLOCK?/CHIP?/RNG?/TEMP?/SECBOOT?/AUDIO?/PART?/
WDT?/FONT?) + platform facades (system heap/clock/partitions, security_fuses,
nvs stats, temperature [new, SOC-gated], watchdog, speaker codec name, text
renderer atlas stats). Both sticks3_debug (S3) + m5stack_gray_debug (classic
ESP32) -Werror clean; native 1168/1168.
QA: console methods + tests/hardware/test_diagnostics.py (one monitor per command,
incl. a secure-boot-off safety assertion + running-partition check); diagnostics
marker. identify.confirm_mac now tolerates an unreadable MAC on a bridge board
(retry then proceed on the unique USB-bridge serial; abort only on a real
mismatch) — fixes Gray/CP210x --flash.
Validated: Stick S3 11 passed, Gray classic ESP32 11 passed.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix branch-clone, rounding, cast, and remaining long-tail findings
- bugprone-branch-clone (12): fold genuinely identical switch arms
(typing status map, PM1 power levels, wake-cause names, glyph
fallback), merge the duplicated small-panel atlas ladder, rebuild
scaleFor as per-panel constexpr scale tables (lookup-table idiom),
restructure the modal confirm poll around explicit hold/press
predicates (also clears the DeMorgan finding), and NOLINT the one
per-SoC gated ADC calibration switch with rationale.
- bugprone-casting-through-void (13): use direct reinterpret_cast (the
documented pro-type-reinterpret-cast deviation) for byte/sockaddr
views; keep the SPI slot two-step through void* with rationale (it
exists to satisfy -Werror=cast-align next to the static_asserts).
- bugprone-incorrect-roundings (9): std::lroundf instead of +0.5 casts.
- cert-dcl50-cpp (5): rationale NOLINTs on the printf-style formatters
(a parameter pack cannot produce the va_list vsnprintf needs).
- cppcoreguidelines-pro-type-const-cast: const-correct the WordReader
ctx (API + trampoline + tests, const_cast deleted); NOLINT the two
std <cstring>-convention mutable overloads.
- Long tail: explicit void* casts + sizeof(T) rationale in rtos_queue,
range-for loop conversions, 0x80-mask instead of int8_t reinterpret
for the AXP2101 gauge, uint8_t slot subscript, parenthesized -1 pin
macros, extended the __wrap_log_printf ABI suppression to the
reserved-identifier aliases, integer-center tween targets.
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
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