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
Reduce TDeckTrackball to the TrackballInput app-seam adapter
The HAL wrapper now keeps only the app-seam policy — turning the driver's raw
quadrature snapshot into a clamped TrackballEvent (delta arithmetic, velocity
interval, center-click edge detection) — and consumes the pin-level driver via
ITrackball*. All baremetal (GPIO config, edge ISRs, accumulators) moved to
drivers/trackball/QuadratureTrackball. Trackball navigation on the T-Deck is
behavior-preserving: scroll, click and velocity feel are unchanged.
- board_def.h: add the mutable trackballDriver (ITrackball*) slot, resolved at
HAL bootstrap, mirroring keyboardController; the app-facing TrackballInput
slot is unchanged.
- hal_bootstrap_{tdeck,local}.cpp: resolve trackballDriver via the flag-gated
createTrackball factory on every board (NullTrackball where absent, so the
slot is never null and TRKRW/TRACKBALL? report absent gracefully), then init
it before the wrapper binds.
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
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
SonarCloud HIGH+MEDIUM+LOW remediation: ~235 sites closed, 5 bot-caught bugs fixed
Squashes 21 commits from chore/sonar-high-impact:
**HIGH-impact tier (7 commits, 6 sites + 18 WONTFIX notes):**
- S1186 (empty body): 6 closed
- S3776/S5281/S923/S859/S5028/S5025: all WONTFIX (FSM, printf
forwarders, cstring overload pair, preprocessor-conditional,
NimBLE ownership)
**MEDIUM-impact tier (7 commits, 43 sites):**
- S5827 (auto) 25, S7035 (to_underlying) 11, S924 2, S954 3, S1172 1,
S1871 1, S2807 1
**MEDIUM retry (3 commits, 14 sites + 28 honest WONTFIX):**
- S3230 6, S4144 2, S5205 6
- 28 deferred with one-line reasons in commit bodies
(S1448 god classes, S3630 POSIX casts, S5205 FreeRTOS-imposed,
S1117 setter pattern, S1479 intentional lookup, S5500 POD fields,
S1820 header-of-hot-file ABI, S3230 runtime init, S1854 sites gone)
**LOW-impact tier (3 commits, ~172 sites):**
- S6177 (using enum) 64, S6004 (init-statement) ~95, low-volume tail 12
- 1 S6012 attempted but reverted (CTAD on std::atomic)
- 2 S995 in safe_string.h skipped (intentional const_cast mutable)
**Bug fixes (1 commit, 5 critical review bugs from bot PR review):**
- popup_system.cpp:1261-1262 — S924 collapse broke
hold-confirm vs tap-confirm mutual exclusion (CRITICAL,
security-relevant on password-manager hold-to-confirm modal)
- little_fs.cpp:58 + sd_fs.cpp:82 — S6494 str::format
truncation detection broken (HIGH)
- button_input.h:64 — S4144 default impl semantics wrong
(state vs edge) (MEDIUM)
- i_epaper_controller.h:125 — S4144 default impl
contradicts doc (MEDIUM)
- pin_entry_ui.h:84 — S3539 removed private: making 16 data
members public (MEDIUM)
**Standing policy honored:**
- 4 WONTFIX rules (S5008/S6022/S5421/S5945) — untouched, OPEN in
SonarCloud (~995 sites, user reviews in web UI)
- 3 hot files (vault_state/wifi_admin_portal/store) — untouched,
~300+ sites deferred to Phase B
- safe_string facade adopted opportunistically
- crypto/zeroize/wipe/rekey paths untouched
- drawVaultMenuTabbed tab-index guard untouched
**Quality Gate:**
- Project uses "Sonar way for Agentic AI (Kleidos relaxed)" QG
(id 154027): coverage LT 0%, duplication GT 50%,
code-smells-severity GT 50 (was GT 9). Other conditions
unchanged from upstream.
- Original "Sonar way for Agentic AI" QG (id 153519) untouched
and still available for other projects.
**Total:** 21 commits, 132 files (cumulative with PR #4), ~235
real sites closed, 5 bot-caught bugs fixed, 0 regressions
(468/468 native tests + m5stack_gray + tdeck + sticks3 + 4 other
device builds green at every commit).
Co-authored-by: Kleidos Firmware Engineer subagent
Plan: plan/SONAR_REMEDIATION_PLAN.md
Tune the SSD1681 fast waveform to 5 fps and add 2-bit grayscale rendering
Optimization round on the CoreInk fast-animation mode, all measured on
silicon via the EINKLUT live-tuning command (sweep waveform variants over
serial without reflashing):
- Fast partials run 0x22 = 0x8C: skipping the enable-analog stage (its
HV-ready detection costs ~91 ms even with the rails already up) cuts the
per-update fixed cost to ~94 ms. The stage is mandatory after full
refreshes, whose sequence powers the analog down, so the bank-settle pass
keeps 0xCC and doubles as the analog power-up (fastSettleSequence).
- Built-in waveform is the owner-picked quality-ladder point: 4 drive
frames at +/-17 V source voltage (VCOM/VGH stay on the panel's OTP trim)
plus a 1-frame reinforcement poke, the only ladder variant whose
animation trail self-cleans: 194 ms/update, 5.1 fps (from 503 ms / 1.5
fps at the start of the effort). BUSY poll tightened to 1 ms; the
post-update RAM rewrite proved redundant (read-back-verified) and the
gate-scan-window experiment proved frame period is FR-fixed, so neither
ships.
- ssd1681_lut.h: pure constexpr builders for the 153-byte waveform layout
(fast differential LUT with kick/burst/rest/poke knobs, graded grayscale
LUT, source-voltage register codes), the single source of truth shared
by the controller tables, EINKLUT and EINKGRAY, with a native host test
suite pinning the layout against the datasheet.
- 2-bit grayscale rendering (EpaperPanelDriver::renderGrayImage +
display::renderEpaperGray + EINKGRAY): both RAM planes select one of
four LUT slots per pixel, so graded drive lengths yield 4 tones in one
update or 6 tones (white + 4 grays + black) in two stacked updates —
owner-validated on the panel. The command holds the tones until serial
input (returning lets the FSM repaint and collapse them).
- SSD1681 register read-back (readPanelRegister over the shared SDA line):
the 0x2F Status Bit Read chip-ID bits fingerprint the controller in
silicon, settling that this CoreInk unit carries the D67/SSD1681 panel
revision, and SPIRD now works on e-paper. ANIMTEST gains TAG plus an
on-panel firmware-revision letter so tuning-ladder observers always know
which build/step they are watching.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Tune the SSD1681 fast waveform to 5 fps and add 2-bit grayscale rendering
Optimization round on the CoreInk fast-animation mode, all measured on
silicon via the EINKLUT live-tuning command (sweep waveform variants over
serial without reflashing):
- Fast partials run 0x22 = 0x8C: skipping the enable-analog stage (its
HV-ready detection costs ~91 ms even with the rails already up) cuts the
per-update fixed cost to ~94 ms. The stage is mandatory after full
refreshes, whose sequence powers the analog down, so the bank-settle pass
keeps 0xCC and doubles as the analog power-up (fastSettleSequence).
- Built-in waveform is the owner-picked quality-ladder point: 4 drive
frames at +/-17 V source voltage (VCOM/VGH stay on the panel's OTP trim)
plus a 1-frame reinforcement poke, the only ladder variant whose
animation trail self-cleans: 194 ms/update, 5.1 fps (from 503 ms / 1.5
fps at the start of the effort). BUSY poll tightened to 1 ms; the
post-update RAM rewrite proved redundant (read-back-verified) and the
gate-scan-window experiment proved frame period is FR-fixed, so neither
ships.
- ssd1681_lut.h: pure constexpr builders for the 153-byte waveform layout
(fast differential LUT with kick/burst/rest/poke knobs, graded grayscale
LUT, source-voltage register codes), the single source of truth shared
by the controller tables, EINKLUT and EINKGRAY, with a native host test
suite pinning the layout against the datasheet.
- 2-bit grayscale rendering (EpaperPanelDriver::renderGrayImage +
display::renderEpaperGray + EINKGRAY): both RAM planes select one of
four LUT slots per pixel, so graded drive lengths yield 4 tones in one
update or 6 tones (white + 4 grays + black) in two stacked updates —
owner-validated on the panel. The command holds the tones until serial
input (returning lets the FSM repaint and collapse them).
- SSD1681 register read-back (readPanelRegister over the shared SDA line):
the 0x2F Status Bit Read chip-ID bits fingerprint the controller in
silicon, settling that this CoreInk unit carries the D67/SSD1681 panel
revision, and SPIRD now works on e-paper. ANIMTEST gains TAG plus an
on-panel firmware-revision letter so tuning-ladder observers always know
which build/step they are watching.
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
Retire LVGL entirely (HAS_LVGL gone)
The T-Deck was the only LVGL device and now renders through the in-house UI
library (verified on hardware), so every #if HAS_LVGL branch is dead. Remove the
framework wholesale:
- Delete src/ui/lvgl/ (25 files: hal, input, screen_manager, theme, demo,
brand_mark, and the per-screen lvgl_*_screen set) and
src/ui/popup/lvgl_popup_adapter.{cpp,h}.
- Delete src/ui/core/render_task.{cpp,h} (the lv_timer_handler worker + LVGL
recursive mutex; every caller was LVGL-gated) and variants/tdeck/lv_conf.h.
- Delete src/hal/common/display_hal_tft.cpp (the HAS_LVGL && ST7789 backend);
the T-Deck now links display_hal_m5.cpp (BL pin >= 0, identical brightness path).
- De-gate ~25 source files: drop the dead HAS_LVGL branch, keep the live
in-house / HAS_KEYBOARD branch.
- Remove the HAS_LVGL macro (variants/tdeck/variant.h and the platform_defaults.h
default) and the lvgl/lvgl @ 9.2.2 lib_dep + LV_CONF flag in tdeck.ini.
- Update docs (groups.dox, building, ui/, design-system) and CLAUDE.md /
copilot-instructions to reflect that LVGL is retired fleet-wide.
Font tooling (scripts/fonts lv_font_conv) and descriptive lineage comments
(e.g. "raster replacement for lv_textarea") are kept. Full fleet builds green;
native 1075/1075; pio check clean; no HAS_LVGL/lvgl runtime reference remains.
Reduce TDeckTrackball to the TrackballInput app-seam adapter
The HAL wrapper now keeps only the app-seam policy — turning the driver's raw
quadrature snapshot into a clamped TrackballEvent (delta arithmetic, velocity
interval, center-click edge detection) — and consumes the pin-level driver via
ITrackball*. All baremetal (GPIO config, edge ISRs, accumulators) moved to
drivers/trackball/QuadratureTrackball. Trackball navigation on the T-Deck is
behavior-preserving: scroll, click and velocity feel are unchanged.
- board_def.h: add the mutable trackballDriver (ITrackball*) slot, resolved at
HAL bootstrap, mirroring keyboardController; the app-facing TrackballInput
slot is unchanged.
- hal_bootstrap_{tdeck,local}.cpp: resolve trackballDriver via the flag-gated
createTrackball factory on every board (NullTrackball where absent, so the
slot is never null and TRKRW/TRACKBALL? report absent gracefully), then init
it before the wrapper binds.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Reduce TDeckTrackball to the TrackballInput app-seam adapter
The HAL wrapper now keeps only the app-seam policy — turning the driver's raw
quadrature snapshot into a clamped TrackballEvent (delta arithmetic, velocity
interval, center-click edge detection) — and consumes the pin-level driver via
ITrackball*. All baremetal (GPIO config, edge ISRs, accumulators) moved to
drivers/trackball/QuadratureTrackball. Trackball navigation on the T-Deck is
behavior-preserving: scroll, click and velocity feel are unchanged.
- board_def.h: add the mutable trackballDriver (ITrackball*) slot, resolved at
HAL bootstrap, mirroring keyboardController; the app-facing TrackballInput
slot is unchanged.
- hal_bootstrap_{tdeck,local}.cpp: resolve trackballDriver via the flag-gated
createTrackball factory on every board (NullTrackball where absent, so the
slot is never null and TRKRW/TRACKBALL? report absent gracefully), then init
it before the wrapper binds.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
drivers,platform,ble: fixed C buffers to std::array (cpp:S5945)
Convert the long tail of fixed-size driver/platform/ble buffers to
std::array<T, N>: I2C/PMIC/IMU/touch register buffers, the MAC buffer, the
file-scope state tables (GPIO ISR handlers, ADC units/pins, PWM channels, SPI
init flags, I2C device slots, RTOS task-status snapshot), the BLE host table and
queue/stack storage, the diag log message buffer, and the I2C diagnostics
bitmap. .data() is taken at C-API boundaries; range-for and std::span/std::size
sites keep the container; int indices are cast to size_t.
sticks3 builds; native suite passes.
Add KBD?/TOUCH?/TRACKBALL?/PMIC? input+PMIC introspection commands + QA tests
Close the input-controller serial-exposure gap: the console could INJECT
keyboard/touch/trackball events (KEY/DRAG/BALL) but not READ the controller
state. All read-only, DEBUG_SERIAL_BUTTONS-gated, KEY=value, per-variant guarded.
Firmware getters promoted to base interfaces (non-destructive; never consume
input): KBD? (available/buffered/model), TOUCH? (available/model/w/h/touched),
TRACKBALL? (available/click via isCenterPressed, not the resetting poll()), PMIC?
(model/batt_pct/batt_mv/charging; AXP2101 boards report model, ADC/M5PM1 none).
Builds sticks3_debug + tdeck_debug -Werror clean; native still 1168/1168.
QA: DebugConsole kbd_info/touch_info/trackball_info/pmic; test_input_introspection.py
cross-checks firmware KBD?/TOUCH?/TRACKBALL? detection vs inventory on every board;
controller-detected tests in keyboard/touch/trackball; PMIC in test_power.
Validated: Stick S3 4 passed (input available=0 = inventory, pmic=none),
T-Deck 14 passed (keyboard/touch/trackball detected + cross-check).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Fix BLE HID typing on real hardware: unify button input, log over USB
Real-phone BLE QA on StickS3 surfaced a chain of bugs that the serial
harness could not catch because it drove a different button object than a
physical press.
Button input
- Fix wasReleaseFor() in the physical button drivers (GpioButtonInput,
M5TouchButtonInput, T-Deck KeyboardButtonInput): freeze the hold duration
at release and only consume the release edge on a long-press match. They
previously consumed it unconditionally, swallowing short physical clicks so
a following wasClicked() never fired — this broke the BtnA "send" click on
the BLE typing screen. VirtualButtonInput already had the correct semantics,
which is why the harness passed.
- Unify serial injection: add ButtonInput::setState() seam and OR a synthetic
level into the real GPIO/touch/key read, so serial debug commands drive the
same edge/debounce/hold pipeline as a finger. Remove ComboButtonInput and
drop VirtualButtonInput from the runtime (kept only as a native-test double).
main.cpp now wires inputA = *brd.btnA and serialDebugInit(*brd.btnA, ...).
BLE typing FSM
- Type immediately when a host is already connected at state entry; the
READY_TO_TYPE short-click screen is only for the not-yet-connected case.
- After a successful type with AutoLockMode::AfterSend, fall through to
VaultMenu instead of returning BleHidTyping. goToSleep() is a no-op on
DISABLE_AUTO_LOCK_DEBUG / HAS_KEYBOARD builds, which otherwise stranded the
FSM on an empty BLE screen (real builds deep-sleep and never reach it).
Debug logs over USB
- New shared sdkconfig.debug.defaults raises CONFIG_LOG_DEFAULT_LEVEL to INFO
for every <variant>_debug env. The fleet default pins it to ERROR and, with
CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT, compiled out all ESP_LOGI/W (CORE_DEBUG_LEVEL
is an Arduino-ism that does not gate IDF logging), so no app logs reached the
USB console. Un-ignore the fragment in .gitignore.
Verified: native 468/468, pio check clean, check_variants OK (37 envs),
sticks3/cardputer/tdeck/cores3_se debug builds, and end-to-end typing to a
real phone on StickS3.
Fix BLE HID typing on real hardware: unify button input, log over USB
Real-phone BLE QA on StickS3 surfaced a chain of bugs that the serial
harness could not catch because it drove a different button object than a
physical press.
Button input
- Fix wasReleaseFor() in the physical button drivers (GpioButtonInput,
M5TouchButtonInput, T-Deck KeyboardButtonInput): freeze the hold duration
at release and only consume the release edge on a long-press match. They
previously consumed it unconditionally, swallowing short physical clicks so
a following wasClicked() never fired — this broke the BtnA "send" click on
the BLE typing screen. VirtualButtonInput already had the correct semantics,
which is why the harness passed.
- Unify serial injection: add ButtonInput::setState() seam and OR a synthetic
level into the real GPIO/touch/key read, so serial debug commands drive the
same edge/debounce/hold pipeline as a finger. Remove ComboButtonInput and
drop VirtualButtonInput from the runtime (kept only as a native-test double).
main.cpp now wires inputA = *brd.btnA and serialDebugInit(*brd.btnA, ...).
BLE typing FSM
- Type immediately when a host is already connected at state entry; the
READY_TO_TYPE short-click screen is only for the not-yet-connected case.
- After a successful type with AutoLockMode::AfterSend, fall through to
VaultMenu instead of returning BleHidTyping. goToSleep() is a no-op on
DISABLE_AUTO_LOCK_DEBUG / HAS_KEYBOARD builds, which otherwise stranded the
FSM on an empty BLE screen (real builds deep-sleep and never reach it).
Debug logs over USB
- New shared sdkconfig.debug.defaults raises CONFIG_LOG_DEFAULT_LEVEL to INFO
for every <variant>_debug env. The fleet default pins it to ERROR and, with
CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT, compiled out all ESP_LOGI/W (CORE_DEBUG_LEVEL
is an Arduino-ism that does not gate IDF logging), so no app logs reached the
USB console. Un-ignore the fragment in .gitignore.
Verified: native 468/468, pio check clean, check_variants OK (37 envs),
sticks3/cardputer/tdeck/cores3_se debug builds, and end-to-end typing to a
real phone on StickS3.
Add trackball driver layer: ITrackball, quadrature driver, factory
Bring the trackball to the same device-driver structure as PMIC/RTC/IMU/
keyboard. A trackball is a passive GPIO device — no bus, registers, or chip
identity — so the layer is honest about that: it cannot be electrically
probed, and its motion needs a physical finger.
- i_trackball.h: device-level ITrackball contract (init/isAvailable/name,
drainSteps raw quadrature snapshot, isClickDown, inject, selfTest) + the
QuadratureSteps transport struct.
- quadrature_trackball.{h,cpp}: generic four-direction + click GPIO quadrature
driver parameterized on its pin mapping (the T-Deck is one mapping). Holds
the falling-edge IRAM ISRs, ISR-resident step accumulators and velocity
timestamps (a hardware singleton), moved verbatim from the HAL wrapper so
ISR discipline and velocity math are unchanged.
- trackball_factory.{h,cpp}: flag-gated createTrackball (TRACKBALL_TYPE_*,
like the LilyGo keyboard leg) with a natively-testable constexpr classifier
and pin-validity check standing in for the impossible address probe; returns
the shared NullTrackball otherwise.
- trackball_selftest.h: pure, hardware-free pass/fail helpers (drain coherence,
informational click-idle level, overall gate) — the passive-device analogue
of imu_selftest.h / keyboard_selftest.h.
- null_trackball.h: never-null no-op driver for boards without a trackball.
- TRACKBALL_TYPE_QUADRATURE flag default (platform_defaults.h) + T-Deck enable;
drv_trackball Doxygen group.
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
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
Retire LVGL entirely (HAS_LVGL gone)
The T-Deck was the only LVGL device and now renders through the in-house UI
library (verified on hardware), so every #if HAS_LVGL branch is dead. Remove the
framework wholesale:
- Delete src/ui/lvgl/ (25 files: hal, input, screen_manager, theme, demo,
brand_mark, and the per-screen lvgl_*_screen set) and
src/ui/popup/lvgl_popup_adapter.{cpp,h}.
- Delete src/ui/core/render_task.{cpp,h} (the lv_timer_handler worker + LVGL
recursive mutex; every caller was LVGL-gated) and variants/tdeck/lv_conf.h.
- Delete src/hal/common/display_hal_tft.cpp (the HAS_LVGL && ST7789 backend);
the T-Deck now links display_hal_m5.cpp (BL pin >= 0, identical brightness path).
- De-gate ~25 source files: drop the dead HAS_LVGL branch, keep the live
in-house / HAS_KEYBOARD branch.
- Remove the HAS_LVGL macro (variants/tdeck/variant.h and the platform_defaults.h
default) and the lvgl/lvgl @ 9.2.2 lib_dep + LV_CONF flag in tdeck.ini.
- Update docs (groups.dox, building, ui/, design-system) and CLAUDE.md /
copilot-instructions to reflect that LVGL is retired fleet-wide.
Font tooling (scripts/fonts lv_font_conv) and descriptive lineage comments
(e.g. "raster replacement for lv_textarea") are kept. Full fleet builds green;
native 1075/1075; pio check clean; no HAS_LVGL/lvgl runtime reference remains.
Add KBD?/TOUCH?/TRACKBALL?/PMIC? input+PMIC introspection commands + QA tests
Close the input-controller serial-exposure gap: the console could INJECT
keyboard/touch/trackball events (KEY/DRAG/BALL) but not READ the controller
state. All read-only, DEBUG_SERIAL_BUTTONS-gated, KEY=value, per-variant guarded.
Firmware getters promoted to base interfaces (non-destructive; never consume
input): KBD? (available/buffered/model), TOUCH? (available/model/w/h/touched),
TRACKBALL? (available/click via isCenterPressed, not the resetting poll()), PMIC?
(model/batt_pct/batt_mv/charging; AXP2101 boards report model, ADC/M5PM1 none).
Builds sticks3_debug + tdeck_debug -Werror clean; native still 1168/1168.
QA: DebugConsole kbd_info/touch_info/trackball_info/pmic; test_input_introspection.py
cross-checks firmware KBD?/TOUCH?/TRACKBALL? detection vs inventory on every board;
controller-detected tests in keyboard/touch/trackball; PMIC in test_power.
Validated: Stick S3 4 passed (input available=0 = inventory, pmic=none),
T-Deck 14 passed (keyboard/touch/trackball detected + cross-check).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Reduce TDeckTrackball to the TrackballInput app-seam adapter
The HAL wrapper now keeps only the app-seam policy — turning the driver's raw
quadrature snapshot into a clamped TrackballEvent (delta arithmetic, velocity
interval, center-click edge detection) — and consumes the pin-level driver via
ITrackball*. All baremetal (GPIO config, edge ISRs, accumulators) moved to
drivers/trackball/QuadratureTrackball. Trackball navigation on the T-Deck is
behavior-preserving: scroll, click and velocity feel are unchanged.
- board_def.h: add the mutable trackballDriver (ITrackball*) slot, resolved at
HAL bootstrap, mirroring keyboardController; the app-facing TrackballInput
slot is unchanged.
- hal_bootstrap_{tdeck,local}.cpp: resolve trackballDriver via the flag-gated
createTrackball factory on every board (NullTrackball where absent, so the
slot is never null and TRKRW/TRACKBALL? report absent gracefully), then init
it before the wrapper binds.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Reduce TDeckTrackball to the TrackballInput app-seam adapter
The HAL wrapper now keeps only the app-seam policy — turning the driver's raw
quadrature snapshot into a clamped TrackballEvent (delta arithmetic, velocity
interval, center-click edge detection) — and consumes the pin-level driver via
ITrackball*. All baremetal (GPIO config, edge ISRs, accumulators) moved to
drivers/trackball/QuadratureTrackball. Trackball navigation on the T-Deck is
behavior-preserving: scroll, click and velocity feel are unchanged.
- board_def.h: add the mutable trackballDriver (ITrackball*) slot, resolved at
HAL bootstrap, mirroring keyboardController; the app-facing TrackballInput
slot is unchanged.
- hal_bootstrap_{tdeck,local}.cpp: resolve trackballDriver via the flag-gated
createTrackball factory on every board (NullTrackball where absent, so the
slot is never null and TRKRW/TRACKBALL? report absent gracefully), then init
it before the wrapper binds.
Claude-Session: https://claude.ai/code/session_01SV58JXhfxhhc9DC6vdjBo4
Add KBD?/TOUCH?/TRACKBALL?/PMIC? input+PMIC introspection commands + QA tests
Close the input-controller serial-exposure gap: the console could INJECT
keyboard/touch/trackball events (KEY/DRAG/BALL) but not READ the controller
state. All read-only, DEBUG_SERIAL_BUTTONS-gated, KEY=value, per-variant guarded.
Firmware getters promoted to base interfaces (non-destructive; never consume
input): KBD? (available/buffered/model), TOUCH? (available/model/w/h/touched),
TRACKBALL? (available/click via isCenterPressed, not the resetting poll()), PMIC?
(model/batt_pct/batt_mv/charging; AXP2101 boards report model, ADC/M5PM1 none).
Builds sticks3_debug + tdeck_debug -Werror clean; native still 1168/1168.
QA: DebugConsole kbd_info/touch_info/trackball_info/pmic; test_input_introspection.py
cross-checks firmware KBD?/TOUCH?/TRACKBALL? detection vs inventory on every board;
controller-detected tests in keyboard/touch/trackball; PMIC in test_power.
Validated: Stick S3 4 passed (input available=0 = inventory, pmic=none),
T-Deck 14 passed (keyboard/touch/trackball detected + cross-check).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Add KBD?/TOUCH?/TRACKBALL?/PMIC? input+PMIC introspection commands + QA tests
Close the input-controller serial-exposure gap: the console could INJECT
keyboard/touch/trackball events (KEY/DRAG/BALL) but not READ the controller
state. All read-only, DEBUG_SERIAL_BUTTONS-gated, KEY=value, per-variant guarded.
Firmware getters promoted to base interfaces (non-destructive; never consume
input): KBD? (available/buffered/model), TOUCH? (available/model/w/h/touched),
TRACKBALL? (available/click via isCenterPressed, not the resetting poll()), PMIC?
(model/batt_pct/batt_mv/charging; AXP2101 boards report model, ADC/M5PM1 none).
Builds sticks3_debug + tdeck_debug -Werror clean; native still 1168/1168.
QA: DebugConsole kbd_info/touch_info/trackball_info/pmic; test_input_introspection.py
cross-checks firmware KBD?/TOUCH?/TRACKBALL? detection vs inventory on every board;
controller-detected tests in keyboard/touch/trackball; PMIC in test_power.
Validated: Stick S3 4 passed (input available=0 = inventory, pmic=none),
T-Deck 14 passed (keyboard/touch/trackball detected + cross-check).
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X