Skip to content

feat(ui): add a 0-X mode timing a standing start to a target speed - #208

Merged
PurpleSentinel merged 1 commit into
mainfrom
feat/207-acceleration
Oct 16, 2026
Merged

PurpleSentinel merged 1 commit into
mainfrom
feat/207-acceleration

Conversation

@PurpleSentinel

Copy link
Copy Markdown
Contributor

Closes #207.

Counts up from 0.00, stops the instant the target is reached, turns green. Coming back to
rest re-arms it; a tap resets it sooner. Target chosen in SETUP → 0-X TARGET.

Zero is not zero

A stationary receiver reports a few centimetres per second of noise, not 0.000 — so the
instant the car began moving cannot be read off a sample.

The start is estimated by extrapolating back along the acceleration between the first
two moving fixes to where speed would have been zero:

t_zero = t1 - v1 * (t2 - t1) / (v2 - v1)

The finish is interpolated between the two fixes straddling the target. At 25 Hz a fix
lands every 40 ms, so without both ends every reading is quantised to 40 ms and the second
decimal is decoration.

The test drives a synthetic 4 m/s² run: 6.71 s measured against a true 6.7056 — well
inside one fix interval, which is the whole point.

A stalled run must not look like a finished one

When the fixes dry up the number stops climbing, which is exactly what arriving at the
target looks like. So it is its own state: LOST FIX, never green.

Green belongs to a completed run alone, and is applied to the decimal point too — a green
number with a white point reads as a glitch rather than a result.

State Shows
no receiver / no fix --.-- with which
no speed --.--, "NO SPEED" — the fix is fine, the receiver just isn't sure enough of its speed
armed 0.00, "READY"
running the climbing time
complete the frozen time, green, "DONE"
abandoned the time it reached, "LOST FIX", never green

armed showing a literal 0.00 is the one place on this device a zero is the right answer
rather than a placeholder: the car genuinely has not moved.

X configurable cost a format bump

Appending an OperatingMode changes no stored value's meaning. Adding a field changes
the length of a fixed-width record, so the settings format goes to version 8 with a
migration
.

A v7 device keeps every setting it had and takes the default target — it never had an
opinion about one, and a migrated record should not be read as a device that chose zero.
There is a test for exactly that.

Setup's entry opens the target's choices directly rather than descending through twelve
fields to reach the one it is named after. Its index into the picker array is
static_asserted rather than trusted: a hand-written index that stopped matching its array
is what made DIAGNOSTICS unreachable in #158.

Layout

12.34 is four digits and a point, and the numeral face carries . among its fourteen
glyphs — so the whole reading is in the large face, at metrics identical to Clock's
HH:MM: 536 px of 540 at 170 px, the countdown's cached size. Two leading digits covers
every real measurement and no unreal one.

The point never moves, which keeps the reading stable as it crosses ten seconds.

Verification

12 host cases in test_acceleration.cpp: both interpolations against hand-worked numbers,
a full run, the time holding once passed, re-arm on returning to rest, tap reset, a slower
target finishing sooner, the speed-accuracy gate, and that an abandoned run never renders
as a finished one. Plus v8 round-trip and v7 migration coverage in test_settings.cpp.

make check passes. Simulator 76/76. Firmware builds at 0xfda30.

Not verified: an actual run. That needs a car and a straight, and the acceptance
criterion — checked against an independent timing source — can only be met there.

Not in scope

  • Rollout correction, the one-foot allowance drag strips use. A different measurement with
    its own conventions, and it should not be silently mixed in
  • Logging runs to the card or surfacing them in Review
  • IMU-assisted start. The accelerometer would mark the launch instant far more precisely
    than GNSS can, and the device already has a launch threshold — but it is a second sensor
    and a fusion problem, better once the GNSS-only error is known rather than guessed

🤖 Generated with Claude Code

Counts up from 0.00, stops the instant the target is reached, turns green. Coming back to
rest re-arms it; a tap resets it sooner. The target is chosen in SETUP -> 0-X TARGET.

This is an instrument rather than a readout, and two things follow from that.

**Zero is not zero.** A stationary receiver reports a few centimetres per second of noise,
so the instant the car began moving cannot be read off a sample. The start is estimated by
extrapolating back along the acceleration between the first two moving fixes to where speed
would have been zero; the finish is interpolated between the two fixes that straddle the
target. At 25 Hz a fix lands every 40 ms, so without both ends interpolated every reading
would be quantised to 40 ms and the second decimal would be decoration. The test drives a
4 m/s^2 run and measures 6.71 s against a true 6.7056.

**A run that stopped being measured must not look like one that finished.** When the fixes
dry up the number stops climbing, which is exactly what arriving at the target looks like.
So that is its own state: LOST FIX, never green. Green belongs to a completed run alone,
and is applied to the decimal point as well, because a green number with a white point
reads as a glitch rather than a result.

The armed state shows a literal 0.00. That is the one place on this device where a zero is
the right answer rather than a placeholder - the car genuinely has not moved - and it is
what the request asked for.

X is configurable, and that cost more than a mode does. Appending an OperatingMode changes
no stored value's meaning; adding a field changes the length of a fixed-width record, so the
settings format goes to version 8 with a migration. A v7 device keeps every setting it had
and takes the default target, because it never had an opinion about one and a migrated
record should not be read as a device that chose zero.

Setup's entry opens the target's choices directly rather than descending through twelve
fields to reach the one it is named after. Its index into the picker array is static_assert
ed rather than trusted: a hand-written count that stopped matching its array is what made
DIAGNOSTICS unreachable in #158.

Closes #207.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PurpleSentinel
PurpleSentinel merged commit 9ce4fb8 into main Oct 16, 2026
3 checks passed
@PurpleSentinel
PurpleSentinel deleted the feat/207-acceleration branch October 16, 2026 18:38
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.

Add a 0-X mode: time a standing start to a target speed

1 participant