Take the QR matrix off the static DRAM budget
core2_v13_debug could not link: DRAM overflowed by 1904 bytes. The classic
ESP32 has far less internal DRAM than the S3, and the debug build spends
what little headroom is left on diagnostics — so that board had no HIL
firmware at all, and the 320x240 render work could not be verified on it.
The QR matrix is 3249 bytes held statically for a code that is drawn only
in admin mode. It was made static deliberately and for a good reason — it
cannot go on the 8 KB loopTask stack, which it would overflow together
with the encoder's work buffers — but "not on the stack" does not have to
mean "resident forever". Take it from the heap on first use and keep it:
PSRAM where the board has it, internal heap otherwise, and nothing at all
until the first QR is drawn. Allocation failure draws no code rather than
faulting.
Two false leads, recorded so they are not chased again: CMock compiles
into the device build and holds a 32 KB static, but the linker drops it —
were it linked the overflow would be 32 KB, not 1.9 KB. And a full clean
rebuild reproduced the overflow exactly, so it was not a stale build
directory either.
core2_v13_debug now links (RAM 2.7%), which closed the last gap in the
render campaign: the zone3 region-scoped repaint is now verified on glass
on a Core2 v1.3 — cursor steps move only the list, a tab change repaints
title, strip and body, and stepping back leaves no stale band. The QR
itself was re-checked on the admin page after this change.
Claude-Session: https://claude.ai/code/session_01ABhkBJsMKTZAxZh4Vh7jsF
Wire deferred settings rows: destructive actions, auto-lock relock, orphan editors
Surface a batch of settings-model options that were deferred by
isSettingImplemented, each with a working on-device handler (no dead rows),
and wire the auto-lock timer to the FSM.
Destructive actions (catalog §9.4): add ResetSettings (restore every device
preference to its default, keep the vault) and FactoryReset (wipe vault +
preferences + BLE bonds + onboarding -> first-run). A new settings_reset module
holds the single source of truth for which NVS entries are preferences
(kPreferenceKeys) — deliberately excluding vault/onboarding/security/identity
state so Reset Settings keeps the vault usable. Both reuse the hold-to-confirm
popup and restart so every subsystem re-reads its default.
Auto-lock relock (catalog §9.3): wire the auto_lock idle timer to the FSM. The
HomeStateHandler now relocks the vault (radios off + key zeroize -> PIN_ENTRY)
once idle passes autoLockMs, distinct from and below the deep-sleep ceiling.
decideRelock (pure, host-tested) splits the two §9.3 cases: lock face visible
when the screen is still on, silent relock (relockSilent -> PinStateHandler
keeps the panel dark) when the screen already powered off. Gated off keyboard
boards and capture builds; does not regress deep-sleep-locks-on-idle.
Orphan editors: add the AudioFeedback and ShakeToLock toggles and the
ShakeSensitivity stepper to the button-nav tree, reusing the existing
speaker/IMU backends (audio_fb / shake_lock / shake_thr NVS keys).
Also add a scoped NOLINT for a pre-existing cppcoreguidelines-owning-memory
finding on the QR scratch placement-new singleton (unrelated to this change;
keeps the clang-tidy gate green).
Native tests: decideRelock policy (6 cases), Reset-Settings preference scope
(7 cases), and the newly listed rows in the nav-logic suite.
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ
Heap-back the QR encoder scratch to fix core2_v13 DRAM overflow
The base core2_v13 build overflowed dram0_0_seg by 3280 bytes at link
time. core2_v13 (classic ESP32) recently enabled CONFIG_SPIRAM to expose
its 8 MB PSRAM as a heap pool; on the classic ESP32 that auto-selects the
SPIRAM cache workaround, which raises the static-DRAM origin (0x3ffbdb5c)
and shrinks dram0_0_seg to 124580 bytes — ~56 KB less than the no-SPIRAM
budget. The app's (unchanged) ~128 KB of static DRAM no longer fits.
The largest relocatable non-secret static-DRAM consumer in the base image
is the QR encoder's worst-case scratch: seven function-local `static` BSS
arrays (data codewords, interleaved stream, reserved-cell map, bit buffer,
per-block Reed-Solomon buffers) totalling ~11.4 KB. The vault and
app-context instances are larger but hold decrypted credential material
that must stay in internal static DRAM (never external PSRAM), and the BLE
task stacks cannot live in PSRAM — so those are deliberately untouched.
Consolidate the scratch into one QrScratch struct, lazily allocated on the
first encode() via platform::memory::allocLarge (PSRAM on core2_v13,
internal DRAM heap — outside dram0_0_seg — elsewhere) and retained for the
process lifetime, matching the former arrays' permanent lifetime. On the
near-impossible allocation failure encode() returns false, which ui::drawQr
already handles by leaving the QR card blank rather than crashing. The QR
payload is the ephemeral admin-AP join string (SSID + session AP password)
shown on-screen for scanning — not a vault secret — so PSRAM placement is
acceptable. Encoder output is byte-identical; the native known-answer
suite is unchanged.
This frees ~11.4 KB of static DRAM: the base build links with 8132 bytes of
dram0_0_seg headroom, and core2_v13_debug links too. Other variants are
placement-only affected (the scratch moves off static DRAM into the heap).
Claude-Session: https://claude.ai/code/session_01P6BNTpbgrvnZXSJnNuj8ZJ