Skip to content

health: export overnight beat-to-beat to Apple Health (HKHeartbeatSeriesSample) - #600

Draft
RyanBeckett wants to merge 1 commit into
OpenStrap:mainfrom
RyanBeckett:health/heartbeat-series-export
Draft

RyanBeckett wants to merge 1 commit into
OpenStrap:mainfrom
RyanBeckett:health/heartbeat-series-export

Conversation

@RyanBeckett

@RyanBeckett RyanBeckett commented Oct 14, 2026 •

Copy link
Copy Markdown

Closes #599.

Why

HealthKit's only HRV quantity is SDNN, so on iOS OpenStrap's nightly RMSSD never reaches Apple Health. Readers that compute RMSSD themselves from heartbeat series, Bevel's default HRV method being one, get no HRV at all from an OpenStrap night, because today only an Apple Watch writes heartbeat series. This adds those series for the night, built from the same beats and clock the nightly RMSSD uses. Related: #315.

What

  • lib/health/health_heartbeat_series.dart (new)
    • buildSleepHeartbeatSeries: pure; runs on an isolate.
      • Takes plausible primary-band beats inside the sleep window and runs monotonizeBeatAxis then correctRr(rr, rrTsMs:).
      • Emits nnTimesMs as beat instants in series of ≤5 min.
      • precededByGap is true unless elapsed time equals the cleaned interval.
      • Corrected beats keep their real instant and are flagged, so no interpolated value is written.
    • MethodChannelHealthKitHeartbeatWriter: serialised replace with a finite timeout (same shape as the sleep writer), plus requestAuthorization.
  • lib/health/health_export.dart
    • _exportAppleHeartbeats runs in _exportDay right after the Apple sleep replace and folds into the day's success.
    • It reads decoded_rr with kPrimaryBandSourceSql and COALESCE(beat_ts_ms, rr_ts_ms), the same coalesce derive_prepare uses.
    • Cleanup window: sleepCleanupWindow.
    • No night means a clear. Night present but no beats left (pruned) means a skip, so re-exporting an old day never deletes what it can't rewrite.
    • request() also asks for heartbeat-series share access on Apple.
    • HealthExporter takes an injectable HealthKitHeartbeatWriter.
  • ios/Runner/HealthKitHeartbeatWriter.swift (new)
    • Channel openstrap/healthkit_heartbeat.
    • Deletes our HKSeriesType.heartbeat() samples in the window, then writes each series through its own HKHeartbeatSeriesBuilder, sequentially.
    • Refuses malformed series (non-increasing offsets, length mismatch, more than maximumCount beats) rather than repairing them.
  • AppDelegate.swift, project.pbxproj: register the channel and add the file to the Runner target.
  • Info.plist: the update usage string mentions beat-to-beat rhythm during sleep.

No metric output changes, so no kAlgoVersion bump. No schema change. Android is untouched (it already writes RMSSD).

Tests

test/health_heartbeat_series_test.dart (10 tests):

  • A contiguous run gives exact intervals and no gaps.
  • A dropout re-anchors and flags exactly one beat.
  • A missed beat keeps its real instant, is flagged, and no instant is invented.
  • An implausible interval is dropped and the next beat flagged.
  • Beats are clipped to the sleep window.
  • Long runs split at 5 min.
  • Absent or malformed input gives no series.
  • Channel payload; an empty list is still sent as the clear.
  • Agreement: on an hour of simulated sinus-arrhythmic beats with dropouts and missed detections, RMSSD computed the way a Health reader would (instants → intervals, honouring precededByGap) is 19.60 ms against hrvTime(correctRr(...)) at 19.52 ms (0.4%). The test asserts within 5%.

flutter analyze is clean. Full flutter test --concurrency=1 locally (Flutter 3.41.6, Linux) gives 6088 passed and 24 skipped. The one failure is test/adapters/miscale_test.dart's DST case: it is timezone-dependent, fails only under the machine's BST, passes with TZ=UTC (as CI runs), and is untouched by this branch.

Not verified, needs a Mac and device

  • The Swift has not been compiled. I wrote this on Linux. Please build it and check HealthKitHeartbeatWriter.swift against the iOS 15 deployment target.
  • On-device: after one night, Health → Browse → Heart → Heartbeat Series should list OpenStrap series across the sleep window. A re-export should replace them, not duplicate them.
  • Whether Bevel or other readers use non-Watch heartbeat series for RMSSD is unconfirmed; see the open questions in Export overnight beat-to-beat (HKHeartbeatSeriesSample) to Apple Health so Health readers can compute RMSSD #599.
  • Existing users have to tap the Health connect/grant control again before the new type is authorised. Writes no-op silently until then, as everywhere else in the exporter.

🤖 Generated with Claude Code

Summary by Sourcery

Export cleaned overnight heartbeat series to Apple Health so downstream readers can calculate HRV metrics such as RMSSD from OpenStrap data.

New Features:

  • Export overnight beat-to-beat heartbeat series to Apple Health for use by apps that calculate HRV from heartbeat intervals.
  • Add explicit Apple Health authorization support for heartbeat-series sharing.

Enhancements:

  • Build heartbeat series from the same cleaned primary-band beats used for nightly HRV, preserving real beat instants and gap information.
  • Replace OpenStrap-owned heartbeat series during re-export while safely handling missing nights and pruned beat data.
  • Process overnight heartbeat-series construction off the UI isolate and serialize native HealthKit writes with validation and timeouts.

Documentation:

  • Update the Apple Health usage description to disclose writing beat-to-beat rhythm during sleep.

