docs(whoop5): record the SpO2 protocol facts and the 5.0-vs-MG identity signals#824
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.
7 tasks
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.