Skip to content

Restore Oura Ring 4 connection, battery, HRV comparison and sleep - #2

Merged
llmassisted merged 7 commits into
mainfrom
fix/restore-oura-enhancements
Sep 27, 2026
Merged

llmassisted merged 7 commits into
mainfrom
fix/restore-oura-enhancements

Conversation

@llmassisted

@llmassisted llmassisted commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

What this PR does

Restores the Android Oura Ring 4 integration that was present in the previously shipped fork APK but absent from the upstream-based branch merged in #1. The missing commits removed the native-versus-NOOP HRV comparison, rotating-address connection recovery, paced Bluetooth writes, battery-response handling, and sleep-window fallbacks.

This merges oura-ring-enhancements into current main and resolves the overlaps with the newer upstream implementation:

  • Restore the ring's battery reply decoding and paced/retried GATT command queue; retain the newer authentication watchdog, timestamp-anchor handling, and serial redaction.
  • Rediscover the paired ring's current privacy address on reconnect, preserve stored credentials, route Settings reconnect to Oura, and request Bluetooth permissions from the ring's Live controls.
  • Restore native Oura RMSSD beside NOOP-calculated HRV, including the separately labelled resting-window comparison. Native values do not replace NOOP's canonical HRV/Charge inputs.
  • Restore Ring 4 native-resting and manually marked sleep windows, while retaining upstream HR-only detection and WHOOP source isolation. Accept an active Oura hypnogram even when its owner equals the imported namespace.
  • Follow the active serial identity in Health data flows, restore ring-specific history syncing and foreground-only live HR demand, and change the analysis watermark so existing stored data is re-scored.
  • Increment Android versionCode to 538. Staging package and signing identity remain unchanged.

Added: Test Centre SpO2 feature-mode write (Android)

Ports the Swift Test Centre feature-mode experiment (ryanbr#2105, OURA_PROTOCOL.md §7.5) to Android, scoped to SpO2: Enable writes mode automatic (2f 03 22 04 01), Disable writes mode off. Each is behind a confirmation, is only sent while the ring is idle-streaming, is followed by a status re-read, and is never sent automatically. Beyond the Swift version:

  • The ring's 2f 03 23 <id> <status> reply for any feature other than daytime HR is routed as a verdict (accepted / NOT_SUPPORTED / NOT_AVAILABLE) and logged, instead of being handed to the driver as a no-op live-HR ack. Live-HR triplet behaviour is unchanged (pinned by tests).
  • The first raw 0x8b spo2_r_pi payloads per session are logged as fixtures, since a Ring 4 is expected to emit that tag once SpO2 runs and NOOP has never received it.

Platform scope: this restores previously shipped Android-only fork behavior. Swift is unchanged; the Oura-specific window/UI additions are not presented as cross-platform parity work. No schema migration or deletion of stored health data is introduced.

Added: live HR lifecycle and HRV comparison fixes (04cd3af)

  • Live HR demand now follows the main Activity's own lifecycle, so a cold launch (observer attached after resume) requests ring HR immediately.
  • The NOOP-side HRV fallback reads R-R per resting window, so older nights can no longer consume the read cap for recent ones.

Added: Oura activity estimate from the ring's MET stream (b44d0a9)

  • Each anchored 0x50 record is persisted as an OURA_MET event row (met_x10 exact tenths, sec_per_sample, state); previously it was only logged, and the ring trims its copy after each drain. Never scored, never a step count.
  • The pre-existing on-device activity sidecar (oura-activity-*.jsonl, written since Oura: capture Tier-B activity MET stream to a JSONL research corpus (Swift + Android) ryanbr/noop#676) is imported once into the same rows; the (deviceId, ts, kind) PK dedups it against live writes.
  • Kotlin port of the Swift OuraActivityEstimator; byte-identity pinned by an oracle test against the Swift source compiled standalone over 300 inputs.
  • Health screen card (only when OURA_MET rows exist): today's active minutes (≥ 3 MET), MET-minutes and estimated above-resting kcal, plus the last 7 days, labelled as an estimate. Within-record sample order is unpinned and documented (±~13 min).

Type of change

  • Bug fix

How it was tested

  • Both Android debug and release Kotlin compilation passed.
  • Full local Android suite: 6,445 tests ran. The four new Oura restoration scenarios passed. One source-layout assertion was subsequently replaced with a production-selector behavior test; two unchanged RecoveryDriversTest exact floating-point assertions fail on this Mac/JBR 21 runtime (-0.4999999999999929 versus -0.5).
  • Final focused Oura tests and signed staging packaging are running. GitHub's Linux/JDK 17 full suite will verify the final commit.
  • Source-hygiene gate and protocol documentation checks passed.
  • New OuraSleepRestorationTest covers native-resting sleep and vitals without gravity, active-owner saved hypnograms, manual-window precedence/separate naps, and rejection of short resting windows as automatic sleep.
  • Restored tests cover native HRV aggregation, active serial-id flows, GATT queue retries/pacing, stored-key selection, and ring generation/identity.

Hardware validation (Oura Ring 4, ORE_06, builds 535/536):

  • Commands are accepted by Android and the ring; battery reads 100%; a full history drain completed (bytes_left 0) and 14 of 20 nights now score (was 4).
  • The resting-HR automatic restore is accepted at connect (feature-mode reply feature=0x8 status=0 (accepted)), and daytime live HR is paused when no live screen is visible (dhr_disable).
  • SpO2 write: on a ring reporting SpO2 mode=0 (off) (server-gated, never cloud-enabled), 2f 03 22 04 01 returned status=0 (accepted) and the re-read showed mode=1 (automatic); the mode persisted across a force-stop and fresh connection. Whether SpO2 records then arrive (and as 0x6F or 0x8b) awaits an overnight capture.

Earlier: the supplied diagnostic log was inspected. It records failed writes, repeated saved-address connection failures, and stored HR/R-R streams rejected as NO-NIGHT with no motion. No physical-ring test has been performed for this branch; pairing, battery refresh, overnight collection, and the restored screens still need on-device confirmation. No raw diagnostic log or personal health data is committed.

Related issues

Refs #1

llmassisted and others added 4 commits August 25, 2026 17:41
…rity

- Move the 13 hardcoded Oura/NOOP HRV comparison strings in HealthScreen
  into string resources for all shipped locales; reuse existing keys for
  "ms", "Date", "Oura" and "Oura ring".
- Drop OuraRestingWindows.sleepCandidates(); callers use
  windows().filter(isSleepCandidate).
- Add platform_specific parity dispositions for the Android-only Oura
  restoration and refresh the twin map.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@llmassisted
llmassisted marked this pull request as ready for review September 26, 2026 18:48
llmassisted and others added 3 commits September 26, 2026 15:41
Port the Swift Test Centre feature-mode experiment (ryanbr#2105, OURA_PROTOCOL.md
s7.5) to Android, scoped to SpO2: Enable writes mode automatic, Disable
writes mode off, each behind a confirmation and never sent automatically.
The write is only sent while the ring is idle-streaming and is followed by a
status re-read.

Beyond the Swift version:
- Route the ring's 2f 03 23 reply for any feature other than daytime HR as a
  verdict (accepted / NOT_SUPPORTED / NOT_AVAILABLE) and log it, instead of
  handing the driver a no-op live-HR ack. Outside EnablingLiveHR the triplet
  behaviour is unchanged.
- Log the first raw 0x8b spo2_r_pi payloads per session, the tag a Ring 4 is
  expected to emit once SpO2 runs, as fixtures for a future decoder.

Strings in all shipped locales; platform_specific parity dispositions for the
Android-only declarations; versionCode 536.

Refs ryanbr#2105

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Synchronize live HR demand with the current Activity lifecycle, including an already-resumed cold launch. Read fallback HRV beats per resting window so older history cannot consume the cap for recent nights.

Add lifecycle and multi-night HRV regression coverage. Validation: 54 focused JVM tests, source hygiene, protocol documentation checks, and i18n audit passed.
The 0x50 activity/MET stream was decoded and logged but never stored, and
the ring trims its copy after each acknowledged drain. On Android:

- Persist each anchored 0x50 record as an OURA_MET event row (met_x10 exact
  tenths, sec_per_sample, state), the same event-table path as OURA_MOTION.
  Never scored, never a step count.
- Import the pre-existing on-device activity sidecar
  (oura-activity-*.jsonl, written since ryanbr#676) once into the same rows; the
  (deviceId, ts, kind) PK dedups it against live writes. Waits for the ring's
  stable serial id.
- Port the Swift OuraActivityEstimator; byte-identity is pinned by an oracle
  test against the Swift source compiled standalone over 300 inputs.
- Health screen card (only when OURA_MET rows exist): today's active
  minutes (>= 3 MET), MET-minutes and estimated above-resting kcal from the
  profile weight, plus the last 7 days, labelled as an estimate.

Within-record sample order is not pinned (documented); a sample's time is
good to within its record's ~13 min span.

Strings in all shipped locales; platform_specific parity dispositions;
versionCode 538.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@llmassisted
llmassisted merged commit 56ff5f3 into main Sep 27, 2026
17 checks passed
@llmassisted
llmassisted deleted the fix/restore-oura-enhancements branch September 27, 2026 06:00
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