Skip to content

Oura: read-only SpO2 / real-steps feature-status probe (confirm the server-flag gate) - #710

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:oura-feature-status-diag
Jul 22, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:oura-feature-status-diag

Conversation

@pipiche38

Copy link
Copy Markdown

Summary

SpO2 (0x6F…) and real_steps (0x7E/0x7F) never appear for a NOOP-only ring because they're server-flag features (health/spo2, activity/real_steps) set by Oura's cloud ClientConfiguration — the ring gatekeeps them and no local SetFeatureMode can enable them offline ([open_oura-feat]). This adds a
read-only diagnostic that has the ring confirm that from its own mouth, so a capture shows "not a bug, it's a gate" instead of just "absent".

What it does

Once HR streams, send the feature-status READ verb — the same 2f 02 20 <id> as dhr_read, never the 2f 03 22 set-mode enable — for SpO2 (0x04) and real_steps (0x0b), decode the 0x21 reply, and log once per feature: Oura: feature status SpO2 (0x04) mode=.. status=.. state=.. subscription=0 - server-gated OFF; not emitted offline

Changes (both platforms)

  • OuraProtocol: featureRealSteps + spo2ReadStatus()/realStepsReadStatus(); OuraFeatureStatus + decodeFeatureStatus (nil on short body); handleSecureFrame splits 0x21 — daytime-HR read (0x02) still .enableAck (advances the live-HR triplet), a diagnostic read (0x04/0x0b) returns the new .featureStatus; an undecodable 0x21 falls back to .enableAck so the triplet never stalls.
  • App: OuraLiveSource sends the two reads once, logs once per feature.

Safety

Read-only — fits the BLE contract (no write-to-enable, no destructive command); never scored, never stored.

Verification

  • swift test OuraProtocol 117 (incl. 6 new); xcodebuild Strand macOS BUILD SUCCEEDED.
  • Android compileFullDebugKotlin OK; testFullDebugUnitTest OuraFeatureStatusTest 6/6.
  • Hardware, next connect: the two feature status … lines print the ring's own subscription bytes.

… the server-flag gate)

SpO2 (0x6F etc.) and real_steps (0x7E/0x7F) never appear for a NOOP-only ring because
they are server-flag features (`health/spo2`, `activity/real_steps`) set by Oura's cloud
ClientConfiguration - the ring gatekeeps them and no local SetFeatureMode can enable them
offline [open_oura-feat]. This adds a READ-ONLY diagnostic that has the ring confirm that
from its own mouth, so a capture shows "not a bug, it's a gate" instead of just "absent".

At connect (once HR streams), send the feature-status READ verb - the SAME `2f 02 20 <id>`
as `dhr_read`, NEVER the `2f 03 22` set-mode enable - for SpO2 (0x04) and real_steps (0x0b),
decode the `0x21` reply (feature/mode/status/state/subscription), and log once per feature:
  `Oura: feature status SpO2 (0x04) mode=.. status=.. state=.. subscription=0 - server-gated OFF`

OuraProtocol (pure, both platforms):
  - OuraCommands.featureRealSteps + spo2ReadStatus()/realStepsReadStatus() (read verb only).
  - OuraFeatureStatus + OuraDecoders.decodeFeatureStatus (nil/null on a short body).
  - OuraDriver.handleSecureFrame splits 0x21: the daytime-HR read (0x02) still returns
    .enableAck (advances the live-HR triplet), a diagnostic read (0x04/0x0b) returns the new
    .featureStatus; an undecodable 0x21 falls back to .enableAck so the triplet never stalls.

App (both platforms): OuraLiveSource sends the two reads once streaming starts and logs the
decoded status once per feature. Read-only - fits the BLE safety contract (no write-to-enable,
no destructive command); never scored, never stored.

Verified: swift test OuraProtocol 117 incl. 6 new; xcodebuild Strand macOS BUILD SUCCEEDED;
Android compileFullDebugKotlin OK + testFullDebugUnitTest OuraFeatureStatusTest 6/6.
Hardware validation (next connect): the two `feature status …` lines print the ring's own
subscription bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi
@ryanbr
ryanbr merged commit 5dad238 into ryanbr:main Jul 22, 2026
9 checks passed
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
Oura: read-only SpO2 / real-steps feature-status probe (confirm the server-flag gate)
@ryanbr ryanbr mentioned this pull request Jul 22, 2026
@pipiche38
pipiche38 deleted the oura-feature-status-diag branch July 22, 2026 18:02
ryanbr pushed a commit that referenced this pull request Jul 24, 2026
…enums in the #710 probe log (#747)

