Skip to content

feat(ui): add a Navigation mode - #229

Merged
PurpleSentinel merged 1 commit into
mainfrom
feat/228-navigation-mode
Oct 20, 2026
Merged

PurpleSentinel merged 1 commit into
mainfrom
feat/228-navigation-mode

Conversation

@PurpleSentinel

Copy link
Copy Markdown
Contributor

Closes #228.

Road speed low and large, the receiver's clock top left, and an arrow top right pointing where the car is going with north at twelve o'clock.

The departure from Speed Only, and what it cost

MPH sits beside the speed rather than under it, because the digit line has moved to the bottom of the panel and there is nothing below it any more.

Speed Only made the opposite call, and its source says why: "beside would mean reserving width that the three-digit case needs, which is exactly the compromise that costs the face its size." That reason still holds, so the cost was paid rather than argued with:

Face Line height
Speed Only 248 px 302 px
Navigation 208 px 253 px

Three digits plus the unit and its gap have to fit 540 px, which leaves 454 px for the digits and caps the face at 208. MPH at montserrat_28 measures 70 px against a 72 px budget, and a three-digit speed then runs x 33..483 with 17 px of clearance to the unit.

Speed Only is unchanged - still 248 px, still with its label underneath. It answers a narrower question with the whole panel to answer it in. A future reader should not reconcile the two.

The arrow

Absent whenever the heading is not trustworthy, with the reason left in the ring where it would have been. The board has no magnetometer, so the direction is course over ground: a parked car has no heading at all - not a stale one, not north.

The gates are Compass mode's, reused rather than restated (headAcc under 15 degrees, with a 0.5 m/s backstop under it). The arrow is five positioned dots that move only when the bearing changes by a whole degree, following CompassScreen rather than rotating artwork - that per-frame work is what starved the UI task in #133 and #141.

One cause stated once

With no receiver all three readouts are absent for the same reason, and printing it beside the speed, under the clock and inside the ring reads as three faults. That one case collapses to a single line.

Every other combination keeps its three separate statuses, because a parked car with a fix and a time and no course really is three different things to say - and the heading is the only one of them the driver cannot work out for themselves.

Settings

The mode is appended to OperatingMode, so this is one byte that already existed changing value: no format bump, no migration, and a device updating to this firmware keeps its settings. The upper bound in valid_settings moves with it, which test_settings now asserts as the enum's last enumerator.

Two things found while building it

The digits first rendered mid-panel rather than low. A label's box is a line box and carries the face's descent, which figures never use, so hanging the box off the bottom margin left a quarter of the face's height as empty panel under the number. Caught by looking at the rendered snapshot. The baseline is placed now and the box hung from it.

That descent comes from arithmetic, not from lv_font_get_line_height. The simulator's countdown_font is a stub that answers every size with one 52 px face, so measuring would have laid the screen out differently in the simulator than on the panel. Every other large numeral here is positioned from the same arithmetic for that reason. The unit does measure, and can, because it is a built-in face that is the same font in both places.

Verification

  • make check, make firmware-container-build, all 78 simulator tests
  • new host test navigation-mode-test: composed readouts, a parked car losing only its arrow, an optimistic headAcc not beating standing still, one cause stated once, and a quiet receiver holding the clock while dropping the rest
  • new simulator-navigation-screen renders the layout and asserts the absent states; simulator-navigation-live-view asserts speed and a three-digit heading together against the real replayed stream
  • the snapshot confirms composition and the absent states but cannot confirm the numeral's size, because the host face is that stub - hence the separate metrics check above

Not yet flashed.

🤖 Generated with Claude Code

Road speed low and large, the receiver's clock top left, and an arrow top
right pointing where the car is going with north at twelve o'clock. Three
readouts the device already computed in three separate modes, on one screen.

Closes #228.

The unit sits beside the speed rather than under it, which is the one place
this screen departs from Speed Only and the reason its face is 208 px against
Speed Only's 248. Three digits plus "MPH" and its gap have to fit 540 px,
which leaves 454 px for the digits and so caps the face at 208. Speed Only
keeps 248 and its label underneath - it answers a narrower question with the
whole panel to answer it in, and is deliberately not changed to match.

The arrow is absent whenever the heading is not trustworthy, with the reason
left in the ring where it would have been. The board has no magnetometer, so
the direction is course over ground and a parked car has no heading at all -
not a stale one, not north. The gates are Compass mode's, reused rather than
restated. The view model composes speed_only_view, clock_view and compass_view
rather than formatting anything itself, so the absent-state rules stay in the
one place that is already tested for them.

One thing none of the three source modes needed: with no receiver all three
readouts are absent for the same reason, and printing it beside the speed,
under the clock and inside the ring reads as three faults. That case is
collapsed to one line. Every other combination keeps its three statuses,
because a parked car with a fix and a time and no course really is three
different things to say.

The mode is appended to OperatingMode, so this is one byte that already
existed changing value: no settings format bump, no migration, and a device
updating to this firmware keeps its settings. The upper bound in
valid_settings moves with it, which test_settings now asserts.

Two things found by building it:

The digits first rendered mid-panel rather than low. A label's box is a line
box and carries the face's descent, which figures never use, so hanging the
box off the bottom margin left a quarter of the face's height as empty panel
under the number. The baseline is placed now and the box hung from it.

That descent is taken from this file's arithmetic rather than from
lv_font_get_line_height, because the simulator's countdown_font is a stub that
answers every size with one 52 px face. Measuring would have laid the screen
out differently in the simulator than on the panel. The unit does measure, and
can, because it is a built-in face that is the same font in both places.

Verified: make check, make firmware-container-build, and all 78 simulator
tests. The rendered snapshot confirms the composition and the absent states;
it cannot confirm the numeral's size, because the host face is that stub -
which is why the measured "MPH" width (70 px against a 72 px budget) and the
resulting three-digit fit were checked against the real metrics separately.

Not yet flashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PurpleSentinel
PurpleSentinel merged commit ccb7339 into main Oct 20, 2026
3 checks passed
@PurpleSentinel
PurpleSentinel deleted the feat/228-navigation-mode branch October 20, 2026 23:09
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.

Navigation mode: speed, GNSS time and direction of travel on one screen

1 participant