GPX ride recording including heart rate #20

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

Goal

Record the ride so it can be exported and re-imported into komoot or Strava.

Acceptance criteria

  • Track points written incrementally, so a crash loses at most a few seconds
  • Heart rate from HR_BPM embedded using the standard Garmin TrackPointExtension namespace
  • Elevation included where available
  • Paused segments handled sensibly (separate track segments)
  • Resulting file validates and re-imports into komoot cleanly

Files

  • companion/.../ride/GpxRecorder.kt

Update — 2026-09-01: heart rate arrives batched, and gaps stay gaps

Heart rate now reaches the phone as a batched HR_SAMPLES message every 5 s, with a watch-side
buffer that survives a Bluetooth dropout (#66, D31).

  • Decode batched samples using their base_timestamp, not arrival time — a drained backlog is
    minutes old and must land at the right point in the track
  • Dropout gaps written as gaps, never interpolated
  • Backlog arriving after the ride has been paused or stopped still lands correctly
## Goal Record the ride so it can be exported and re-imported into komoot or Strava. ## Acceptance criteria - [ ] Track points written incrementally, so a crash loses at most a few seconds - [ ] Heart rate from `HR_BPM` embedded using the standard Garmin TrackPointExtension namespace - [ ] Elevation included where available - [ ] Paused segments handled sensibly (separate track segments) - [ ] Resulting file validates and re-imports into komoot cleanly ## Files - `companion/.../ride/GpxRecorder.kt` ## Update — 2026-09-01: heart rate arrives batched, and gaps stay gaps Heart rate now reaches the phone as a batched `HR_SAMPLES` message every 5 s, with a watch-side buffer that survives a Bluetooth dropout (#66, D31). - [ ] Decode batched samples using their `base_timestamp`, not arrival time — a drained backlog is minutes old and must land at the right point in the track - [ ] **Dropout gaps written as gaps, never interpolated** - [ ] Backlog arriving after the ride has been paused or stopped still lands correctly
Owner

Closed by PR #111 (merged). GpxWriter (pure, :companion:core) writes incrementally, embeds heart rate under Garmin's real TrackPointExtension/v1 namespace and element shape (verified against the published XSD, BeatsPerMinute_t enforced as a real range constraint), and GpxRecorder turns MovementState.STOPPED/MOVING transitions into real track-segment seams -- a stop closes the current trkseg, the first moving fix after opens a new one.

Honest gaps, not silently faked: elevation support is fully built and tested in GpxWriter/GpxRecorder, but GpsFix/LocationFix (#17/#19) carry no altitude field at all despite Location.getAltitude() existing on the platform -- a real, separate gap flagged for those types, not fixed here. HR stays nullable end-to-end since nothing in companion/ produces a live bpm yet (confirmed via grep) -- the capability is real and tested, just unfed, matching #65's own pattern.

Real round-trip validation: a two-segment, elevation+HR-bearing recording written by GpxWriter and re-imported through the project's own GpxImporter (#24), asserting lat/lon/elevation/timestamps match exactly -- a genuine consistency proof between the read and write sides, not just well-formed-XML checking.

Verified for real: ./gradlew :companion:core:test (20 new tests, 0 failures), ./gradlew :companion:assembleDebug (RideService/RideGpxFiles Android wiring compiles).

Closed by PR #111 (merged). GpxWriter (pure, :companion:core) writes incrementally, embeds heart rate under Garmin's real TrackPointExtension/v1 namespace and element shape (verified against the published XSD, BeatsPerMinute_t enforced as a real range constraint), and GpxRecorder turns MovementState.STOPPED/MOVING transitions into real track-segment seams -- a stop closes the current trkseg, the first moving fix after opens a new one. Honest gaps, not silently faked: elevation support is fully built and tested in GpxWriter/GpxRecorder, but GpsFix/LocationFix (#17/#19) carry no altitude field at all despite Location.getAltitude() existing on the platform -- a real, separate gap flagged for those types, not fixed here. HR stays nullable end-to-end since nothing in companion/ produces a live bpm yet (confirmed via grep) -- the capability is real and tested, just unfed, matching #65's own pattern. Real round-trip validation: a two-segment, elevation+HR-bearing recording written by GpxWriter and re-imported through the project's own GpxImporter (#24), asserting lat/lon/elevation/timestamps match exactly -- a genuine consistency proof between the read and write sides, not just well-formed-XML checking. Verified for real: ./gradlew :companion:core:test (20 new tests, 0 failures), ./gradlew :companion:assembleDebug (RideService/RideGpxFiles Android wiring compiles).
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#20
No description provided.