Skip to content

docs(oura): reconcile protocol spec with decode fixes, wear/charge signal, observed tags#678

Merged
ryanbr merged 3 commits into
ryanbr:mainfrom
pipiche38:oura-protocol-docs-v2
Jul 22, 2026
Merged

docs(oura): reconcile protocol spec with decode fixes, wear/charge signal, observed tags#678
ryanbr merged 3 commits into
ryanbr:mainfrom
pipiche38:oura-protocol-docs-v2

Conversation

@pipiche38

Copy link
Copy Markdown

Summary

Brings docs/OURA_PROTOCOL.md and docs/DEVICE_SUPPORT_ROADMAP.md up to date with the Oura findings, as a clean single docs commit off main. Supersedes the entangled draft #488 (which carried
stale #570 cherry-picks main has since surpassed via #572). Docs only — no decoder or behaviour change.

Changes

  • §6.4 — IBI decode reconcile. Corrected 0x60/0x80 byte layout + quality==1 gate (matches open_oura's parse_api_green_ibi_quality_event); the IBI→HR finding (HR comes from the
    0x80/0x60 records, not 0x50; ~94% overnight coverage tracking the hypnogram; sparser-but-clean daytime HR). Still Tier‑B, never scored.
  • §6.4 — caveats. "Usable overnight HR" holds only on worn nights (a charger night banks a hypnogram + temp but no IBIs), and a banked IBI must be persisted at its anchored ring-time, not
    the drain-arrival wall-clock — otherwise overnight beats misfile to the daytime sync moment (pairs with the .ibi anchor fix, Oura: stamp banked IBIs at their real ring-time, not the drain-arrival time #677).
  • §6.15 — wear/charge signal. Documents inferring wear/charge from 0x45/0x53 (the trailing "chg. detected"/"chg. stopped" strings + live‑HR presence + a live‑HR silence watchdog), and adds
    the 0x86 aohr note: open_health documents it "appears when worn" but it never appears in a NOOP capture (code-confirmed only).
  • §8 + new §9. 0x68/0x86 are not emitted (0 of ~131k records). New "Observed-but-undecoded tags" appendix with verbatim example payloads from the Gen‑3 corpus
    (0x61/0x4a/0x72/0x6a/0x6d/0x6c/0x5b/0x79/0x76/0x5c/0x56) + shape hints — flags that 0x61 (the highest-rate tag) is not battery despite the open_oura-adjacent label.
  • DEVICE_SUPPORT_ROADMAP.md — MET validation + HR-from-0x80 status.

Verification

Docs only; renders as Markdown. Confirmed the diff vs main is a pure superset (the two - lines are in-place updates of the Oura roadmap row and one §6.4 clause — no content lost).

Supersedes #488

@ryanbr ryanbr left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed against the code — this is solid, valuable docs work, and a clean single-commit supersede of the entangled #488. Confirmed docs-only, no content lost: both - lines are in-place updates (the roadmap Oura row and one §6.4 clause); everything else is additive. Spot-checked the tags it asserts against the decoders — temp 0x46, HRV 0x5D, activityInfo 0x50, IBI 0x80/0x60, the 0x50 two-slope MET formula, OuraDecoders.decodeActivityInfo → OuraEvent.activityInfo, and the sibling-anchoring claim — all match.

Three small accuracy nits, none blocking, but worth fixing since a spec doc's value is precision.

1. 0x12/0x13 mislabeled as the UTC anchor (contradicts §6.11)

The new roadmap line reads "SyncTime (0x12/0x13) UTC anchoring." But 0x12/0x13 is the app→ring time-SET command/response (Commands.syncTime, 12 09 …). NOOP's actual UTC anchor is the ring-emitted 0x42 time-sync event (+ 0x85 RTC fallback) — which this same document already states in §6.11 ("the primary UTC anchor") and §5.5, and which the driver keys on (OuraDriver .timeSyncunixSeconds(forRingTimestamp:)). As written the roadmap contradicts its own §6.11.

Suggest: SyncTime (0x12/0x13) handshake + 0x42 time-sync event as the UTC anchor.

2. §6.4 caveat is past-tense for an unmerged fix

The §6.4 text — "…the datastore's rrInterval was not until .ibi was anchored like its sibling banked streams" — describes #677 as already done, but #677 isn't merged yet (I've left a change request on it). Merging these docs first documents behaviour main doesn't have.

Cleanest is to land #677 first, then this — or soften to "pairs with #677 (pending)." (The PR body's phrasing is already honest about the pairing.)

3. Optional — §9 appendix RE-usability

The observed-tags table gives one header-stripped example payload per tag. We got bitten once (noop #123) by a truncated prefix that hid tail fields, so for the higher-rate tags (0x61, 0x72, 0x6d) the full type/len/rt record + 2–3 samples each would be more useful than a single stripped example. You already do exactly this for 0x76 and note the full hex is in the capture — just extend that to the frequent tags. Enhancement, not a blocker.

Happy to sequence this right after #677 lands so both Oura changes go in together — and I can push the nit-1/nit-2 wording fixes myself if you'd prefer.

pipiche38 added a commit to pipiche38/noop that referenced this pull request Jul 20, 2026
…tense, §9 full records)

- Roadmap: correct "SyncTime (0x12/0x13) UTC anchoring" — 0x12/0x13 is the
  app→ring time-SET handshake; the actual UTC anchor is the ring-emitted 0x42
  time-sync event (§6.11). Fixes the self-contradiction the reviewer flagged.
- §6.4: reword the rrInterval caveat to present tense + "PR ryanbr#677 (pending merge)";
  the docs no longer describe an unmerged fix as already landed on main.
- §9: reproduce full type/len/rt records + 2–3 samples for the high-rate tags
  0x61/0x72/0x6d (like 0x76) so future RE sees framing and cross-sample variation
  directly; note 0x61's payload length is not fixed (len byte moves 0x10/0x11/0x12).

Docs-only. Full records extracted verbatim from the NOOP Gen-3 raw capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi
@pipiche38

Copy link
Copy Markdown
Author

Thanks — all three addressed in dc4f7ca (docs-only):

  1. Fixed the 0x12/0x13 roadmap line — now "SyncTime (0x12/0x13) handshake plus the ring-emitted 0x42 time-sync event as the UTC anchor (§6.11)", so it no longer contradicts §6.11.

  2. §6.4 reworded to present tense + "PR Oura: stamp banked IBIs at their real ring-time, not the drain-arrival time #677 (pending merge)". Happy to sequence behind Oura: stamp banked IBIs at their real ring-time, not the drain-arrival time #677 so it reads correctly on merge.

  3. Extended §9 with full type/len/rt records + 3 samples each for 0x61/0x72/0x6d (like 0x76). The samples surfaced that 0x61's payload length isn't fixed (len moves 0x10/0x11/0x12) — called out inline.

Left it as a follow-up commit for review visibility; glad to squash into the single supersede commit on merge. Fine to land right after #677.

@pipiche38

Copy link
Copy Markdown
Author

Kindly merge with the order is still #677#678

@pipiche38

Copy link
Copy Markdown
Author

Linked to #710 What I added to docs/OURA_PROTOCOL.md

  • New §7.4 "Feature-status probe (read-only diagnostic)" — documents the shipped spo2_status / realsteps_status probes: READ verb only (2f 02 20 , never the
    set-mode write), the 5-byte 0x21 reply layout, and the key finding — all-zero mode/status/state = server-gated OFF, distinct from the active daytime-HR (mode=1
    status=0x11 state=2), so subscription==0 alone isn't the signal.
  • §7.1 SpO2 row (0x04) — upgraded the earlier "confirmed OFF" note to cite the actual 2026-07-20 Gen 3 capture and the shipped probe, with the decoded mode=0
    status=0 state=0 subscription=0.
  • §7.1 real_steps row (0x0b) — was a bare "server-flag-gated"; now carries the same hardware confirmation and ties it to why 0x7E/0x7F never appear (§6.13).

pipiche38 and others added 3 commits July 21, 2026 19:53
…e signal, and observed tags

Brings docs/OURA_PROTOCOL.md and docs/DEVICE_SUPPORT_ROADMAP.md up to date with the Oura
findings, as a clean single commit off main (supersedes the entangled draft ryanbr#488):

- §6.4: reconcile the 0x60/0x80 IBI decode with the corrected byte layout + `quality==1`
  gate (matches open_oura's parse_api_green_ibi_quality_event), and the IBI->HR research
  finding (HR from the 0x80/0x60 records, not 0x50; ~94% overnight coverage tracking the
  hypnogram, sparser clean daytime HR). Still Tier-B, never scored.
- §6.4 caveats: "usable overnight HR" holds only on WORN nights (a charger night banks a
  hypnogram + temp but no IBIs), and a banked IBI must be persisted at its own anchored
  ring-time, not the drain-arrival wall-clock, or overnight beats misfile to the daytime
  sync moment.
- §6.15: the NOOP wear/charge signal from 0x45/0x53 (trailing "chg. detected"/"stopped"
  strings + live-HR presence + a live-HR silence watchdog), and the 0x86 aohr note:
  open_health documents it "appears when worn" but it never appears in a NOOP capture.
- §8 + §9: 0x68/0x86 are not emitted (0 of ~131k records); new "Observed-but-undecoded
  tags" appendix with verbatim example payloads from the Gen-3 corpus (0x61/0x4a/0x72/
  0x6a/0x6d/0x6c/0x5b/0x79/0x76/0x5c/0x56) + shape hints - flags that 0x61 (highest-rate
  tag) is NOT battery despite the open_oura-adjacent label.
- DEVICE_SUPPORT_ROADMAP.md: MET validation + HR-from-0x80 status.

Docs only. No decoder or behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi
…tense, §9 full records)

- Roadmap: correct "SyncTime (0x12/0x13) UTC anchoring" — 0x12/0x13 is the
  app→ring time-SET handshake; the actual UTC anchor is the ring-emitted 0x42
  time-sync event (§6.11). Fixes the self-contradiction the reviewer flagged.
- §6.4: reword the rrInterval caveat to present tense + "PR ryanbr#677 (pending merge)";
  the docs no longer describe an unmerged fix as already landed on main.
- §9: reproduce full type/len/rt records + 2–3 samples for the high-rate tags
  0x61/0x72/0x6d (like 0x76) so future RE sees framing and cross-sample variation
  directly; note 0x61's payload length is not fixed (len byte moves 0x10/0x11/0x12).

Docs-only. Full records extracted verbatim from the NOOP Gen-3 raw capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DN6cU2ERWZUS9tWYCjPvi
…server-gated OFF

The read-only feature-status probe (spo2_status / realsteps_status) NOOP ships
had both SpO2 (0x04) and real_steps (0x0b) read back all-zero
(mode/status/state/subscription = 0) on a real Gen 3 ring (2026-07-20). Add a
new §7.4 documenting the probe (READ verb only, never set-mode), the all-zero =
server-gated signature vs the active daytime-HR (mode=1 status=0x11 state=2), and
tie the §7.1 SpO2 + real_steps rows to that hardware capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pipiche38
pipiche38 force-pushed the oura-protocol-docs-v2 branch from 5dd56f5 to a6bc062 Compare July 21, 2026 17:53
@ryanbr
ryanbr merged commit 918a7ea into ryanbr:main Jul 22, 2026
1 check passed
@pipiche38
pipiche38 deleted the oura-protocol-docs-v2 branch July 22, 2026 18:02
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