Next turn, then-turn, remaining distance and ETA #33

Closed
opened 2026-08-31 17:14:48 +02:00 by robert · 1 comment
robert commented 2026-08-31 17:14:48 +02:00 (Migrated from git.butzei.de)

Goal

The payoff of owning the cue sheet: information no live third-party relay could give us.

Acceptance criteria

  • Distance to the next cue computed along the route, not straight line
  • The cue after next exposed as NAV_THEN_TURN
  • NAV_REMAIN_M is remaining route distance
  • NAV_ETA_S derived from a rolling average speed, and stable rather than jittery
  • Route completion detected and NAV_STATE set to finished
  • Updates coalesced to about 1 Hz

Files

  • companion/.../route/nav/NavEngine.kt
## Goal The payoff of owning the cue sheet: information no live third-party relay could give us. ## Acceptance criteria - [ ] Distance to the next cue computed along the route, not straight line - [ ] The cue after next exposed as `NAV_THEN_TURN` - [ ] `NAV_REMAIN_M` is remaining route distance - [ ] `NAV_ETA_S` derived from a rolling average speed, and stable rather than jittery - [ ] Route completion detected and `NAV_STATE` set to finished - [ ] Updates coalesced to about 1 Hz ## Files - `companion/.../route/nav/NavEngine.kt`
Owner

Closed by PR #119 (area/nav-engine). New NavEngine/NavUpdate/NextCue/RollingSpeedAverage in companion/core/.../route/nav/NavEngine.kt, consuming #31's RouteSnap and the route's own Cue list directly — no new position tracking or distance-along-route computation, everything falls out of the axis those two already share.

  • Distance to next cue, along the route: cue.distanceAlongRouteMeters - riderSnap.distanceAlongRouteMeters — never straight-line.
  • NAV_THEN_TURN: the cue after next, found by the same no-cursor-needed linear scan (safe since RouteSnap.distanceAlongRouteMeters only ever advances by construction).
  • NAV_REMAIN_M: the route-length complement of the same axis, clamped >= 0.
  • NAV_ETA_S: a genuinely new RollingSpeedAverage — a 30s moving-time-only window, deliberately neither AverageAccumulator (a whole-ride mean gets less responsive over time, exactly wrong for ETA) nor SpeedPipeline's 3s GPS-jitter smoothing (tuned for a live readout, still swings too hard for a stable ETA). Excludes STOPPED samples entirely (mirrors AvgHrAccumulator/AvgCadenceAccumulator) so an ETA holds steady at a red light instead of ballooning.
  • Route completion: latched, not re-evaluated every call — once within a reasoned 15m tolerance of the route's total length, FINISHED is sticky for the engine's lifetime, specifically to prevent oscillation from ordinary closest-point-projection wobble on the final segment (tested directly: a later fix reporting a smaller distance-along-route does not un-finish the engine).
  • "Coalesced to ~1Hz": correctly scoped out as a transport-layer concern — shared/message_keys.json's nav group already declares period_ms: 1000, mirroring #40's map-group convention. NavEngine itself does no rate limiting.

NAV_THEN_TURN/nextCue are plain nullable Kotlin, not a fabricated sentinel, when fewer than the needed cues remain ahead — matches the established "unavailable is never a fabricated value" rule.

Deliberately does not produce NO_ROUTE or OFF_ROUTE — NavProgressState only ever alternates ON_ROUTE/FINISHED; composing the full NAV_STATE wire enum with #32's off-route detection is left to a later layer, matching RouteSnap's own scoping precedent.

Real verification: genuine ./gradlew :companion:core:test --rerun-tasks, full :companion:core suite green, NavEngineTest 5/5 including a real jittery-speed-to-stable-ETA test (settled values within 2s of each other vs. a ~400s naive instantaneous swing) and the FINISHED-latch-against-backward-noise case.

This closes part of #39's remaining scope (distance-to-turn/ETA) — #39 still needs #32 for the hysteresis half.

43 issues closed.

Closed by PR #119 (`area/nav-engine`). New `NavEngine`/`NavUpdate`/`NextCue`/`RollingSpeedAverage` in `companion/core/.../route/nav/NavEngine.kt`, consuming #31's `RouteSnap` and the route's own `Cue` list directly — no new position tracking or distance-along-route computation, everything falls out of the axis those two already share. - **Distance to next cue, along the route**: `cue.distanceAlongRouteMeters - riderSnap.distanceAlongRouteMeters` — never straight-line. - **NAV_THEN_TURN**: the cue after next, found by the same no-cursor-needed linear scan (safe since `RouteSnap.distanceAlongRouteMeters` only ever advances by construction). - **NAV_REMAIN_M**: the route-length complement of the same axis, clamped `>= 0`. - **NAV_ETA_S**: a genuinely new `RollingSpeedAverage` — a 30s moving-time-only window, deliberately neither `AverageAccumulator` (a whole-ride mean gets *less* responsive over time, exactly wrong for ETA) nor `SpeedPipeline`'s 3s GPS-jitter smoothing (tuned for a live readout, still swings too hard for a stable ETA). Excludes `STOPPED` samples entirely (mirrors `AvgHrAccumulator`/`AvgCadenceAccumulator`) so an ETA holds steady at a red light instead of ballooning. - **Route completion**: latched, not re-evaluated every call — once within a reasoned 15m tolerance of the route's total length, `FINISHED` is sticky for the engine's lifetime, specifically to prevent oscillation from ordinary closest-point-projection wobble on the final segment (tested directly: a later fix reporting a *smaller* distance-along-route does not un-finish the engine). - **"Coalesced to ~1Hz"**: correctly scoped out as a transport-layer concern — `shared/message_keys.json`'s `nav` group already declares `period_ms: 1000`, mirroring #40's `map`-group convention. `NavEngine` itself does no rate limiting. `NAV_THEN_TURN`/`nextCue` are plain nullable Kotlin, not a fabricated sentinel, when fewer than the needed cues remain ahead — matches the established "unavailable is never a fabricated value" rule. Deliberately does not produce `NO_ROUTE` or `OFF_ROUTE` — `NavProgressState` only ever alternates `ON_ROUTE`/`FINISHED`; composing the full `NAV_STATE` wire enum with #32's off-route detection is left to a later layer, matching `RouteSnap`'s own scoping precedent. **Real verification**: genuine `./gradlew :companion:core:test --rerun-tasks`, full `:companion:core` suite green, `NavEngineTest` 5/5 including a real jittery-speed-to-stable-ETA test (settled values within 2s of each other vs. a ~400s naive instantaneous swing) and the FINISHED-latch-against-backward-noise case. This closes part of #39's remaining scope (distance-to-turn/ETA) — #39 still needs #32 for the hysteresis half. 43 issues closed.
Sign in to join this conversation.
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
robert/PedalPebble#33
No description provided.