Raise the per-variant capacities, and stop shipping unusable vault UI
Every board now pins the capacity its flash and RAM actually support:
tdeck and cores3_se 700, core2_v13 500, sticks3 and cardputer 250,
m5stickc_plus2 180. The two 4 MB boards stay at 100 — their rekey-safe
ceiling is 139, and staging a second vault for a change-PIN on a fuller
one would run the filesystem out of space.
m5stickc_plus2 could not honestly hold 180: it had 3,132 B of internal
DRAM left against the 4,096 B bar. The slack came from a real absurdity.
Every button board compiled BOTH vault-menu layouts and picked one at
runtime from whether a third button existed, though a board's buttons are
fixed at manufacture. Each unused layout carries a row array sized by
capacity, so plus2 was paying 5 KB for a screen it can never draw. Boards
now declare which layout they use and only that one is compiled: plus2
goes to 9,196 B, sticks3 to 143,141, cores3_se to 78,985. core2_v13
recovers only the view object because its row arrays already live in
PSRAM, which is the placement working as intended.
The declaration is deliberate rather than inferred. Button count looked
like the discriminator only because today's fleet correlates: cardputer
and tdeck are wide-screen boards with no third button, and the old rule
would have handed them the narrow layout if their keyboards ever went
away. What actually decides the layout is width — a sliding pill
indicator is what fits 135 px, while a top tab bar plus an action bar
need room, and a past CoreInk header garble came from that view
hardcoding 320x240. Input decides only how you move between tabs. So the
variant states its choice, with no default, and a build-time tie rejects
a board declaring a layout its hardware cannot drive.
The names were lying too. Both layouts draw tabs, so "Tabbed"
distinguished nothing: they are now NarrowVaultView and WideVaultView,
after the constraint that actually separates them. KeyboardVaultView
keeps its name on purpose — cardputer is 240x135 and tdeck is 320x240,
one narrow and one wide, and both use it, so there the discriminator
really is the input device.
Review of the change caught a T-Deck pin that had never been raised while
five other artifacts already advertised 700, a NAV TOTP macro that
regressed on the wide boards so the screenshot suite filed a vault-list
capture as the TOTP screen, a selector whose "no silent default" promise
had a hole (an unknown token preprocesses to zero, which was the one
value exempted, so a typo compiled no view at all), and a variant guard
whose regex rejected correct declarations that carried a trailing
comment.
Verified: native 2861/2861, all three repo guards, and all eight boards
build with their declared layout confirmed present and its siblings
absent in each map.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Add hardware coverage: buttons(all), keyboard, touch, trackball, IMU, RTC, audio, SD, I2C, display, power, crypto
Broaden HIL coverage from serial/BLE/WiFi/video to every hardware peripheral,
capability-gated so each test spawns only on the boards that have it. No firmware
change needed — the debug console already exposes every command.
Inventory: devices.yaml capabilities now carry imu/rtc/audio/sd/trackball per the
per-variant hardware matrix; config.py adds the matching capability predicates.
DebugConsole: key, ball, drag, beep, imu, rtc_set, spi, disp, brightness, sd,
sdtest, i2c_scan, cryptokat, pbkdf2bench.
New tests:
- Input: test_buttons (1/2/3-button PRESS/RELEASE + tap, beyond two_button),
test_keyboard (KEY), test_touch (DRAG), test_trackball (BALL).
- tests/hardware/: display (SPI?/DISP?/BRIGHT), rtc (RTCSET+RTC?), imu (~1 g),
audio (BEEP), sd (SD?/SDTEST), i2c (scan + expected addrs), power (batt %/mV),
crypto (CRYPTOKAT 6/6 + PBKDF2BENCH).
Markers: capability (trackball/imu/rtc/audio/sd/buttons) + hardware-domain
(display/power/crypto/i2c/onboarding/editor) so you can run/skip a single domain
(-m imu), file, test, or -k substring. Documented in qa-system.md.
Validated: Stick S3 13 passed / 10 capability-skipped; T-Deck 19 passed / 4
skipped. ruff + black clean; no-hardware suite green.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
test(hil): encrypted SD backup/restore + TOTP + change-PIN on-device (P1-02/P2-03)
Add DebugConsole wrappers:
- sd_export() → SdExportResult (captures device-generated passphrase, path, bytes)
- sd_import(passphrase) → raw reply str
- kexp_info() → dict[str,str] (present/bytes/magic_ok)
- add_totp(name, secret, digits, period) → raw reply str
- change_pin(old, new) → raw reply str
All secret parameters are registered via register_secret() and never logged.
New HIL tests:
- qa/tests/hardware/test_sd_backup.py: round-trip export+restore, explicit
passphrase export, wrong-passphrase import (oracle guard), kexp introspection
after export, SD-less board guard (SDEXPORT_FAIL: no SD + present=0).
@pytest.mark.sd auto-skips SD round-trip tests on non-SD boards.
- qa/tests/serial/test_change_pin.py: CHANGEPIN_OK round-trip, session-lock
assertion, old-PIN rejection, new-PIN acceptance, wrong-old-PIN guard,
credentials survive rekey. autouse fixture wipes+reprovisions DEBUG_PIN
before each test and in teardown.
Verified on sticks3 (firmware 1e293d15-dirty, only DUT with commit 72b51ec8
new commands): 4 pass / 4 skip (sd tests auto-skip on non-SD DUT).
No-hardware collect: 8 skipped / EXIT=0.
Remaining SD DUTs (tdeck/cardputer/core2_v13/cores3_se/m5stack_gray) show
firmware 422adc69 (pre-72b51ec8) — SD round-trip tests will pass once
those DUTs are re-flashed with the new debug firmware.
Claude-Session: https://claude.ai/code/session_01SLSU3WoYEyX1J7qWXxmyKo
Add serial transport + DUT control, port serial HIL tests (Phase 1)
Build the DUT-control layer of the QA system and port the first two serial
scripts to pytest, validated on the Stick S3 pilot (MAC 70:04:1d:dc:50:20).
qa_lib DUT layer:
- dut/identify.py: resolve a device's port by MAC. USB-JTAG S3 boards expose the
MAC as the USB serial number, so pyserial matches it directly — an
authoritative check with NO device reset (avoids the Stick's download-mode PMIC
artifact). Bridge boards match a usb_serial hint, then confirm via esptool
read-mac. Aborts on mismatch (cross-flash guard).
- transport/serial_dut.py: pyserial DutTransport with DTR/RTS held low (USB-JTAG
not held in reset) and port-stuck recovery (lsof + kill holders, retry) so a
held port is not mistaken for a disconnect.
- transport/base.py: promote send_expect/drain and add drain_until_idle (wait out
a log burst before querying) so mock and serial transports share the idiom.
- dut/debug_console.py: typed API over the *_debug console (state/where/vault,
tap/hold/nav/goto/arc, unlock with redacted PIN, add_dummy, res, screenshot).
- dut/screenshot.py: RGB565->PNG framebuffer decode ported from
scripts/serial_screenshot.py, operating on the raw serial handle.
- dut/flash.py: MAC-verify-then-upload; refuses any _secure env.
Tests + fixtures:
- Real dut fixture: MAC-verify, open, wait for boot (opening the CDC port resets
the S3), gate on a *_debug console (STATE? reply), yield a DebugConsole,
re-lock on teardown. Autouse fixture attaches each test's redacted serial log
to Allure.
- tests/serial/test_button_input.py + test_fsm_flows.py (HIL, ported from
serial_button_test.py / serial_fsm_test.py) + test_debug_console_mock.py
(no-hardware coverage of the console parsing/redaction).
Gate: 8 HIL tests green on sticks3_debug (MAC-verified), a 135x240 home-screen
screenshot + 8 redacted serial logs attached to Allure, the debug PIN never
leaks (0 occurrences in logs); no-hardware suite stays green; ruff + black clean.
Claude-Session: https://claude.ai/code/session_01Y7Cy1HetRp6TZAUAeekN8X
Make QA multi-DUT: auto-flash, device-agnostic geometry/motion, telemetry evidence
Validated on the T-Deck (--dut tdeck) as a second DUT family (keyboard/touch,
320x240) alongside the Stick S3:
- `--flash`: build + flash <dut>_debug once per session, MAC-verified, refusing
_secure — so `pytest --dut tdeck --run-hardware --flash` compiles, flashes, and
runs everything in one command. flash.py finds pio on PATH or the penv.
- Screenshot assertion no longer hard-codes 135x240 — accepts any plausible
framebuffer (T-Deck/Core are 320x240).
- Video motion is now device-agnostic: capture frames SPACED across full-screen
toggles (PIN entry <-> HOME) instead of a fast burst under a Stick-only ARC
sweep. Verified green on both Stick S3 and T-Deck (the stream does reflect UI
changes; the old burst just sampled a single screen state).
- Telemetry as evidence: the HAL telemetry test now captures and attaches RES
(heap/loop-stack), TASKS (per-task core/prio/stack-free/CPU%), and STACKHWM
(per-task stack high-water) to Allure; add DebugConsole.stackhwm().
T-Deck result: serial (button tests capability-skip — 1-button), HAL/telemetry,
screenshots, smoke, WiFi admin-portal + monitor, and video all green; BLE E3/E4
security green; BLE happy-path pairs (AUTH_OK) but HOGP_FAILs — the Linux HoG
plugin does not create a uhid keystroke node for the T-Deck (a real per-device
finding, not masked).
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
Raise the per-variant capacities, and stop shipping unusable vault UI
Every board now pins the capacity its flash and RAM actually support:
tdeck and cores3_se 700, core2_v13 500, sticks3 and cardputer 250,
m5stickc_plus2 180. The two 4 MB boards stay at 100 — their rekey-safe
ceiling is 139, and staging a second vault for a change-PIN on a fuller
one would run the filesystem out of space.
m5stickc_plus2 could not honestly hold 180: it had 3,132 B of internal
DRAM left against the 4,096 B bar. The slack came from a real absurdity.
Every button board compiled BOTH vault-menu layouts and picked one at
runtime from whether a third button existed, though a board's buttons are
fixed at manufacture. Each unused layout carries a row array sized by
capacity, so plus2 was paying 5 KB for a screen it can never draw. Boards
now declare which layout they use and only that one is compiled: plus2
goes to 9,196 B, sticks3 to 143,141, cores3_se to 78,985. core2_v13
recovers only the view object because its row arrays already live in
PSRAM, which is the placement working as intended.
The declaration is deliberate rather than inferred. Button count looked
like the discriminator only because today's fleet correlates: cardputer
and tdeck are wide-screen boards with no third button, and the old rule
would have handed them the narrow layout if their keyboards ever went
away. What actually decides the layout is width — a sliding pill
indicator is what fits 135 px, while a top tab bar plus an action bar
need room, and a past CoreInk header garble came from that view
hardcoding 320x240. Input decides only how you move between tabs. So the
variant states its choice, with no default, and a build-time tie rejects
a board declaring a layout its hardware cannot drive.
The names were lying too. Both layouts draw tabs, so "Tabbed"
distinguished nothing: they are now NarrowVaultView and WideVaultView,
after the constraint that actually separates them. KeyboardVaultView
keeps its name on purpose — cardputer is 240x135 and tdeck is 320x240,
one narrow and one wide, and both use it, so there the discriminator
really is the input device.
Review of the change caught a T-Deck pin that had never been raised while
five other artifacts already advertised 700, a NAV TOTP macro that
regressed on the wide boards so the screenshot suite filed a vault-list
capture as the TOTP screen, a selector whose "no silent default" promise
had a hole (an unknown token preprocesses to zero, which was the one
value exempted, so a typo compiled no view at all), and a variant guard
whose regex rejected correct declarations that carried a trailing
comment.
Verified: native 2861/2861, all three repo guards, and all eight boards
build with their declared layout confirmed present and its siblings
absent in each map.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Raise the per-variant capacities, and stop shipping unusable vault UI
Every board now pins the capacity its flash and RAM actually support:
tdeck and cores3_se 700, core2_v13 500, sticks3 and cardputer 250,
m5stickc_plus2 180. The two 4 MB boards stay at 100 — their rekey-safe
ceiling is 139, and staging a second vault for a change-PIN on a fuller
one would run the filesystem out of space.
m5stickc_plus2 could not honestly hold 180: it had 3,132 B of internal
DRAM left against the 4,096 B bar. The slack came from a real absurdity.
Every button board compiled BOTH vault-menu layouts and picked one at
runtime from whether a third button existed, though a board's buttons are
fixed at manufacture. Each unused layout carries a row array sized by
capacity, so plus2 was paying 5 KB for a screen it can never draw. Boards
now declare which layout they use and only that one is compiled: plus2
goes to 9,196 B, sticks3 to 143,141, cores3_se to 78,985. core2_v13
recovers only the view object because its row arrays already live in
PSRAM, which is the placement working as intended.
The declaration is deliberate rather than inferred. Button count looked
like the discriminator only because today's fleet correlates: cardputer
and tdeck are wide-screen boards with no third button, and the old rule
would have handed them the narrow layout if their keyboards ever went
away. What actually decides the layout is width — a sliding pill
indicator is what fits 135 px, while a top tab bar plus an action bar
need room, and a past CoreInk header garble came from that view
hardcoding 320x240. Input decides only how you move between tabs. So the
variant states its choice, with no default, and a build-time tie rejects
a board declaring a layout its hardware cannot drive.
The names were lying too. Both layouts draw tabs, so "Tabbed"
distinguished nothing: they are now NarrowVaultView and WideVaultView,
after the constraint that actually separates them. KeyboardVaultView
keeps its name on purpose — cardputer is 240x135 and tdeck is 320x240,
one narrow and one wide, and both use it, so there the discriminator
really is the input device.
Review of the change caught a T-Deck pin that had never been raised while
five other artifacts already advertised 700, a NAV TOTP macro that
regressed on the wide boards so the screenshot suite filed a vault-list
capture as the TOTP screen, a selector whose "no silent default" promise
had a hole (an unknown token preprocesses to zero, which was the one
value exempted, so a typo compiled no view at all), and a variant guard
whose regex rejected correct declarations that carried a trailing
comment.
Verified: native 2861/2861, all three repo guards, and all eight boards
build with their declared layout confirmed present and its siblings
absent in each map.
Claude-Session: https://claude.ai/code/session_01Q2J5gQSFMTDLVzPUYog51r
Add screenshots, HAL smoke, cross-device smoke fan-out (Phase 2)
Port the screenshot and HAL-smoke scripts to pytest, add telemetry evidence,
and wire the `--dut all` fan-out with capability auto-skip. Validated across the
connected fleet.
- debug_console.py: HAL diagnostics (hal/rtc/buttons/press/release/wifi_status/
ble_status), RES/TASKS telemetry, and a key=value field parser.
- config.py: capability-marker predicates + device_satisfies() so a test auto-
skips on a DUT lacking the hardware it needs.
- conftest: pytest_generate_tests parametrizes `dut` over `--dut <name>` or
`--dut all` (fleet, filtered by the test's capability markers); a makereport
hook + dut-fixture teardown attach a screenshot + RES/TASKS to Allure on
failure. The debug-ready gate now requires TWO consecutive STATE? replies so a
slow board (Gray over CP210x) is stable before it is driven.
- identify.py: bridge boards resolve by their STABLE per-unit USB-bridge serial
(no disruptive esptool reset for driving); esptool MAC confirmation moves to a
flash-time confirm_mac() (flash.py), and an unreadable MAC now raises
IdentityError (skip), not MacMismatchError (abort).
- tests/serial/: test_screenshots.py (PIN + MOCKVAULT preview, no provisioning),
test_hal_smoke.py (non-interactive diagnostics + virtual buttons), test_smoke.py
(tiny cross-device liveness + a keyboard-gated test).
Gate: 17 HIL tests green on sticks3 (incl. 2 screenshots attached); `--dut all
-m smoke` = 14 passed across 6 debug boards (sticks3/cardputer/tdeck/cores3_se/
gray/core2_v13), 6 skipped (plus2/plus1/coreink are not *_debug builds), the
keyboard test parametrizes only Cardputer + T-Deck and skips the Stick with a
clear reason; no-hardware suite green; ruff + black clean.
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
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