Skip to content

ringconn: pair + drain, raw archive only - #344

Merged
abdulsaheel merged 6 commits into
mainfrom
feat/ringconn
Sep 5, 2026
Merged

abdulsaheel merged 6 commits into
mainfrom
feat/ringconn

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator

User description

pairs RingConn (Gen 2 / Gen 2 Air / Gen 3), authenticates, drains both history channels (sleep + awake), banks every frame verbatim. experimental — no signals declared, nothing decoded, pairing UI wired same as oura.

needs the protocol PR (OpenStrap/protocol#48) merged and pinned before this can merge — using a local pubspec_overrides.yaml in the meantime.

Summary by Sourcery

Integrate experimental RingConn support for authenticated raw-history synchronization across pairing, background sync, and device management.

New Features:

  • Add experimental RingConn pairing and authentication for Gen 2, Gen 2 Air, and Gen 3 devices.
  • Drain RingConn sleep and awake history channels into the raw archive without decoding physiological data.
  • Integrate RingConn manual pairing, device management, and background synchronization.

Bug Fixes:

  • Prevent notify-only sensors from inheriting an incorrect signal tier and route RingConn unpairing through its dedicated link.

Enhancements:

  • Extend the BLE link abstraction with resilient one-shot GATT characteristic reads.
  • Update the protocol dependency pin to include RingConn frame support.

Tests:

  • Add coverage for RingConn authentication, channel draining, raw archiving, registry integration, and failure handling.

PR Type

Enhancement


Description

  • Adds experimental RingConn pairing and authentication.

  • Drains history channels into raw_archive without decoding.

  • Adds read() to BandLink for GATT reads.

  • Prevents notify-only sensors from inheriting incorrect tiers.

  • Integrates RingConn into background headless sync flow.


Diagram Walkthrough

flowchart LR
  UI["UI (devices.dart)"] -- "Pair RingConn" --> RingConnLink["RingConnLink"]
  BgSync["Background Sync"] -- "Trigger Sync" --> RingConnLink
  RingConnLink -- "Drive Session" --> RingConnAdapter["RingConnAdapter"]
  RingConnAdapter -- "Read/Write/Notify" --> GattLink["GattBandLink"]
  RingConnAdapter -- "Bank Frames" --> RawArchive[("raw_archive DB")]
Loading

File Walkthrough

Relevant files
Configuration changes
1 files
_registry.dart
Add RingConn constants and pairTier to BandEntry                 
+84/-5   
Enhancement
6 files
adapter.dart
Add read method to BandLink interface                                       
+19/-0   
gatt_link.dart
Implement read method for GATT characteristic reads           
+23/-0   
ringconn.dart
Implement RingConnAdapter for auth and channel draining   
+309/-0 
ringconn_link.dart
Implement RingConnLink host for device sessions                   
+256/-0 
background_sync.dart
Add RingConn sync to headless background sync                       
+13/-6   
devices.dart
Add RingConn to device pairing UI                                               
+17/-1   
Bug fix
1 files
hrs_link.dart
Use pairTier and route RingConn unpairing                               
+18/-6   
Tests
4 files
adapter_signals_registry_test.dart
Add RingConn to adapter signals test                                         
+2/-0     
ringconn_adapter_test.dart
Test RingConn adapter state machine                                           
+296/-0 
band_registry_test.dart
Update registry tests to include RingConn                               
+1/-1     
ringconn_link_test.dart
Test RingConn host session and raw archiving                         
+237/-0 

Summary by CodeRabbit

  • New Features

    • Added support for pairing and syncing RingConn Gen 2, Gen 2 Air, and Gen 3 rings.
    • RingConn history is collected in the background and archived for later processing.
    • Added RingConn device management, synchronization, and removal options.
    • Added safeguards for reliable transfers, including timeouts, durable commits, and capped sync bursts.
  • Bug Fixes

    • Improved handling of Bluetooth characteristic reads, including missing, timed-out, or disconnected reads.

adds a BandLink.read() seam (ringconn's auth needs one GATT read that
nothing else did) and a per-entry pairTier so a second zero-signal
notify-class band can't inherit ble_hrs's beat-to-beat tag.
@sourcery-ai

sourcery-ai Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Introduces an experimental, raw-archive-only RingConn integration for Gen 2, Gen 2 Air, and Gen 3: it pairs and authenticates using the standard System ID, drains sleep and awake history through a new GATT-read-capable link, persists every frame verbatim without decoding, and integrates lifecycle, UI, background sync, and replay/database coverage. The implementation depends on the pending pinned openstrap_protocol PR.

Sequence diagram for RingConn pairing and raw history sync

sequenceDiagram
    actor User
    participant UI as Pairing UI
    participant HrsLink
    participant RingConnLink
    participant Ring as RingConn
    participant DB as Local DB

    User->>UI: Select RingConn
    UI->>HrsLink: pairNotifySensor(kRingConn)
    HrsLink->>Ring: Connect and discover services
    HrsLink->>DB: Store paired device

    RingConnLink->>Ring: Read System ID
    Ring-->>RingConnLink: EUI-64
    RingConnLink->>Ring: write status command
    Ring-->>RingConnLink: Authentication challenge
    RingConnLink->>Ring: write SM3 auth response
    Ring-->>RingConnLink: Authentication confirmation

    RingConnLink->>Ring: Open sleep channel at now
    Ring-->>RingConnLink: Sleep history pages
    RingConnLink->>Ring: ACK page
    RingConnLink->>Ring: Open awake channel at now
    Ring-->>RingConnLink: Awake history pages
    RingConnLink->>Ring: ACK page
    RingConnLink->>DB: Archive every frame verbatim
Loading

Flow diagram for RingConn authentication and dual-channel drain

flowchart TD
    Start[Start sync] --> Read[Read System ID]
    Read --> MAC[Recover BLE MAC]
    MAC --> Auth[Authenticate with challenge and SM3 response]
    Auth -->|success| Sleep[Drain sleep channel]
    Auth -->|failure| ArchiveAuth[Archive received auth frames]
    Sleep --> Awake[Drain awake channel]
    Awake --> Archive[Archive all frames verbatim]
    ArchiveAuth --> End[End sync]
    Archive --> End
Loading

File-Level Changes

Change Details Files
Adds an experimental RingConn adapter and host that authenticate from the ring’s System ID, drain both history channels, and archive all received frames without decoding signals.
  • Registers RingConn service, command/notify characteristics, and required System ID characteristic.
  • Implements one-shot MAC recovery, challenge-response authentication, channel sync-open/fetch/ACK draining, bounded timeouts, and frame buffering.
  • Archives authentication, status, and bulk frames verbatim while declaring no measurement signals or decoded samples.
  • Adds paired-device lifecycle, serialized secondary-link syncing, teardown, forgetting, and background-sync integration.
  • Adds replay-link and database tests covering authentication, channel draining, raw archival, no-signal behavior, failure paths, and pairing cleanup.
lib/ble/adapters/_registry.dart
lib/ble/adapters/ringconn.dart
lib/ble/ringconn_link.dart
lib/sync/background_sync.dart
test/adapters/ringconn_adapter_test.dart
test/ringconn_link_test.dart
Extends the BLE link abstraction to support the plain GATT read required by RingConn authentication.
  • Adds nullable characteristic reads to BandLink, with replay support and production timeout/error handling.
  • Requires RingConn’s System ID during characteristic validation.
lib/ble/adapters/adapter.dart
lib/ble/adapters/gatt_link.dart
lib/ble/adapters/_registry.dart
Makes notify-class pairing quality explicit per registry entry so zero-signal sensors do not inherit the heart-rate strap tier.
  • Adds BandEntry.pairTier and resolves omitted pairing tiers from the selected entry.
  • Explicitly assigns beat-to-beat quality only to the BLE heart-rate strap.
  • Keeps Oura and RingConn unranked and exposes RingConn as a null-pick pairing option.
lib/ble/adapters/_registry.dart
lib/ble/hrs_link.dart
lib/ui2/profile/devices.dart
test/band_registry_test.dart
Wires RingConn into device metadata, discovery, UI presentation, signal registry validation, and pairing/forget flows.
  • Adds the RingConn registry entry and empty declared-signal map.
  • Adds a distinct generic device icon and pairing description.
  • Routes forgetting through RingConn-specific teardown and validates registry signal coverage.
lib/ble/adapters/_registry.dart
lib/ble/hrs_link.dart
lib/ui2/profile/devices.dart
test/adapter_signals_registry_test.dart

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

@coderabbitai

coderabbitai Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: e53d32c2-b8c3-4651-8c1c-4cfe48e99f56

📥 Commits

Reviewing files that changed from the base of the PR and between 61b1994 and 32a7f83.

⛔ Files ignored due to path filters (5)
  • pubspec.lock is excluded by !**/*.lock
  • test/adapter_signals_registry_test.dart is excluded by !test/**
  • test/adapters/ringconn_adapter_test.dart is excluded by !test/**
  • test/band_registry_test.dart is excluded by !test/**
  • test/ringconn_link_test.dart is excluded by !test/**
📒 Files selected for processing (10)
  • lib/ble/adapters/_registry.dart
  • lib/ble/adapters/adapter.dart
  • lib/ble/adapters/gatt_link.dart
  • lib/ble/adapters/ringconn.dart
  • lib/ble/hrs_link.dart
  • lib/ble/ringconn_link.dart
  • lib/compute/derivation_engine.dart
  • lib/sync/background_sync.dart
  • lib/ui2/profile/devices.dart
  • pubspec.yaml

📝 Walkthrough

Walkthrough

Adds RingConn BLE support for pairing, authentication, raw history synchronization, archive storage, device management, and background synchronization. It also adds GATT read support and updates the protocol dependency pin.

Changes

RingConn BLE contracts

Layer / File(s) Summary
Registry and GATT read contracts
lib/ble/adapters/_registry.dart, lib/ble/adapters/adapter.dart, lib/ble/adapters/gatt_link.dart
Adds RingConn UUIDs, registry metadata, an empty signal set, and single-characteristic reads for GATT and replay links.

RingConn adapter

Layer / File(s) Summary
Authentication and history draining
lib/ble/adapters/ringconn.dart, pubspec.yaml, lib/compute/derivation_engine.dart
Adds SM3 authentication, sleep and awake channel draining, raw-frame archiving, durable page acknowledgements, timeout handling, and a 512-page burst limit. Updates the protocol dependency pin.

Connection and storage

Layer / File(s) Summary
Connection and archive orchestration
lib/ble/ringconn_link.dart
Adds paired-device lookup, serialized synchronization, secondary-link lifecycle handling, archive-row creation, cleanup, and scripted ingestion support.

Application integration

Layer / File(s) Summary
Device management and synchronization entry points
lib/ble/hrs_link.dart, lib/sync/background_sync.dart, lib/ui2/profile/devices.dart
Adds RingConn pairing and display metadata, manual and background synchronization, and RingConn-specific device deletion.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeviceScreen
  participant RingConnLink
  participant BluetoothDevice
  participant RingConnAdapter
  participant ArchiveStore
  DeviceScreen->>RingConnLink: request sync
  RingConnLink->>BluetoothDevice: connect and discover services
  RingConnLink->>RingConnAdapter: run synchronization
  RingConnAdapter->>BluetoothDevice: authenticate and drain history
  RingConnAdapter->>ArchiveStore: commit raw frames
  RingConnLink->>BluetoothDevice: disconnect
Loading
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ringconn

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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/ble/adapters/ringconn.dart" line_range="216-244" />
<code_context>
+      final rec = await inbox.next(replyTimeout);
</code_context>
<issue_to_address>
**issue (bug_risk):** The page loop silently stops after 512 pages even if the ring still reports more pages or has not emitted a burst-end response, leaving the remainder of a valid history burst undrained while still returning a successful session.

**Triggers:** When a channel has more than 512 pages queued in one burst.

**Suggested fix:** Surface the ceiling as an incomplete-drain failure or continue using a protocol-defined completion condition rather than treating the cap as a normal successful end.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if the unverified RingConn protocol or archive handling is wrong, the sync can persist incorrect or unusable raw records and leave paired-device state behind after a revert; those effects are bounded and can be removed or repaired, but they do not disappear automatically when the code is reverted.

Blocking findings: lib/ble/adapters/ringconn.dart:244


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lib/ble/adapters/ringconn.dart Outdated
@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 02ac02e)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Data-durability ordering violation

The adapter yields the SampleBatch and immediately sends the ACK to the ring. Because yield in an async* function does not await the consumer's asynchronous processing (like a DB write), the ACK is sent before the data is durably committed to SQLite. If the app crashes or is killed by the OS between the ACK and the DB commit, the ring will have advanced its resume pointer, resulting in permanent data loss. This violates the hard invariant: 'Data-durability ordering in BLE sync. Decoded rows plus the cursor must be committed in one transaction BEFORE the [ACK]'.

yield SampleBatch(const [], raw: [bytes]);
if (ringConnIsBulk(f.respid)) {
  final parsed = parseRingConnBulkPage(f);
  // A page that will not slice cleanly is treated the same as the last
  // page of a burst: there is nothing safe left to do with it, and the
  // bytes are archived above either way.
  if (parsed == null || parsed.remaining == 0) break;
  final ack = f.respid == kRingConnRespBulkPpg
      ? ringConnCmdAckPpg()
      : ringConnCmdAckActivity();
  if (!await link.write(kRingConnCommandChar, ack)) {

a burst that never signals done and just runs out the 512-page cap
fell through with no log line, unlike every other early exit here.
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

No code suggestions found for the PR.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b149aa2

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Yield raw frames before ACKing to prevent data loss

To uphold the "Commit before ACK" invariant, yield the accumulated raw frames before
sending the ACK. This ensures the host commits the data to the database before the
ring advances its cursor, preventing data loss if the connection drops immediately
after the ACK.

lib/ble/adapters/ringconn.dart [231-240]

         if (parsed == null || parsed.remaining == 0) break;
+
+        if (raw.isNotEmpty) {
+          yield SampleBatch(const [], raw: List.of(raw));
+          raw.clear();
+        }
+
         final ack = f.respid == kRingConnRespBulkPpg
             ? ringConnCmdAckPpg()
             : ringConnCmdAckActivity();
         if (!await link.write(kRingConnCommandChar, ack)) {
           link.log('ringconn: page ack refused on channel $channel; ending '
               'its drain.');
           break;
         }
         continue;
Suggestion importance[1-10]: 6

__

Why: Yielding the accumulated frames before sending the ACK is a good practice to prevent data loss if the app crashes or the connection drops immediately after the ACK (assuming the consumer commits to the DB upon receiving the batch). Note that applying this change will require updating test/adapters/ringconn_adapter_test.dart, which currently expects exactly 3 batches.

Low

Wire the ring into the devices screen's manual sync affordance (was
hardcoded to Oura only) and repin openstrap_protocol to feat/ringconn's
head so the branch actually compiles in CI instead of only through a
gitignored local override.

Yield each drained frame to the host as it arrives instead of
accumulating a whole channel's burst for one yield at the end, so a
page reaches the host's buffer before the ack that advances the
ring's own resume pointer, not after the whole burst finishes. Drop
anything left over in the inbox between the two channels so a
straggler from the first can't get mistaken for the second's
sync-open reply.
@abdulsaheel

Copy link
Copy Markdown
Collaborator Author

Triaging the reviewer-guide findings from the PR-Agent pass (issue comments, no comment_id to thread a reply on):

Missing Pin Update — confirmed, and it was breaking CI (dart analyze failing on every RingConn symbol). Repinned openstrap_protocol in pubspec.yaml to feat/ringconn's own head (b819ee0) and moved kProtocolPin in derivation_engine.dart to match — pubspec.lock re-resolved against the git ref, not the local override. Fixed in e274793.

Data Loss Window — confirmed. _drainChannel now yields each frame to the host the moment it arrives, before the ack that advances the ring's own resume pointer, instead of accumulating a whole burst for one yield at the end. Fixed in e274793 (test/adapters/ringconn_adapter_test.dart updated for the new per-frame batch counts).

State Machine Desync — confirmed as a real race, but I didn't take the suggested payload[1] == channel check: nothing in the protocol package documents what that byte actually carries, and guessing its meaning is exactly the kind of unverified per-field assumption this file's own header refuses to make elsewhere. Instead, anything left in the inbox from the sleep channel is drained (and still archived) before the awake channel's sync-open starts waiting, so a straggler can't be picked up as the wrong channel's reply. Fixed in e274793.

Also replied inline on the 512-page-cap comment (declined, with reason).

A pub get run with pubspec_overrides.yaml present after the repin
rewrote the lock back to the local worktree path, which is exactly
what the CI guard script exists to catch.
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit e274793

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 02ac02e

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

1 similar comment
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

The page ack is what advances the ring's own resume pointer, and until
now the adapter sent it directly after yielding the page — the yield
only buffers the raw bytes in the host's memory, durable commit is a
separate periodic flush. A crash between the two would have let the
ring move past a page nothing ever wrote to disk. Route the ack through
OffloadCheckpoint's confirm callback, same contract oura.dart already
uses, so the host commits before the ack goes out.
@abdulsaheel

Copy link
Copy Markdown
Collaborator Author

Data-durability ordering finding — confirmed, fixed in 9d479ab. The previous round's fix (yield each page before its ack, not accumulated for the whole burst) only got the raw bytes into the host's in-memory buffer; the actual DB commit was still on the periodic flush timer, so a crash between the ack and that flush could let the ring's resume pointer move past a page nothing had written to disk yet. Routed the ack through OffloadCheckpoint's confirm callback (same contract oura.dart already uses for its own cursor) so the host commits durably before the physical ack goes out. Updated the adapter test's drive loop to confirm checkpoints the way BandHost does; all 9 adapter tests plus band_registry_test.dart and adapter_signals_registry_test.dart still pass, flutter analyze clean.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

@abdulsaheel
abdulsaheel merged commit 7752fc0 into main Sep 5, 2026
3 of 4 checks passed
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Failed to generate code suggestions for PR

@abdulsaheel
abdulsaheel deleted the feat/ringconn branch September 18, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant