Repository navigation
docs(whoop5): record the SpO2 protocol facts and the 5.0-vs-MG identity signals - #824
Merged
Merged
Conversation
…ty signals Re-implements the landable parts of #807 (dnesdan) against current main, avoiding a conflict their PR picked up when #814 rewrote the same @82 paragraph. Kept, and theirs: the SpO2 protocol facts. No GET_SPO2-style opcode exists — verified against our own CommandNumber table, 80 commands and none oxygen-related — SpO2 is computed on-device during sleep, the CSV export is a per-cycle aggregate rather than a mean of wire samples, and a night with no export value is a real gap. None of that was recorded anywhere; it answers a recurring question with a protocol answer instead of a shrug. Dropped: their model-id range table (0…37 = MG, 48…85 = 5.0). Not because of its provenance — #822 settled that decompile-sourced facts are admissible with attribution — but because we already resolve identity a better way and do not use model-ids anywhere. Whoop5Variant reads the DIS serial prefix (5AM/5AG) and the WG50 hardware-revision token, shipped in #781 and sourced from #520, and it is documented here from our own code instead. Changed: their §8.1 v18 field map is a POINTER, not a copy. BLE_REVERSE_ENGINEERING already carries the authoritative table, and a second copy drifts — theirs was already missing the dynamic_acceleration@41 row added earlier today. One table, one place to keep correct. Co-authored-by: Dan Urbanek <abc@dnesdan.cz>
Re-review caught two structural mistakes in the previous commit. Wrong heading level: I used `## 7.9` / `## 7.10` where section 7's subsections are all `### 7.1`..`### 7.5`, so mine rendered as top-level siblings of section 7 rather than parts of it. Wrong parent regardless: section 7 is "Historical-data offload (backfill)". Device identity and SpO2 semantics are not offload topics, so nesting them there was wrong even at the right level, and 7.5 -> 7.9 left a phantom gap implying missing content. Moved to their own top-level sections after §8 Decoded output, numbered 9 and 10, with File map renumbered 9 -> 11. Checked first that nothing cross-references PROTOCOL.md's §8/§9 by number or anchor (the §8/§9 hits elsewhere are an Oura design spec citing its own sections) and that the file carries no table of contents to keep in sync. Also re-verified the resolution logic the identity table describes against Whoop5Variant.resolve: a hardware string saying 5.0 while the serial says MG really does return .unknown rather than picking a winner.
… 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.
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.
5 tasks done
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re-implements the landable parts of #807 (@dnesdan) against current
main. Their PR picked up a conflict when #814 rewrote the same@82paragraph; writing fresh avoids it. Credited as co-author.Kept, and theirs — the SpO₂ protocol facts. These answer a recurring question with a protocol answer, and weren't recorded anywhere:
GET_SPO2-style opcode exists. Verified against our ownCommandNumbertable — 80 commands, none oxygen-related. It isn't a pollable gauge, so hunting a missing opcode is a dead end.blood_oxygen_pctis a per-cycle aggregate, so it won't equal a mean of raw wire samples.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 my original objection no longer applies. Dropped because we already do identity better and don't use model-ids anywhere.
Whoop5Variantreads the DIS serial prefix (5AM/5AG) and theWG50hardware-revision token — shipped in #781, sourced from #520. Documented here from our own code instead, including that contradictory signals resolve to.unknownand that.unknownis not MG.Changed — the v18 field map is a pointer, not a copy.
Their
§8.1duplicated the authoritative table inBLE_REVERSE_ENGINEERING.md. Two copies drift, and it had already started: theirs predates thedynamic_acceleration@41row added earlier today. One table, one place to keep correct.Verification: both factual claims checked against the tree rather than taken from the source PR — the command table has no SpO₂ opcode, and
Whoop5Variantcarries the three identity signals as described. Links resolve, i18n clean, docs only.#807 stays open — its author may prefer to land their own version, and that's their call to make rather than mine.