Nav engine: next/then-turn, remaining distance and ETA (#33) #119
No reviewers
Labels
No labels
area:companion
area:docs
area:shared
area:tooling
area:watchapp
blocker
kind:chore
kind:feature
kind:spike
kind:test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/PedalPebble!119
Loading…
Reference in a new issue
No description provided.
Delete branch "area/nav-engine"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #33.
What NavEngine consumes/produces
NavEngine(route: GpxRoute, cues: List<Cue>), oneupdate(snap: RouteSnap, speedSample: SpeedPipelineSample): NavUpdatecall per fix. Consumes exactly two existing types — #31'sRouteSnapand the route's ownCuelist (#26/#40's tiers) — and the sameSpeedPipelineSampleshape the rest of the ride pipeline already produces, per the issue's own "shaped consistently with what the rest of the ride pipeline already produces" instruction. No new position tracking, no new distance-along-route computation.NavUpdateis a plain in-memory output type (progressState,nextCue: NextCue?,thenTurn: Direction?,remainingDistanceMeters,etaSeconds: Long?) — deliberately not a wire encoding. There is no existing "NAV wire encoding" class in this codebase yet (checked before writing this), so this mirrorsRouteSnap/MapSliceBuilder's own precedent of a pure-computation type that a later, separate issue turns into AppMessage keys.Rolling-average-speed design
New
RollingSpeedAverageclass (inNavEngine.kt, same package): a 30 s, moving-time-only simple moving average ofSpeedPipelineSample.speedMetersPerSecond,ArrayDeque-backed, ramping up from a partial window rather than withholding output (shape borrowed fromPower3sAverage, but a genuinely new class —Power3sAveragenever gates onMovementState, this one must).AverageAccumulator(the whole-ride mean behindAVG_HR/AVG_CADENCE/AVG_POWER): gets more stable and less responsive the longer a ride runs — exactly wrong for an ETA that needs to reflect current pace, not the ride-to-date average.SpeedPipeline's own 3-sample GPS smoothing: that removes single-fix jitter for a live speed readout where a half-second of lag is fine; a 3 s average still swings hard on ordinary riding variation (coast into a light, sprint out of it) and produces a jittery ETA.AvgHrAccumulator/AvgCadenceAccumulator: a sample whileMovementState.STOPPEDis excluded entirely rather than folded in as0.0, so a red light holds the ETA steady instead of ballooning it. Tested directly (NavEngineTest, "excludes stopped time")."Coalesced to about 1 Hz"
Scoped to the transport layer, which does not exist yet — same as #40's
MapSliceBuildercaveat forMAP_*keys.shared/message_keys.json'snavgroup already declaresperiod_ms: 1000/heartbeat_ms: 3000, mirroring themapgroup's own 0.2 Hz/5000 ms convention from #40.NavEngineitself is pure computation with no rate limiting of its own; whatever future AppMessage-transport layer wiresNavUpdateonto the wire is where the actual "at most once every ~1000 ms" gate belongs, using that already-declaredperiod_ms. Scoping a second rate limiter into this class would just duplicate that convention.Then-turn / route-completion edge cases
thenTurnisnull(not a fabricatedDirection) whenever fewer than two cues remain ahead of the rider — plain Kotlin nullability, the same "unavailable is a sentinel, never a fabricated value" convention already used throughout this codebase (Cue.streetName,AvgHrAccumulator.averageBpm, ...), rather than inventing a new wire-level sentinel at this (pre-wire) layer.RouteSnap.distanceAlongRouteMetersonly ever advances, "next cue" is just the first sorted cue still ahead of the rider — a passed cue drops out of that filter by itself. Cue counts are small, so the linear scan costs nothing at ~1 Hz.FINISH_TOLERANCE_METERS(15 m — comfortably aboveGpxImporter's own 5 m RDP simplification tolerance, #24) of the route's total length. Latched, not re-evaluated every call: onceFINISHED, it is sticky for the engine's lifetime, because ordinary closest-point-projection wobble on the rider's final polyline segment can shift the reported distance-along-route a meter or two either way between calls even though the search cursor itself never moves backward — without the latch this could oscillateFINISHED↔ON_ROUTEright at the finish. Tested directly, including a fix reporting a smaller distance-along-route than the one that already triggeredFINISHED.NavProgressStateis deliberately onlyON_ROUTE/FINISHED, not the fullNAV_STATEwire enum —NO_ROUTEandOFF_ROUTE(#32) are out of scope here, composed in one layer up once #32 exists, same scopingRouteSnap's own KDoc already draws.Tests
companion/core/src/test/kotlin/de/butzei/pedalpebble/core/route/nav/NavEngineTest.kt, 5 tests, real./gradlew :companion:core:test --rerun-tasksrun (green, full suite):bikerouter-style-cues.gpxtwo-cue route, run through the actualGpxImporter/GpxCueEnricherpipeline.NAV_REMAIN_Mmonotonically decreasing as the rider advances along that same real route.FINISHEDand stayingFINISHEDagainst a later fix reporting a smaller distance-along-route (backward GPS noise near the end).Files
companion/core/src/main/kotlin/de/butzei/pedalpebble/core/route/nav/NavEngine.kt(new)companion/core/src/test/kotlin/de/butzei/pedalpebble/core/route/nav/NavEngineTest.kt(new)