Skip to content

fix(oura): do not arm daytime HR on a connect made while live HR is suspended - #2412

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:fix/oura-no-dhr-triplet-while-suspended-v2
Sep 23, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:fix/oura-no-dhr-triplet-while-suspended-v2

Conversation

@pipiche38

Copy link
Copy Markdown

Replaces #2275, rebased onto current main as a single commit. The patch is the one reviewed there at 6497e7068 — unchanged apart from the base — and #2275's thread keeps the review and the three hardware read-outs.

What

Every Oura connect ran the driver's live-HR enable triplet (dhr_read / dhr_enable / dhr_subscribe) regardless of the app's screen-off suspend, and startReengageTimer()'s suspended guard sent dhr_disable / dhr_unsubscribe one second later. The ring logs DHR_mode:3DHR_mode:0 on every overnight reconnect. The Oura app never runs live mode during a sync.

This PR adds OuraDriver.liveHRWanted (default true, both platforms). When false, auth success goes straight to .streaming — authenticated and idle, which is all the history drain, SyncTime, battery and status reads key on — and nothing is written to the daytime-HR feature. iOS OuraLiveSource clears it at the auth-OK step when liveHRSuspended, logs which path it took (auth OK - live HR suspended (screen off), daytime HR left untouched), and gates disableLiveHR() on a new liveHRArmedThisSession so a suspended connect does not follow the skipped enable with a gratuitous mode 0x00 write. A live push while suspended marks the session armed first, so the existing self-heal still sends the disable. Morning resume is unchanged: reengageLiveHR() arms the stream without the triplet's read.

Android: driver flag and tests only. OuraLiveSource.kt has no screen-off suspend (#1546), so nothing clears the flag there yet and behaviour is unchanged — the twin is the driver API, not the gate.

On the two config shapes raised in review (notificationMask init-time vs liveHRWanted mutable): liveHRSuspended can flip between the connect and the auth-OK step, and reading it at auth is what makes the log line and the write agree. A typed session config passed into nextStep(.authCompleted) would be the cleaner shape; that is its own change, not this one.

Why

On a Ring 5 overnight capture from #2075's reporter (2026-09-16, oura-raw.jsonl), the ring's own SpO2 session (0x6F, 1 Hz) was interrupted five times inside its bedtime window, for 32 / 49 / 3 / 6 / 31 min ≈ 2 h of a 9 h night. Four of the five interruptions start on the exact second of a NOOP reconnect (the ring's 0x76 bedtime_period record stamps the connect second; its 0x43 log prints SpO2_HFM;0 at both ends of each gap). Whether the ring is reacting to the daytime-HR flick or to the connection itself cannot be separated from that capture — every visit had both — so this change removes the one we control and makes the next night the measurement.

Verification

  • OuraProtocol: testAuthSuccessSkipsLiveHRTripletWhenNotWanted (no command on auth success, .streaming, a stray enable ACK is inert, the history fetch runs from there) and testLiveHRWantedDefaultsToArmingTheTriplet (default path byte-for-byte the existing testFullEnableSequence). swift test on the rebased head: 254/0.
  • Kotlin twins in OuraDriverTest./gradlew testFullDebugUnitTest --tests com.noop.oura.OuraDriverTest: 52/0.
  • App targets built locally on the rebased head (CI does not): Strand (macOS) and NOOPiOS (generic/platform=iOS), both BUILD SUCCEEDED. doc_comment_lint clean.
  • Hardware, Gen 3 (BLB_03), iOS — three nights, all on fix(oura): do not arm daytime HR on a connect made while live HR is suspended #2275:
    • 2026-09-16/17: 34 suspended connects, every one logging the new line with no dhr_enable after it; the ring's own 0x43 log on those seconds holds only check_sleepDHR_mode:3 0/34, against the pre-fix DHR_state:4 → DHR_mode:3 → … → DHR unsub sequence in the same sidecar twenty minutes earlier (fix(oura): do not arm daytime HR on a connect made while live HR is suspended #2275, comment 5710152977).
    • 2026-09-17/18: 18 overnight reconnects inside the sleep window, 0 triplets (comment 5726339240).
    • 2026-09-20/21, rebased tree: 51 suspended overnight connects, dhr_enable 0 between 22:15 and 07:50, hypnogram complete (comment 5756852576).

Refs #2075

…uspended

Every Oura connect ran the driver's live-HR enable triplet (dhr_read / dhr_enable /
dhr_subscribe) regardless of the app's screen-off suspend, and `startReengageTimer()`'s
suspended guard then sent dhr_disable / dhr_unsubscribe one second later. The ring logged
`DHR_mode:3` -> `DHR_mode:0` on every overnight reconnect. On a Ring 5 overnight capture
(issue ryanbr#2075's reporter, 2026-09-16) four of the five interruptions of the ring's own SpO2
session began on the exact second of such a reconnect (3-49 min each, about 2 h of a 9 h
night); the Oura app never runs live mode during a sync either.

`OuraDriver.liveHRWanted` (default true, both platforms): when false, auth success goes
straight to `.streaming` - authenticated and idle, which is all the history drain, SyncTime
and status reads key on - and nothing is written to the daytime-HR feature. iOS
`OuraLiveSource` clears it at the auth-OK step when `liveHRSuspended`, logs which path it
took, and gates `disableLiveHR()` on a new `liveHRArmedThisSession` so a suspended connect
does not follow the skipped enable with a gratuitous `mode 0x00` write. A live push while
suspended marks the session armed first, so the existing self-heal still sends the disable.
Morning resume is unchanged: `reengageLiveHR()` arms the stream without the triplet's read.

Android: driver flag and tests only. `OuraLiveSource.kt` has no screen-off suspend (ryanbr#1546),
so nothing clears the flag there yet and behaviour is unchanged.

Tests: OuraProtocol `testAuthSuccessSkipsLiveHRTripletWhenNotWanted` /
`testLiveHRWantedDefaultsToArmingTheTriplet` and their Kotlin twins (swift test 55/55,
gradle OuraDriverTest 51/51). App targets: `Strand` (macOS) and `NOOPiOS` built locally.
Hardware read-out owed before merge: one suspended night on a Gen 3 - `DHR_mode:3` per
overnight reconnect (1 -> 0), `0x6F` gap count inside the sleep window, morning resume.

Refs ryanbr#2075

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCBfMBAJzLsefWjb6dLSeL
@ryanbr
ryanbr merged commit e67c051 into ryanbr:main Sep 23, 2026
16 checks passed
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.

2 participants