Tests:

  • Add coverage for heartbeat-series construction, gaps, clipping, splitting, payloads, clearing, and agreement with nightly RMSSD calculations.

HealthKit's only HRV quantity is SDNN, so on iOS the nightly RMSSD never
reaches Apple Health. Readers that compute RMSSD themselves from
HKHeartbeatSeriesSample (Bevel's default HRV method) get no HRV from an
OpenStrap night: today only an Apple Watch writes heartbeat series.

Write the night's primary-band beats as heartbeat series on the clock
the nightly RMSSD runs on (correctRr's nnTimesMs). Only measured
instants are written; a spline-corrected beat keeps its real time and
is flagged precededByGap, as is every beat after a dropout or dropped
artifact run. Series are <=5 min; the native writer deletes our own
series in the sleep cleanup window first, so re-export never
duplicates. A night whose beats are pruned is skipped, not cleared.

The health plugin has no heartbeat-series type, so request() also asks
for HKSeriesType.heartbeat() share access natively.

No metric output change (no kAlgoVersion bump), no schema change,
Android untouched.
@coderabbitai

coderabbitai Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR exports overnight primary-band beat instants as replaceable HKHeartbeatSeriesSamples, using the existing cleaned HRV beat pipeline and isolate processing, then writes them through a serialized native HealthKit channel with explicit authorization, validation, cleanup, and re-export semantics. Review should focus especially on compiling and validating the Swift HealthKit integration on iOS 15 and confirming authorization and on-device replacement behavior.

Sequence diagram for overnight heartbeat export to Apple Health

sequenceDiagram
    participant Exporter as HealthExporter
    participant DB as LocalDb
    participant Isolate as BackgroundIsolate
    participant Writer as HealthKitHeartbeatWriter
    participant HealthKit as HKHealthStore

    Exporter->>DB: rawQuery(decoded_rr)
    DB-->>Exporter: primary-band rr_ms and beat timestamps
    Exporter->>Isolate: buildSleepHeartbeatSeries(rrMs, rrTsMs, sleep window)
    Isolate->>Isolate: monotonizeBeatAxis()
    Isolate->>Isolate: correctRr(rr, rrTsMs:)
    Isolate-->>Exporter: heartbeat series with offsets and gap flags
    Exporter->>Writer: replace(cleanup window, series)
    Writer->>HealthKit: deleteObjects(heartbeat samples)
    Writer->>HealthKit: HKHeartbeatSeriesBuilder.addHeartbeatWithTimeInterval()
    Writer->>HealthKit: finishSeries()
    HealthKit-->>Writer: write result
    Writer-->>Exporter: export success
Loading

State diagram for replaceable heartbeat-series export

stateDiagram-v2
    [*] --> ReadNight
    ReadNight --> Clear: no sleep night
    ReadNight --> Skip: night exists but no beats
    ReadNight --> Build: beats available
    Build --> Replace
    Clear --> DeleteWindow
    Replace --> DeleteWindow
    DeleteWindow --> WriteSeries
    WriteSeries --> Success: all writes succeed
    WriteSeries --> Failure: validation or write error
    Skip --> [*]
    Success --> [*]
    Failure --> [*]
Loading

File-Level Changes

Change Details Files
Builds validated overnight heartbeat-series payloads from the same cleaned primary-band beats used for nightly HRV, preserving real beat timestamps and gap metadata.
  • Filters beats to the sleep window and plausible primary-band input.
  • Runs beat-axis monotonicity and RR correction off the UI isolate.
  • Exports only real beat instants, marks discontinuities with precededByGap, and splits runs at five minutes.
  • Adds unit coverage for dropouts, missed or invalid beats, clipping, splitting, malformed input, and RMSSD agreement.
lib/health/health_heartbeat_series.dart
lib/health/health_export.dart
test/health_heartbeat_series_test.dart
Adds a serialized, replace-oriented Flutter-to-HealthKit writer for native heartbeat series.
  • Requests separate HKHeartbeatSeriesType share authorization.
  • Deletes only OpenStrap heartbeat samples in the cleanup window before sequentially writing new builders.
  • Validates series structure and bounds natively, applies a finite timeout, and treats empty series as an explicit clear.
  • Injects the writer into HealthExporter and folds heartbeat export status into daily Apple export success.
ios/Runner/HealthKitHeartbeatWriter.swift
lib/health/health_heartbeat_series.dart
lib/health/health_export.dart
Registers the new iOS channel and documents the additional HealthKit data written.
  • Registers the native writer with the Flutter app and adds it to the Runner target.
  • Updates the HealthKit usage description to mention overnight beat-to-beat rhythm.
ios/Runner/AppDelegate.swift
ios/Runner.xcodeproj/project.pbxproj
ios/Runner/Info.plist

Assessment against linked issues

Issue Objective Addressed Explanation
#599 Export primary-band beat-to-beat data within each sleep window to Apple Health as HKHeartbeatSeriesSamples, using the corrected RMSSD beat clock, preserving measured instants and marking gaps without exporting interpolated beats. ✅
#599 Make heartbeat-series export replaceable and idempotent: delete OpenStrap's series in the sleep cleanup window before writing, clear when there is no night, and skip rather than clear when the night exists but its beats have been pruned. ✅
#599 Request Apple Health authorization to share heartbeat series, which the existing Health plugin does not expose. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

This branch has not been deployed

No deployments
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.

Export overnight beat-to-beat (HKHeartbeatSeriesSample) to Apple Health so Health readers can compute RMSSD

1 participant