Skip to content

Unified status/onboarding/error screen - #24

Merged
sven97 merged 13 commits into
mainfrom
unified-status-screen
Aug 19, 2026
Merged

sven97 merged 13 commits into
mainfrom
unified-status-screen

Conversation

@sven97

@sven97 sven97 commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces three visually unrelated screens (KEY1 status page, Wi-Fi onboarding, error page) with one shared renderer and one content-fitting engine, ported from a browser-validated design prototype (docs/superpowers/specs/2026-08-13-status-page-atlas.html).

  • New pure-logic font-ladder sizing engine + content-reduction cascade (src/logic/layout_math.h rewrite, src/logic/status_content.h new) — host-testable, no Arduino dependency
  • Pixelarticons-derived 1-bit bitmap icons (src/icons.h, new) replacing the old vector-drawn icons
  • One shared drawFrameScreen() renderer (src/ui.cpp) driving all three screens as thin wrappers (drawStatusScreen(), showProvisioningScreen(), showError())
  • Forced-portrait rendering via an RAII PortraitScope guard, restoring the configured orientation afterward

Spec & plan

  • Design: docs/superpowers/specs/2026-08-18-unified-status-screen-design.md
  • Implementation plan: docs/superpowers/plans/2026-08-18-unified-status-screen.md

Verification

  • Native tests: 85/85 passing across 10 suites (pio test -e native), including 23 new cases covering font-ladder selection, shrink priority, QR scannability floor, and the reduction cascade
  • Build: all 4 board envs (ee02/ee03/ee04/ee05) build successfully
  • Real hardware (EE02, the only board physically available): flashed and visually confirmed — status screen renders correctly (header, stat icons+values, QR+scan text, legend with keycaps); PortraitScope's force-then-restore was independently verified with rotation set to landscape (status screen stayed portrait; the panel's reported dimensions flipped back to landscape immediately afterward)
  • EE03/EE04/EE05: build-verified only — no hardware available to flash or visually check, stated plainly rather than implied otherwise

Process

Built via superpowers:subagent-driven-development — one implementer + task reviewer per plan task, plus a final whole-branch review that caught and fixed two real issues before merge:

  • An invisible separator character (outside every vendored font's glyph range) that would have rendered as a gap on the default screen
  • A typographic-hierarchy edge case on 480×800 panels where stat/chrome text could render larger than the title

Both were fixed and re-reviewed clean.

Claude-Session: https://claude.ai/code/session_01TV93uMPitQ8sUgVSw5WmL5

sven97 added 13 commits August 18, 2026 21:21
Adds the design spec for replacing ui.cpp's drawStatusScreen(),
net.cpp's showProvisioningScreen(), and display.cpp's showError()
with one shared renderer and content-fitting engine, plus the
browser-based atlas (docs/superpowers/specs/2026-08-13-status-page-
atlas.html) it was validated against across all 21 EE02-EE05 panel
variants before this port began.

No firmware behavior changes yet -- design only.
Task-by-task port of the validated atlas design into ui.cpp/net.cpp/
display.cpp: font-ladder sizing engine + reduction cascade (pure logic,
host-testable), pixelarticons-derived bitmap icons, one shared
drawFrameScreen() renderer, forced-portrait via a PortraitScope RAII
guard. Verification split per the design spec: real native tests for
the pure logic (all boards run it), build check on all 4 board envs,
real flash+visual check on EE02 only (the only board physically
available).

Claude-Session: https://claude.ai/code/session_01TV93uMPitQ8sUgVSw5WmL5
…ation

Task 3 of the unified-status-screen plan. Adds src/logic/status_content.h
(host-testable, no Arduino deps) on top of Task 2's layout_math.h:

- ScreenState/LineKind/StatIcon enums, ContentLine/ScreenContent structs
- buildLines(): assembles a screen's content lines (header, stats,
  caption/scan/url, legend) under a given reduction level's ContentConfig
- fitScreen(): top-level orchestration -- walks the reduction cascade,
  fits title/stat/chrome sizes with icon-column reserve, runs the
  height-aware shrink pass, and returns a ScreenFit ready for rendering

test/test_status_content/main.cpp covers content assembly per state
(Normal/Onboarding/Error), legend/detail reduction, and fitScreen
termination/QR-floor behavior on both generous and tiny panels.

