Skip to content

docs(oura): record BLE learnings update - #488

Closed
pipiche38 wants to merge 7 commits into
ryanbr:mainfrom
pipiche38:oura-protocol-doc-updates
Closed

pipiche38 wants to merge 7 commits into
ryanbr:mainfrom
pipiche38:oura-protocol-doc-updates

Conversation

@pipiche38

Copy link
Copy Markdown

docs/OURA_PROTOCOL.md

  • §6.4 (0x80 green-IBI) — IBI→HR (60000/ibi); open_oura feeds this record's hr_bpm into its activity model (HR comes from 0x80, not 0x50); first daytime sample sparse + ~15% artifact = quality-subset, overnight density still open. Tier-B, never scored.
  • §6.13 (0x50 activity_info) — cross-device validation: MET tracks intensity (3 walks ~4 MET vs 0.9 sleep floor, r=0.89 vs Suunto .fit speed), underreads water, sparse ring-side cadence; NOOP has no MET slot → stays diagnostic JSONL.
  • New citation key [open_oura-act] for activity_model.rs (I'd used a bare [open_oura] which wasn't a defined key — now fixed, matches the doc's citation rigor).

docs/DEVICE_SUPPORT_ROADMAP.md

pipiche38 and others added 3 commits July 15, 2026 14:35
…map status

Fold this session's Oura findings into the two docs (docs-only, no code):

OURA_PROTOCOL.md
- §6.4 (0x80 green-IBI): IBI→HR (60000/ibi); open_oura feeds this record's hr_bpm
  into its activity model (HR comes from 0x80, NOT 0x50). First live daytime sample
  sparse + ~15% artifact — a quality-sampled subset, not a full beat record; overnight
  density is the open question. Tier-B research sidecar, never scored.
- §6.13 (0x50 activity_info): cross-device validation — MET tracks intensity (3 walks
  ~4 MET vs 0.9 sleep floor; r=0.89 vs a Suunto .fit speed profile), underreads water,
  sparse ring-side cadence (~86%). NOOP has no MET slot → stays diagnostic JSONL.
- New citation key [open_oura-act] for activity_model.rs (met←0x50, motion←0x47,
  temp←0x46, hr_bpm←0x80).

DEVICE_SUPPORT_ROADMAP.md
- Oura row + a new "Oura Ring — BLE (experimental)" section: what works, sleep
  hypnogram persist (ryanbr#446, display-only, main-night gate open), MET corpus (ryanbr#447),
  banked-IBI→HR research, and the diagnostics tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6
The recorded §6.4 (0x80) note predated the decode-bug discovery: it documented
the WRONG little-endian bit layout and drew a now-falsified conclusion ("sparse
quality-subset, overnight density the open question"). Both were artifacts of
the decoder reading the ring's high-byte-first 11-bit IBI as a masked LE u16.

- Layout corrected to high-byte-first `(b1&7)|(b0<<3)`, `quality==1` gate —
  identical to the fix in the oura-ibi-decode-fix PR, so the two edits merge
  trivially regardless of land order (§6.1/0x60 there owns the twin fix).
- Research note updated with the resolved result: the earlier daytime "~15%
  impossible-HR" reading was largely the decode bug, not a ring limitation; the
  corrected decode gives a usable 94%-coverage overnight HR that tracks the
  hypnogram, and a sparser-but-clean, effort-tracking daytime HR. Still Tier-B,
  never scored; restingHr/avgHrv promotion gated on multi-night validation.

Suggested land order: oura-ibi-decode-fix first, then rebase this.

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

Copy link
Copy Markdown
Author

Just update the OURA_PROTOCOL section 6.4 in accordance of today's finding around #511

pipiche38 and others added 4 commits July 16, 2026 14:50
The Test Centre report bundle gathered report.txt + raw-capture.jsonl (the
WHOOP frame capture) + meta.json, but never the Oura ring's Tier-B JSONL
sidecars (oura-raw / oura-ibihr / oura-activity), so a maintainer could only
get the redacted log lines, not the raw corpora needed to reframe/validate.

Attach them WHEN PRESENT (mirroring raw-capture.jsonl), not behind a test
domain: they cut across Sleep / HRV / Connection / Sources, and file presence
is the honest gate (no ring used -> no files -> nothing attached, so an
Experimental ring needs no separate brand check). The ring UUID inside each
line is masked to <device> by the existing redactEntries pass; entry names are
normalized (id dropped) since redaction never touches names.

- ouraDiagnosticEntries()/normalizedOuraEntryName(): enumerate
  <App Support>/OpenWhoop/Diagnostics for oura-{raw,ibihr,activity}-*.jsonl,
  map to id-free names, keep the largest file per name (read-only; never
  creates the dir so a bundle build has no side effects).
- capEntries(): generalize the raw-capture-only tail-trim to a trimmableNames
  set and split the budget across trimmable files in proportion to size. With a
  single trimmable file this is byte-identical to the prior behaviour (its share
  is the whole remainder), so the existing cap test still passes.
- whoop.sqlite is deliberately NOT attached (large, binary, unredactable;
  meta.json already reports db_bytes + row counts).

App-target Swift (CI doesn't cover it): built Strand (macOS) + NOOPiOS, and
TestBundleAssemblerTests green 8/8 (5 existing + 3 new).

Verified on test/oura-persist-activity-combined; to be retrofitted onto the
per-feature diagnostics branches (oura-sleep-stage-timeline-persist, the
activity/ibihr branches).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bj38eZ4JMejA813XhYvMk6
…t (fixes hang)

The review-before-share sheet renders every text bundle entry as a single
SwiftUI Text. When a large research stream is attached — the ~12 MB Oura
oura-raw.jsonl sidecar, or the WHOOP raw-capture — SwiftUI/CoreText tries to lay
out megabytes of text as one Text, allocating >1 GB of glyph storage and pinning
the main thread indefinitely, so generating a report bundle appears to hang.

Diagnosed from a main-thread sample: 100% of samples in
SheetContentRoot.sizeThatFits -> StyledTextLayoutEngine.sizeThatFits ->
CTLineCreateWithAttributedString -> TGlyphEncoder::EncodeChars, at a 1.1 GB
footprint.

ReportReviewGate.previewText now excludes the large raw streams by name
(screenshot.png + raw-capture.jsonl + the oura-{raw,ibihr,activity}.jsonl
sidecars) AND anything over a 1 MiB per-entry guard, so any oversized stream
(a future one, or a pathologically large report.txt) is named in the "attached"
note rather than laid out. report.txt / meta.json / last-crash.txt still show in
full, so the review still shows exactly the report body the user shares; the
excluded streams stay in the .zip and are named so the review stays honest.

Self-contained (no dependency on the Test Centre assembler), so it applies to
main and every feature branch that attaches diagnostics. App-target Swift (CI
doesn't cover it): built Strand (macOS), ReportReviewGateTests 4/4.

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

Copy link
Copy Markdown
Author

Supersedes by #678

@pipiche38 pipiche38 closed this Jul 20, 2026
pipiche38 added a commit to pipiche38/noop that referenced this pull request Jul 21, 2026
…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
@pipiche38
pipiche38 deleted the oura-protocol-doc-updates branch July 22, 2026 18:03
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…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
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