* docs(oura): correct + expand the feature-ID table and mode/subscription enums from the Ring-4 APK

Cross-checked against Defying/oura-ring4-ble (APK static-analysis + Ring-4 captures) [ring4-ble]:
- §7.1 feature IDs: CORRECTION — Ambient light is `0x0F`, not `0x10`; `0x10` is special_feature. Added
  the APK's full list: `0x0E` charging_control, `0x0F` ambient_light, `0x10` special_feature, `0x11`
  raw_data_sampler, `0x12` atlas, `0x16` long_events.
- Feature MODE enum: `0x03` is `connected_live` (an earlier draft read "requested-subscription"); added
  the SUBSCRIPTION enum (`0` off / `1` state / `2` latest / `4` feature_specific_data — NOOP subscribes
  live HR with `2`=latest) and the feature request-status codes.
- `0x03` Exercise HR: its data arrives as `0x73` ehr_trace / `0x74` ehr_acm_intensity (never observed —
  server-gated like SpO2/steps).
- §9: `0x6d` is `measurement_quality` (24-bit signed), superseding the earlier gravity/accel guess.
- Also notes the repo independently confirms the framing/auth/tags are generation-invariant (Ring 4 ==
  Ring 3) and that the app derives BPM as round(60000/ibi_ms) — the same formula #728 uses.

Facts-only clean-room citation; no code copied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* diag(oura): name the mode/subscription enums in the #710 feature-status probe log

The read-only SpO2/real_steps feature-status probe (#710, merged) logged the raw enum bytes:
`mode=0 status=0 state=0 subscription=0`. With the enum meanings now pinned (OURA_PROTOCOL.md s7.1
[ring4-ble]), name them inline so the log is self-evident:
  `mode=0 (off) status=0 state=0 subscription=0 (off) - INACTIVE (server-gated off; …)`
and the active daytime-HR reads `mode=1 (automatic)`. Mode: 0 off / 1 automatic / 2 requested /
3 connected_live. Subscription: 0 off / 1 state / 2 latest / 4 feature_data.

Log-string only, both platforms (Swift Strand/BLE + Kotlin com.noop.ble twin); no behaviour change,
no new writes to the ring. Follow-up to the merged #710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
Oura: read-only SpO2 / real-steps feature-status probe (confirm the server-flag gate)
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…enums in the ryanbr#710 probe log (ryanbr#747)

* docs(oura): correct + expand the feature-ID table and mode/subscription enums from the Ring-4 APK

Cross-checked against Defying/oura-ring4-ble (APK static-analysis + Ring-4 captures) [ring4-ble]:
- §7.1 feature IDs: CORRECTION — Ambient light is `0x0F`, not `0x10`; `0x10` is special_feature. Added
  the APK's full list: `0x0E` charging_control, `0x0F` ambient_light, `0x10` special_feature, `0x11`
  raw_data_sampler, `0x12` atlas, `0x16` long_events.
- Feature MODE enum: `0x03` is `connected_live` (an earlier draft read "requested-subscription"); added
  the SUBSCRIPTION enum (`0` off / `1` state / `2` latest / `4` feature_specific_data — NOOP subscribes
  live HR with `2`=latest) and the feature request-status codes.
- `0x03` Exercise HR: its data arrives as `0x73` ehr_trace / `0x74` ehr_acm_intensity (never observed —
  server-gated like SpO2/steps).
- §9: `0x6d` is `measurement_quality` (24-bit signed), superseding the earlier gravity/accel guess.
- Also notes the repo independently confirms the framing/auth/tags are generation-invariant (Ring 4 ==
  Ring 3) and that the app derives BPM as round(60000/ibi_ms) — the same formula ryanbr#728 uses.

Facts-only clean-room citation; no code copied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* diag(oura): name the mode/subscription enums in the ryanbr#710 feature-status probe log

The read-only SpO2/real_steps feature-status probe (ryanbr#710, merged) logged the raw enum bytes:
`mode=0 status=0 state=0 subscription=0`. With the enum meanings now pinned (OURA_PROTOCOL.md s7.1
[ring4-ble]), name them inline so the log is self-evident:
  `mode=0 (off) status=0 state=0 subscription=0 (off) - INACTIVE (server-gated off; …)`
and the active daytime-HR reads `mode=1 (automatic)`. Mode: 0 off / 1 automatic / 2 requested /
3 connected_live. Subscription: 0 off / 1 state / 2 latest / 4 feature_data.

Log-string only, both platforms (Swift Strand/BLE + Kotlin com.noop.ble twin); no behaviour change,
no new writes to the ring. Follow-up to the merged ryanbr#710.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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