Auto-pause: one definition of stopped, in Phase 2 (#69) #100
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!100
Loading…
Reference in a new issue
No description provided.
Delete branch "area/stop-detector"
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 #69.
Implements D35's one shared definition of "stopped":
StopDetector(companion/core,core.ridepackage) plusSpeedPipeline(companion/core,core.locationpackage), the wheel-vs-GPS arbitration that feeds it.Speed threshold: 0.8 m/s, the same number as #17's display clamp, deliberately — two numbers for "stopped" is exactly the class of bug D35 exists to prevent (the display reading 0 km/h while the ride timer still counted the same GPS jitter as motion, or vice versa).
Dwell: 3 s before a stop registers — long enough that one noisy low sample (a bad GPS fix, a tight slow corner) can never trigger a stop on its own, short enough not to visibly lag a real stop.
Resume: 1.5 m/s held for 1 s — hysteresis: the 1.5 m/s threshold sits comfortably above brisk walking pace (~1.4 m/s) so a rider shuffling forward at a light does not flap the detector; the 1 s confirm rejects a single spurious high reading (multipath GPS spike, sensor glitch) without meaningfully delaying a real departure.
Wheel sensor preferred over GPS when live.
SpeedPipeline.onWheelSpeedSampletreatsWheelSpeedSample.NoNewRevolutionas a genuine, noise-free 0.0 m/s — the wheel confirming it isn't turning, not a missing sample — and GPS fixes arriving while the wheel is live (reported within the last 5 s) are dropped outright, not merely deprioritised. The wheel gets no separate threshold or dwell-skip; it feeds the sameStopDetectora cleaner signal, which is what "the wheel knows immediately" actually buys.Auto-pause on/off (#44).
autoPauseRideState()is the only place that setting gates anything —StopDetectoritself runs unconditionally (fed bySpeedPipelineregardless of the setting), so moving-time accumulation keeps working correctly even with auto-pause off, exactly as the issue asks.Manual pause always beats an automatic resume.
autoPauseRideState()checksmanualPauseActivefirst and unconditionally, before consultingMovementStateat all. On the watch side this composes with, rather than competes against,state.c's existing D43 semantics:docs/PROTOCOL.md§2.1/§3.1 already forbids aRIDE_STATEpush from overwriting a watch command the phone hasn't yet acknowledged viaSTATE_ACK, and the watch drains its queued Select/long-Select commands before accepting any push at all. This issue adds the phone-side half of the same principle for whichever future issue wires an actualmanualPauseActivesource (none exists yet — no phone-side pause UI, no watch-command receive path; that's downstream of #4/Spike A).StopDetector/autoPauseRideState()are the decision; wiring the source is out of this issue's scope.Module placement. Both files land in
:companion:corerather than the issue's literal.../ride/StopDetector.ktand.../location/SpeedPipeline.ktpaths taken as Gradle module paths —:companion:ridedepends on:companion:location, so aStopDetectorin:companion:ridecould never be consumed by aSpeedPipelinein:companion:location(a dependency cycle). Both paths'.../prefixes are read as elliptical package names instead, following the precedent this codebase already set withcore.ride.RideSetupState(pure decision logic in:companion:core, Android-facing adapter in:companion:location'sRidePermissions.kt). Full reasoning inSpeedPipeline.kt's own KDoc.No Android SDK surface needed. Both files have zero
android.*imports — confirmed, not assumed — and are entirely host-JVM-testable../gradlew :companion:core:testpasses for real: 18 new tests (12StopDetectorTest, 6SpeedPipelineTest), all green, no Android build invoked because nothing here has anything to prove against one.Tests.
StopDetectorTest.ktcovers the dwell/hysteresis state machine directly (brief dips absorbed, confirmed stop/resume timing, dwell-clock restart,autoPauseRideState's truth table).SpeedPipelineTest.ktcovers wheel-live-vs-GPS-fallback arbitration,NoNewRevolutiondriving a real stop, disconnect handling, exact wheel-distance accumulation — and a synthetic 1 Hz stop-and-go "traffic light" scenario (cruise 0–10 s, decelerate 11–16 s, stopped at the light 17–25 s, pull away 26–27 s, cruise 28–35 s) standing in for #23's not-yet-built GPX replay harness, asserting 25.0 s of moving time out of a 35.0 s synthetic ride (confirmed stop at t=18, confirmed resume at t=28) against hand-checked arithmetic documented in the test's own KDoc.Docs.
docs/DECISIONS.mdD35 updated with the concrete numbers this PR chose, per D48 (a checked fact — derived and verified by the tests in this PR, not recalled).https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt