Record rides to GPX, including HR embedding capability (issue #20) #111
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!111
Loading…
Reference in a new issue
No description provided.
Delete branch "area/gpx-recording"
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 #20.
What this builds
GpxRecorder/GpxWriter(:companion:core, pure JVM, noandroid.*) — an incremental GPX 1.1writer that flushes after every
<trkpt>, plus the pause/resume segment-splitting policy driven byMovementState(D35), the same factSpeedPipeline/StopDetector(#17/#69) already produce.RideGpxFiles(:companion:ride) — the thin Android adapter that opens a real file undergetExternalFilesDir("gpx")and givesGpxRecordera realjava.io.Writer.RideServicenow feeds everySpeedPipeline-accepted fix into the recorder and finishes the fileon stop/destroy. Recording is best-effort: an open/write failure logs and gives up on recording for
the rest of the ride rather than affecting the ride itself.
Re-assessment context
This issue was judged mostly blocked earlier tonight (HR needs #66→#16→#4's hardware chain; live
speed needed #17/#69, which didn't exist yet). #17/#69 have since merged (PRs #101/#100) — a real
SpeedPipeline/StopDetectorpipeline now exists andRideServicealready runs real fixes throughit — so the core GPX-writing scope is real and buildable. HR embedding is still genuinely blocked on
data (see below) and is scoped out explicitly, same honest pattern as #65's un-fed accumulators.
Acceptance criteria, checked off honestly
addTrackPoint()flushes immediately; a crashloses at most the points since the last flush. This is a stronger bound than #12's watch-side
precedent (a 30 s periodic full-snapshot rewrite, throttled for flash-write endurance): GPX is an
append-only text format, so appending each point already is the incremental write, with no
snapshot-rewrite cost to throttle against.
is built and tested:
GpxWriterembeds<gpxtpx:hr>underhttp://www.garmin.com/xmlschemas/TrackPointExtension/v1, verified against Garmin's own publishedXSD (
https://www8.garmin.com/xmlschemas/TrackPointExtensionv1.xsd) rather than recalled frommemory —
hrisBeatsPerMinute_t, anxsd:unsignedBytewithminInclusive1, whichGpxTrackPoint's owninitblock enforces. No real HR value exists anywhere incompanion/today (checked by grep for
HR_SAMPLES/HR_BPM/heart.?rateacross the module, same checkDerivedMetrics.kt'sAvgHrAccumulatorKDoc already documents) —HR_SAMPLESdecoding is #66,itself blocked on #16/#4's hardware chain.
heartRateBpmis nullable end-to-end andRideServicealways passes
nulltoday; no fake HR source was fabricated to exercise the embedding path. The2026-09-01 update's batched-decode/gap/backlog requirements are #66's ordering problem to solve, not
this writer's —
GpxRecorder.onFix()makes no assumption that calls arrive in wall-clock order, soonce #66 exists, feeding it fix-by-fix in true timestamp order is enough.
<ele>when present, omitted when not, tested both ways), but it is never available today:GpsFix/LocationFix(:companion:core/:companion:location, #17/#19's own types) carry noaltitude field at all, even though
android.location.Location.getAltitude()exists on the platform.Plumbing that through is real, separate scope outside this issue's
Files:list —RideServicepasses
nulluntil it happens. Documented inGpxRecorder's KDoc, not fabricated.GpxRecorderdrops every fixStopDetectorreports
STOPPED(no jitter points from a parked phone), closes the current<trkseg>the instanta stop is confirmed, and opens a fresh one on the first fix reported
MOVINGagain — a stop becomesa segment seam, mirroring a real GPS head unit's own auto-pause export. Proven by
GpxRecorderTest's stop/resume/multi-cycle/starts-already-stopped cases, asserted against the raw<trkseg>count (import-side flattening erases this, see below).GpxRecorderTest's round-trip test writes a full recording (two segments, elevation, HR) andre-imports it through this project's own
GpxImporter(#24), asserting geometry/elevation/timestamps come back exactly as written. The fixture geometry deliberately mirrors
GpxImporterTest's own zigzag pattern, sinceGpxImporterruns everything throughPolylineSimplifier(~5 m RDP tolerance) before returning it — a straight-line fixture would haveinterior points legitimately simplified away, proving nothing. Segment-seam and HR-embedding
structure are checked against the raw XML separately, since
GpxImporterdeliberately flattens<trkseg>boundaries on read and its ownGpxPointhas no HR field.Tests, run for real
./gradlew :companion:core:test— passes, including 13 newGpxWriterTestcases and 7 newGpxRecorderTestcases (the round trip among them)../gradlew :companion:assembleDebug— passes (confirms theRideService/RideGpxFilesAndroidwiring compiles for real).
https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt