Skip to content

v18 @82: decode the SpO2 candidate as instrumentation (#103) - #709

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
digitalerdude:fix/103-v18-spo2-instrumentation
Jul 22, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
digitalerdude:fix/103-v18-spo2-instrumentation

Conversation

@digitalerdude

Copy link
Copy Markdown

Follows up on the latest #103 developments (2026-07-17). Instrumentation only — this does not ship an SpO2 metric.

Where #103 stands

Three things happened in the thread, in order:

  1. A decompile-sourced decode (gen5.rs, reimplemented here as a protocol fact with attribution, like the repo's other RE work) reads v18 byte @82 (= inner 74) as a strap-computed SpO2 % scalar: tri-mode — 70–100 a real %, bit-7 values (0x80/0xA0…) saturation sentinels, other sub-70 nonzero diagnostic codes; populated only during sleep.
  2. The two nights checked on the original WHOOP 5.0 raw HCI capture — where to submit? (docs reference #174, which doesn't exist) #103 capture device failed the cross-night test even with that gate applied (app 95.50→92.83 falling, gated mean 93.62→93.80 rising) — and the thread set the bar explicitly: the gate must track the app number across nights where the true value actually moves.
  3. An 8-night independent validation with real spread then met exactly that bar: corr +0.99, ~0.4 %/night error, tracks a 92 % desat and a 98 % high, and only byte 82 tracks in a 74–92 offset scan.

So the evidence is currently split across devices — the strongest yes so far, against an unexplained no on one device. Per the derived-biosignal rule (never a shipped metric on contested evidence; instrumentation first), the honest next step is to decode the candidate raw on every install so multiple devices can correlate it against the WHOOP app's own nightly SpO2 — which is what this PR does, and nothing more.

What changes

  • Both decoders (Packages/WhoopProtocol/Sources/WhoopProtocol/Interpreter.swift / android/.../protocol/HistoricalStreams.kt, byte-parity): alongside the existing raw aux_byte_82, an in-band (70–100) value now also surfaces as spo2_candidate_82 with an explicit instrumentation-only note. Sentinels, diagnostic codes and the awake 0 produce no candidate key.
  • No consumer changes: spo2Pct stays null on every WHOOP 5.0, no card, no score, no persistence-schema change. The existing never-invent-physiology guard (testHistoricalV18OpticalFieldsAreNotNamedPhysiologically, spo2_red/spo2_ir absent on v18) passes untouched.
  • Tests: new twin tests on both platforms pinning the tri-mode gating (in-band passes, 0x80/0xA0/0x20/69/101 and awake-0 do not; the raw byte stays visible in every mode). The shared decoder-oracle fixtures are unaffected (@82 = 0 on every fixture frame, verified by the byte-identical-copies drift guard).
  • docs/WHOOP5_DEEP_DATA.md: the SpO2 section no longer claims "the v18 layout carries no blood-oxygen value" — it now describes the candidate, the split evidence, and what would flip it (multi-device cross-night tracking, including on the device where the two checked nights move opposite).

Testing

  • swift test (Packages/WhoopProtocol): 308 tests, 0 failures — includes the new testHistoricalV18Spo2Candidate82TriMode
  • ./gradlew compileFullDebugKotlin testFullDebugUnitTest --tests "com.noop.protocol.*" ✓ — includes the new spo2Candidate82SurfacesOnlyInBandValues and the DecoderOracleTest drift guard
  • No BLE path, UI, or stored-value shape touched, so no app-target build or on-device run is required for this change

The ryanbr#103 thread produced a decompile-sourced decode of v18 byte @82
(= inner 74): a strap-computed SpO2 % scalar, tri-mode - 70-100 a
real %, bit-7 values saturation sentinels, other sub-70 diagnostic
codes, populated only during sleep. An 8-night independent validation
with real spread (corr +0.99, ~0.4%/night, tracks a 92% desat) meets
the cross-night bar the thread had set - but the two nights checked
on the original capture device moved OPPOSITE to the app value, so
the cross-device evidence is split and unresolved.

Per the project's derived-biosignal rule this therefore ships as
INSTRUMENTATION ONLY: both decoders surface the in-band (70-100)
value as a new `spo2_candidate_82` field beside the existing raw
`aux_byte_82`, so more devices can correlate it against the WHOOP
app's own nightly SpO2. It does not populate spo2Pct, any card, or
any score - the existing never-invent-physiology guard test
(spo2_red/spo2_ir absent on v18) still passes untouched.

Both platforms in lockstep (Interpreter.swift / HistoricalStreams.kt),
tri-mode gating pinned by new twin tests on each side; the shared
decoder-oracle fixtures are unaffected (@82 = 0 on every fixture).
docs/WHOOP5_DEEP_DATA.md updated from "no blood-oxygen value on the
wire" to the current split-evidence state and what would flip it.
@digitalerdude
digitalerdude marked this pull request as ready for review July 21, 2026 16:05
@ryanbr
ryanbr merged commit 3eab3d2 into ryanbr:main Jul 22, 2026
9 checks passed
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…trumentation

v18 @82: decode the SpO2 candidate as instrumentation (ryanbr#103)
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…trumentation

v18 @82: decode the SpO2 candidate as instrumentation (ryanbr#103)
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