Skip to content

docs(whoop5): SpO2 @82 promote checklist + band sleep flag note - #807

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
dnesdan:docs/whoop5-spo2-sleepflag-facts
Aug 15, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
dnesdan:docs/whoop5-spo2-sleepflag-facts

Conversation

@dnesdan

@dnesdan dnesdan commented Jul 25, 2026 •

Copy link
Copy Markdown

What this PR does

Docs-only, narrowed after review: PROTOCOL.md changes dropped (SpO₂ wire facts already landed as §10 in #824, with co-author credit). This PR only adds the still-missing promote-bar prose to docs/WHOOP5_DEEP_DATA.md.

docs/WHOOP5_DEEP_DATA.md

  • @82 validation checklist — five promote points for spo2_candidate_82 → spo2Pct (multi-device, offset specificity, incomplete-night falsification, WHOOP 5.0 raw HCI capture — where to submit? (docs reference #174, which doesn't exist) #103 contradiction, no recovery/illness gating)
  • Points the checklist at the existing validate_spo2_candidate.py harness gates (r / MAE / duty / feature_absent)
  • Band sleep flag vs hypnogram quick reference (sleep_state @81: wake / still / asleep / up — not Light/SWS/REM)
  • Cross-link to PROTOCOL.md §10 for wire facts already on main
  • How-to-help step 5 points at the checklist when reading harness PASS

No app / decode / metric behaviour changes. No model-id table.

Type of change

  • Documentation

How it was tested

  • Docs review only (no BLE / package code paths)
  • Checklist language checked against Tools/linux-capture/validate_spo2_candidate.py gates on current main
  • sleep_state nibble mapping checked against Interpreter.swift decode notes

Checklist

  • Swift / Android tests N/A (docs only)
  • No hardcoded hex / no decode changes
  • Follows CONTRIBUTING (facts only; no proprietary material)
  • Rebased on current main; no PROTOCOL.md delta

Related issues

Provenance

Clean-room docs grounded in NOOP's own decoder contracts and the existing harness. No decompiled WHOOP app/firmware code, assets, or proprietary material.

@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.

Field map checks out — all 14 offsets match our decoder. Useful addition: I got @82 wrong in a doc earlier today because this summary did not exist yet.

Three things before merge:

  1. Where do the model-id ranges come from? 0…37 = MG, 48…85 = 5.0 is not corroborated anywhere in our code, and the wording reads app-derived. If it is from your own captures, say so and it stands. If it came from a decompile we cannot take it — clean-room rule, regardless of accuracy.
  2. It conflicts now — WHOOP5_DEEP_DATA.md changed under you today. One hunk, small rebase.
  3. Add a row for dynamic_acceleration@41 — landed today, sits between @36 and @45, so the table has a hole.

Happy to carry it in with the fixes on top and you as co-author, like #808, if the fork rebase is a hassle.

@ryanbr

ryanbr commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Heads-up: the conflict is now partly my doing. #814 rewrote the @82 paragraph in WHOOP5_DEEP_DATA.md — one hunk, same paragraph you're editing. Rebase on current main and take whichever wording reads better; the facts are the same.

Two things from my review still open:

  1. Where do the model-id ranges come from? (0…37 = MG, 48…85 = 5.0). That's the only blocker — everything else here is grounded in our own decoders. Own captures → it stands. Decompile → also fine now, see below, but say so and it gets attributed.
  2. A row for dynamic_acceleration@41 in the v18 table, which landed today and sits between @36 and @45.

Also worth knowing, since it may change what you thought was submittable: #822 wrote down that decompile-sourced facts are admissible here with attribution, as unvalidated candidates. I'd told a contributor the opposite earlier and it wasn't our rule. Implementations and literals still stay out.

The rest of this is good — the "no GET_SPO2 opcode" and export-aggregation facts aren't recorded anywhere else.

ryanbr added a commit that referenced this pull request Jul 26, 2026
… for the export shape

Two claims re-checked against the tree rather than left as inherited assertions.

"There is no GET_SPO2-style opcode" was an overclaim. What we can actually say is
that our CommandNumber catalogue has 80 commands and none reads oxygen, and that
independent RE reports none. That is not proof about the strap's command space:
§6 is explicitly a "safe subset", and the 98-vs-87 battery dispute (#818, #592)
shows the map is incomplete. Restated as "nobody has found one", which still
supports the useful conclusion — hunting a missing opcode is the wrong lead.

The per-cycle export shape came from #807 and I had passed it through unverified.
Now confirmed from our own code and cited: bloodOxygenPct is read on the
physiological-cycles row beside recovery_score_pct and day_strain, keyed on
cycleStart/cycleEnd (WhoopExportImporter.swift:272). So "one value per recovery
cycle" is ours to assert, not a claim borrowed on trust.
ryanbr added a commit that referenced this pull request Jul 26, 2026
Final pass over the two sections, checking each assertion rather than the ones I
happened to doubt.

Verified and now cited: the DIS characteristics are ours — BLEManager.swift:473-474
declares disSerialChar 2A25 "Serial Number String" and disHwRevChar 2A27 "Hardware
Revision String". And the sleep-gating claim has in-tree corroboration: our own
decode observes aux_byte_82 nonzero only while the band sleep flag reads asleep, so
the doc cites that instead of asserting it bare.

Marked, because it cannot be verified here: "naps and incomplete nights carry no
SpO2 in the export" is a contributor observation from #807 about WHOOP's export
behaviour, not something this repo can check from the wire. Kept because "my SpO2 is
missing" otherwise reads as a decode failure, but labelled so nobody later cites it
back as established.

Every claim in these sections is now either checked against this tree or explicitly
attributed as unverified.
ryanbr added a commit that referenced this pull request Jul 26, 2026
…ty signals (#824)

Re-implements the landable parts of #807 (@dnesdan) against current main. Their PR
picked up a conflict when #814 rewrote the same @82 paragraph; writing fresh avoided
moving a bigger conflict onto them.

KEPT, AND THEIRS — the SpO2 protocol facts, recorded nowhere else:

  - No SpO2 read opcode is known. Our CommandNumber catalogue carries 80 commands
    and none reads oxygen.
  - Computed on-device during sleep — our own decode corroborates it, aux_byte_82
    is nonzero only while the band sleep flag reads asleep.
  - The CSV blood_oxygen_pct is a per-cycle aggregate, read on the
    physiological-cycles row beside recovery_score_pct and day_strain
    (WhoopExportImporter.swift:272), so it will not equal a mean of wire samples.
  - A night with no export value is a real gap, not a decode failure.

DROPPED — their model-id range table (0…37 = MG, 48…85 = 5.0).

Not for provenance: #822 settled today that decompile-sourced facts are admissible
with attribution, so the original objection no longer applies. Dropped because we
already resolve identity a better way and use model-ids nowhere. Whoop5Variant reads
the DIS serial prefix (5AM/5AG) and the WG50 hardware-revision token — shipped in
#781, sourced from #520 — and that is documented here from our own code, including
that contradictory signals return .unknown and that .unknown is not MG.

CHANGED — the v18 field map is a pointer, not a copy. Their §8.1 duplicated the
authoritative table in BLE_REVERSE_ENGINEERING.md, and drift had already started:
theirs predates the dynamic_acceleration@41 row added earlier today. One table, one
place to keep correct.

FOUR REVIEW PASSES, ALL FINDING MY OWN MISTAKES

  - Wrong heading level and wrong parent: ## 7.9/7.10 under a section whose
    subsections are ### 7.1..7.5, and neither topic belongs under "Historical-data
    offload". Moved to top-level 9 and 10, File map renumbered to 11 after checking
    nothing cross-references §8/§9 and that the file has no TOC.
  - Overclaimed negative: "there is no GET_SPO2 opcode" restated as "none is known".
    §6 is explicitly a safe SUBSET and the 98-vs-87 dispute shows the map is
    incomplete; the catalogue is evidence, not proof about the strap.
  - Two inherited claims passed through unverified, now sourced: the per-cycle
    export shape (cited to our importer) and the DIS characteristics (cited to
    BLEManager.swift:473-474).
  - One claim that cannot be checked here — naps/incomplete nights carrying no
    export SpO2 — kept but explicitly marked a contributor observation, so it is not
    later cited back as established.

Every claim in these sections is now either checked against this tree or labelled
unverified. Docs only.

Co-authored-by: Dan Urbanek <abc@dnesdan.cz>
@ryanbr

ryanbr commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Updating the ask — this got smaller, and the provenance question I was blocking on is now moot.

#824 landed the PROTOCOL.md half. The SpO₂ protocol facts are in as §10, credited to you as co-author. So that part of this PR is redundant now, and with it the model-id range table: I dropped it there because Whoop5Variant already resolves 5.0-vs-MG from the DIS serial prefix (5AM/5AG) and the WG50 hardware token, and nothing in NOOP uses model-ids. That means you no longer need to answer where those numbers came from — the table isn't needed either way. (For the record, #822 has since written down that decompile-sourced facts are fine here with attribution, so my original objection was wrong anyway.)

What's still worth landing is the WHOOP5_DEEP_DATA.md half, and I checked — none of it is on main:

  • The @82 validation checklist. This is the one I'd most like. tools: SpO2 @82 multi-device validation harness (#103) — with the fixes it needs to run #814 shipped validate_spo2_candidate.py to measure the promotion bar while the bar itself is still one sentence in prose. Your five points make it explicit — and point 3 ("when the export omits SpO₂, the wire candidate should be empty or out-of-band, not invent a number") is a real falsification test nobody has written down.
  • Band sleep flag vs hypnogram quick reference.
  • "What the strap actually does" / "Wire status today" — partly overlapping §10 now, your call whether to keep, trim or cross-link.

Two mechanical things: rebase on current main (#814 rewrote the @82 paragraph, which is where your conflict comes from), and the how-to-help line points at Tools/linux-capture/correlate_ground_truth.py — that moved to lowercase tools/ in #814, since the two case-variant directories broke every import on Linux.

Drop the PROTOCOL.md changes entirely and this becomes a much smaller PR than the one you opened. Happy to take it from here instead if you'd rather not juggle the rebase — but the checklist is your work and I'd rather it landed as such.

@dnesdan
dnesdan force-pushed the docs/whoop5-spo2-sleepflag-facts branch from a2d5e50 to 70cbd64 Compare August 10, 2026 08:19
@dnesdan

dnesdan commented Aug 10, 2026

Copy link
Copy Markdown
Author

Sorry for the silence — I was on holiday and couldn't respond to review.

Provenance (clean-room)

Nothing in this contribution was taken from decompiled WHOOP app/firmware sources, proprietary assets, or vendor code. The remaining content is grounded in:

No decompiled implementations or literals were copied into the PR.

Addressing the updated ask

Thanks for narrowing this — and for #824 / #814 carrying the bulk forward (including co-author credit on the PROTOCOL half). I've rebased on current main and dropped all PROTOCOL.md changes (model-id table included — moot with Whoop5Variant / DIS serial as you noted).

What's left in this PR (single file: docs/WHOOP5_DEEP_DATA.md):

  1. @82 validation checklist (the five promote points) — including incomplete-night falsification and "no recovery/illness gating until the bar clears"
  2. Band sleep flag vs hypnogram quick reference (sleep_state @81)
  3. Short cross-link to PROTOCOL §10 so we don't re-litigate wire facts that already landed
  4. How-to-help points at the existing harness + checklist

Happy for any further wording polish on your side — the checklist is the piece that still wasn't on main.

Narrow the remaining #807 content after PROTOCOL.md §10 landed elsewhere:
- explicit five-point promote bar for spo2_candidate_82 (incl. incomplete-night
  falsification and no recovery/illness gating)
- band sleep_state @81 vs hypnogram quick reference
- cross-link to PROTOCOL §10 and the existing validate_spo2_candidate harness

No PROTOCOL.md changes; no model-id table; no app/decode behaviour change.
@dnesdan
dnesdan force-pushed the docs/whoop5-spo2-sleepflag-facts branch from 70cbd64 to d4e611d Compare August 10, 2026 08:22
@dnesdan dnesdan changed the title docs(whoop5): SpO2 sleep path, SLEEPFLAG, skin-temp wire scale docs(whoop5): SpO2 @82 promote checklist + band sleep flag note Aug 10, 2026
@dnesdan

dnesdan commented Aug 10, 2026

Copy link
Copy Markdown
Author

Follow-up after holiday silence:

  • Rebased onto current main (single commit, docs-only).
  • Dropped all PROTOCOL.md / model-id content as requested (docs(whoop5): record the SpO2 protocol facts and the 5.0-vs-MG identity signals #824 already covers the wire half).
  • Title + description updated to match the narrowed scope.
  • Checklist text aligned with the live validate_spo2_candidate.py gates on main (r/MAE/specificity/variance/duty/feature_absent).
  • Still clean-room: no decompiled WHOOP sources in this PR.

Ready for another look whenever you have time.

@ryanbr
ryanbr merged commit 1fab8f2 into ryanbr:main Aug 15, 2026
2 checks passed
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…ty signals (ryanbr#824)

Re-implements the landable parts of ryanbr#807 (@dnesdan) against current main. Their PR
picked up a conflict when ryanbr#814 rewrote the same @82 paragraph; writing fresh avoided
moving a bigger conflict onto them.

KEPT, AND THEIRS — the SpO2 protocol facts, recorded nowhere else:

  - No SpO2 read opcode is known. Our CommandNumber catalogue carries 80 commands
    and none reads oxygen.
  - Computed on-device during sleep — our own decode corroborates it, aux_byte_82
    is nonzero only while the band sleep flag reads asleep.
  - The CSV blood_oxygen_pct is a per-cycle aggregate, read on the
    physiological-cycles row beside recovery_score_pct and day_strain
    (WhoopExportImporter.swift:272), so it will not equal a mean of wire samples.
  - A night with no export value is a real gap, not a decode failure.

DROPPED — their model-id range table (0…37 = MG, 48…85 = 5.0).

Not for provenance: ryanbr#822 settled today that decompile-sourced facts are admissible
with attribution, so the original objection no longer applies. Dropped because we
already resolve identity a better way and use model-ids nowhere. Whoop5Variant reads
the DIS serial prefix (5AM/5AG) and the WG50 hardware-revision token — shipped in
ryanbr#781, sourced from ryanbr#520 — and that is documented here from our own code, including
that contradictory signals return .unknown and that .unknown is not MG.

CHANGED — the v18 field map is a pointer, not a copy. Their §8.1 duplicated the
authoritative table in BLE_REVERSE_ENGINEERING.md, and drift had already started:
theirs predates the dynamic_acceleration@41 row added earlier today. One table, one
place to keep correct.

FOUR REVIEW PASSES, ALL FINDING MY OWN MISTAKES

  - Wrong heading level and wrong parent: ## 7.9/7.10 under a section whose
    subsections are ### 7.1..7.5, and neither topic belongs under "Historical-data
    offload". Moved to top-level 9 and 10, File map renumbered to 11 after checking
    nothing cross-references §8/§9 and that the file has no TOC.
  - Overclaimed negative: "there is no GET_SPO2 opcode" restated as "none is known".
    §6 is explicitly a safe SUBSET and the 98-vs-87 dispute shows the map is
    incomplete; the catalogue is evidence, not proof about the strap.
  - Two inherited claims passed through unverified, now sourced: the per-cycle
    export shape (cited to our importer) and the DIS characteristics (cited to
    BLEManager.swift:473-474).
  - One claim that cannot be checked here — naps/incomplete nights carrying no
    export SpO2 — kept but explicitly marked a contributor observation, so it is not
    later cited back as established.

Every claim in these sections is now either checked against this tree or labelled
unverified. Docs only.

Co-authored-by: Dan Urbanek <abc@dnesdan.cz>
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…br#807)

Narrow the remaining ryanbr#807 content after PROTOCOL.md §10 landed elsewhere:
- explicit five-point promote bar for spo2_candidate_82 (incl. incomplete-night
  falsification and no recovery/illness gating)
- band sleep_state @81 vs hypnogram quick reference
- cross-link to PROTOCOL §10 and the existing validate_spo2_candidate harness

No PROTOCOL.md changes; no model-id table; no app/decode behaviour change.
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