Claude-Session: https://claude.ai/code/session_01TV93uMPitQ8sUgVSw5WmL5
…pper

Replaces the old LayoutMetrics/currentLayout()-based status screen with
drawFrameScreen(), the shared unified-frame renderer for the
status/onboarding/error screens (header/status/action/legend zones sized
by fitScreen()'s font-ladder engine). gatherLiveContent() gathers the
battery/Wi-Fi/next-fetch fields shared by the status and error screens.

drawStatusScreen() now calls epaper.update() itself instead of leaving it
to the caller; main.cpp's runStatusMode() is updated to match (no more
double update()).

net.cpp is still on the old broken API at this point -- pio run -e ee02
fails only for reasons in net.cpp now, not ui.cpp/main.cpp.
Replaces the LayoutMetrics/currentLayout()-based onboarding screen with
a thin wrapper around drawFrameScreen(ScreenState::Onboarding, ...):
battery read fresh (no NVS metadata yet before a connection), Wi-Fi/
next-fetch left at "--" per the design spec's per-state field table.

Also drops the stale "layout.h" include (deleted in the layout_math
font-ladder rewrite, Task 2) which was the last thing keeping net.cpp
off the removed API and blocking the board build.

pio run -e ee02 -e ee03 -e ee04 -e ee05: all 4 SUCCESS -- first fully
green board build since Task 2. pio test -e native: 84/84 passed.
Every GFXFF font on this firmware's ladder only covers 0x20-0x7E; U+00B7
(middle dot) falls outside that range and TFT_eSPI's drawString() silently
skips it, rendering "98% \xC2\xB7 4.17V" as "98%  4.17V" (dot missing, double
space) on real hardware. Replace all 7 occurrences (3 stat-detail lines,
2 each in the Normal/Error combined-legend strings) with " - ", which
every font on the ladder -- including classic Font1/GLCD -- can render.

Update test_buildLines_stat_detail_dropped_at_level3 to assert against
the new " - " separator instead of the old UTF-8 byte sequence.
Finding 5: guard the statItemsRough/statItems array fills with explicit
bounds (statCount < 3, i < 3) matching the chromeItems[9] loop's existing
chromeCount < 9 guard a few lines below -- defense-in-depth even though
buildLines() never currently emits more than 3 stat lines.

Finding 7: remove display.h's now-dead #include "logic/wifi_strength.h";
its only consumer (drawWifiIcon()) was removed in an earlier task and
nothing else in display.h/display.cpp references WifiStrength (confirmed
via grep). battery_curve.h stays -- batteryColorForLevel() still needs
BatteryLevel.
…finding 2)

fitScreen() picked each role's size independently via fitSize() based on
width alone, with no cross-role ordering constraint. On 480x800 portrait
panels (EE04/EE05-shaped), a short stat value or short URL could pick a
large ladder rung while the long title string got forced to a small rung
by the narrow row -- producing stat/chrome text visibly larger than the
title. This violated the design spec's Sizing engine intent: "hierarchy
is a rule to hold, not an accident of which role happened to be biggest."

Add a clamp right after the width-only fit computes sizes = RoleSizes{...}
(before the height-aware shrink-pass block): stat is capped to title,
chrome to (the post-clamp) stat. Clamping only ever shrinks a role -- it
can never grow one past what fitSize already proved fits its own width --
so it cannot reintroduce a width overflow.

Clamped via a new clampToLadder() helper (searches the role's own ladder
for the largest rung <= the cap via ladderFor()) rather than a raw
numeric min(): TITLE_SIZES/STAT_SIZES/CHROME_SIZES don't share identical
rungs, and fontFor() in ui.cpp switches on exact rung values (56/42/29/
22/16/8), falling through to its default: case for anything else.

New test: test_fitScreen_hierarchy_holds_on_480x800_panel, parameterized
over all three ScreenStates at 480x800, asserting chrome <= stat <=
title. Verified this test fails without the clamp (temporarily reverted
it to confirm) before restoring the fix.

See the final fix report for the reserve-recompute and ladder-clamp
reasoning: .superpowers/sdd/2026-08-18-unified-status-screen/final-fix-report.md
@sven97
sven97 merged commit 91a2b00 into main Aug 19, 2026
5 checks passed
@sven97
sven97 deleted the unified-status-screen branch August 19, 2026 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant