fix(oura): do not arm daytime HR on a connect made while live HR is suspended - #2412
Merged
ryanbr merged 1 commit intoSep 23, 2026
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #2275, rebased onto current
mainas a single commit. The patch is the one reviewed there at6497e7068— 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, andstartReengageTimer()'s suspended guard sentdhr_disable/dhr_unsubscribeone second later. The ring logsDHR_mode:3→DHR_mode:0on every overnight reconnect. The Oura app never runs live mode during a sync.This PR adds
OuraDriver.liveHRWanted(defaulttrue, both platforms). Whenfalse, 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. iOSOuraLiveSourceclears it at the auth-OK step whenliveHRSuspended, logs which path it took (auth OK - live HR suspended (screen off), daytime HR left untouched), and gatesdisableLiveHR()on a newliveHRArmedThisSessionso a suspended connect does not follow the skipped enable with a gratuitousmode 0x00write. 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.kthas 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 (
notificationMaskinit-time vsliveHRWantedmutable):liveHRSuspendedcan 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 intonextStep(.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's0x76 bedtime_periodrecord stamps the connect second; its0x43log printsSpO2_HFM;0at 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) andtestLiveHRWantedDefaultsToArmingTheTriplet(default path byte-for-byte the existingtestFullEnableSequence).swift teston the rebased head: 254/0.OuraDriverTest—./gradlew testFullDebugUnitTest --tests com.noop.oura.OuraDriverTest: 52/0.Strand(macOS) andNOOPiOS(generic/platform=iOS), bothBUILD SUCCEEDED.doc_comment_lintclean.dhr_enableafter it; the ring's own0x43log on those seconds holds onlycheck_sleep—DHR_mode:30/34, against the pre-fixDHR_state:4 → DHR_mode:3 → … → DHR unsubsequence 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).dhr_enable0 between 22:15 and 07:50, hypnogram complete (comment 5756852576).Refs #2075