Heart-rate uplink: batching and a dropout buffer #66

Open
opened 2026-09-01 17:35:45 +02:00 by robert · 0 comments
robert commented 2026-09-01 17:35:45 +02:00 (Migrated from git.butzei.de)

Goal

Stop losing heart rate to Bluetooth dropouts.

Heart rate is generated on the watch (D3) and recorded on the phone (#20), and nothing connected those two facts. The uplink rate for heart rate was specified nowhere, and nothing buffered samples across a disconnection. NFR-R2 and NFR-R3 cover the phone's ride state, not the watch's samples.

On a bike the phone is in a jersey pocket with a body between it and the wrist. 2.4 GHz dropouts of tens of seconds are ordinary, not exceptional - so as planned, every one of them punched a hole in the recorded ride.

Batching also removes a message per second from the uplink, which the map view will want back.

Acceptance criteria

  • Watch-side ring buffer holding at least 10 minutes of samples at the production sample period from #5
  • Samples sent as a batched HR_SAMPLES message every 5 s, not one message per second
  • Wire format per docs/PROTOCOL.md section 5: uint32 base_timestamp then a run of uint8 bpm
  • On reconnect the backlog drains oldest-first before live samples resume
  • Buffer overflow drops oldest-first, and the drop is recorded rather than silently swallowed
  • Phone writes gaps into the GPX as gaps - never interpolated across a dropout
  • Buffer survives a watchapp restart, alongside the ride state in #12
  • Heap cost of the buffer measured with heap_bytes_free() and recorded - 10 minutes at 1 s is 600 bytes, but the production period may differ
  • Tested by disconnecting for 30 s, 5 min and longer than the buffer holds

Files

  • watchapp/src/c/hrm.c
  • watchapp/src/c/proto.c
  • companion/.../pebble/Proto.kt
  • companion/.../ride/GpxRecorder.kt

Notes

See D31 and docs/PROTOCOL.md section 5.

Update — 2026-09-02: do not assume a fixed sample cadence

The buffer specified "a run of uint8 bpm at the production sample period".
HealthEventHeartRateUpdate fires irregularly, so a fixed-cadence run misattributes every
timestamp after the first — and those timestamps land in the recorded GPX, where nothing can recover
them (D46).

  • HR_SAMPLES carries uint32 base_timestamp followed by (uint8 dt, uint8 bpm) pairs, where
    dt is seconds since the previous sample in the run and 0 for the first
  • A dt that would exceed 254 s cuts the run; a new message starts instead
  • Sample times are recorded as observed, never reconstructed from a nominal period
  • The 10-minute buffer, oldest-first draining, and gaps-recorded-as-gaps are unchanged
## Goal Stop losing heart rate to Bluetooth dropouts. Heart rate is generated on the watch (D3) and recorded on the phone (#20), and nothing connected those two facts. The uplink rate for heart rate was specified nowhere, and nothing buffered samples across a disconnection. NFR-R2 and NFR-R3 cover the phone's ride state, not the watch's samples. On a bike the phone is in a jersey pocket with a body between it and the wrist. 2.4 GHz dropouts of tens of seconds are ordinary, not exceptional - so as planned, every one of them punched a hole in the recorded ride. Batching also removes a message per second from the uplink, which the map view will want back. ## Acceptance criteria - [ ] Watch-side ring buffer holding **at least 10 minutes** of samples at the production sample period from #5 - [ ] Samples sent as a batched `HR_SAMPLES` message every 5 s, not one message per second - [ ] Wire format per docs/PROTOCOL.md section 5: `uint32 base_timestamp` then a run of `uint8 bpm` - [ ] On reconnect the backlog drains oldest-first before live samples resume - [ ] Buffer overflow drops oldest-first, and the drop is recorded rather than silently swallowed - [ ] Phone writes gaps into the GPX **as gaps** - never interpolated across a dropout - [ ] Buffer survives a watchapp restart, alongside the ride state in #12 - [ ] Heap cost of the buffer measured with `heap_bytes_free()` and recorded - 10 minutes at 1 s is 600 bytes, but the production period may differ - [ ] Tested by disconnecting for 30 s, 5 min and longer than the buffer holds ## Files - `watchapp/src/c/hrm.c` - `watchapp/src/c/proto.c` - `companion/.../pebble/Proto.kt` - `companion/.../ride/GpxRecorder.kt` ## Notes See D31 and docs/PROTOCOL.md section 5. ## Update — 2026-09-02: do not assume a fixed sample cadence The buffer specified "a run of `uint8` bpm at the production sample period". `HealthEventHeartRateUpdate` **fires irregularly**, so a fixed-cadence run misattributes every timestamp after the first — and those timestamps land in the recorded GPX, where nothing can recover them (D46). - [ ] `HR_SAMPLES` carries `uint32 base_timestamp` followed by `(uint8 dt, uint8 bpm)` pairs, where `dt` is seconds since the previous sample in the run and `0` for the first - [ ] A `dt` that would exceed 254 s cuts the run; a new message starts instead - [ ] Sample times are recorded as observed, never reconstructed from a nominal period - [ ] The 10-minute buffer, oldest-first draining, and gaps-recorded-as-gaps are unchanged
Sign in to join this conversation.
No description provided.