Skip to content

activity: invalidate raw HR metric when the HRM reports off-wrist - #1960

Open
Dreamkeeper wants to merge 1 commit into
coredevices:mainfrom
Dreamkeeper:raw-hr-offwrist-invalidate
Open

activity: invalidate raw HR metric when the HRM reports off-wrist#1960
Dreamkeeper wants to merge 1 commit into
coredevices:mainfrom
Dreamkeeper:raw-hr-offwrist-invalidate

Conversation

@Dreamkeeper

Copy link
Copy Markdown

Problem

Off-wrist BPM events never update hr.metrics.current_bpm — the storage behind ActivityMetricHeartRateRawBPM / HealthMetricHeartRateRawBPM — so health_service_peek_current_value() keeps serving the last on-wrist reading indefinitely. The health event payload already reports current_bpm = 0 for off-wrist, but apps cannot read event payloads; peeking the metric is their only path.

Measured on a Pebble Time 2: after taking the watch off, an app peeking HealthMetricHeartRateRawBPM on each HeartRateUpdate event saw the last on-wrist value (bit-identical, e.g. 82) at ~1 Hz event cadence for 9+ minutes across table / face-down / on-fabric placements. For apps that infer wear state from the raw metric (safety monitors in particular), off-wrist is indistinguishable from a wearer with a steady pulse.

Root cause

Off-wrist, the gh3x2x driver zero-initializes HRMData (bpm = 0, quality = HRMQuality_OffWrist). In prv_hrm_subscription_cb, bpm 0 fails the ACTIVITY_DEFAULT_MIN_HR range check, so the metrics writer never runs — and nothing else ever invalidates the stored raw metric.

Fix

  • New activity_metrics_prv_set_raw_hr_offwrist() invalidates the peekable metric on off-wrist events (bpm 0, quality OffWrist, fresh timestamp), mirroring the adjacent locking/write patterns.
  • Off-wrist events are also excluded from valid_hr_reading, so a driver-dependent nonzero bpm on an off-wrist event (the existing bpm=120/OffWrist unit-test input) cannot overwrite the invalidation through the range-only check.

Possibly related to the existing TODO PBL-40784 about special-casing HRMQuality_OffWrist.

Testing

  • Regression assertions added to the existing off-wrist sequence in test_activity.c: raw BPM/quality after bpm=120/OffWrist, after bpm=0/OffWrist, and restoration on the next valid on-wrist reading. Full unit test suite passes.
  • Field-validated on a retail Pebble Time 2 (obelix@pvt, sideloaded dev build): the peeked value drops to 0 within ~30 s of removal; worn readings unchanged across a 450-sample, six-condition sensor sweep. Raw data available on request.

Disclosure

Developed with AI assistance (Claude). I reviewed the diff, built it locally, and field-tested it on real hardware; the use case is a safety application (unresponsiveness monitor) where wear-state honesty matters.

🤖 Generated with Claude Code

Off-wrist BPM events never update hr.metrics.current_bpm - the storage
behind ActivityMetricHeartRateRawBPM / HealthMetricHeartRateRawBPM -
so health_service_peek_current_value() keeps serving the last on-wrist
reading indefinitely. The health event payload already reports
current_bpm 0 for off-wrist, but apps cannot read event payloads;
peeking the metric is their only path.

Measured on a Pebble Time 2: after taking the watch off, apps peeking
HealthMetricHeartRateRawBPM on each HeartRateUpdate event saw the last
on-wrist value (bit-identical, e.g. 82) with ~1 Hz event cadence for
9+ minutes across table/face-down/fabric placements. For apps that
infer wear state from the raw metric (safety monitors in particular),
off-wrist is indistinguishable from a wearer with a steady pulse.

Invalidate the peekable metric on off-wrist events (bpm 0, quality
OffWrist, fresh timestamp), and exclude off-wrist events from the
metrics/median path entirely: an off-wrist event may still carry a
nonzero bpm (driver-dependent - the existing bpm=120/OffWrist unit
test input), which previously would have overwritten the invalidation
via the range-only validity check. Regression assertions added to the
existing off-wrist sequence in test_activity.

Field-validated on a retail Time 2 (obelix@pvt): the peeked value
drops to 0 within ~30 s of removal; worn readings unchanged across a
450-sample six-condition sensor sweep.

Signed-off-by: Dmitry Kvasnikov <dkvasnikov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant