Pin the first real 5/MG FAILURE response, from the #891 wrist pair - #906
Merged
Conversation
Until now no real WHOOP 5/MG frame in the tree carried `result = FAILURE(0)`. The only non-SUCCESS fixtures on that family are the two synthetic GET_HELLO frames, so the FAILURE branch of the result decode added in #899 had never been exercised by bytes a strap actually sent. #891's latest hardware runs supply one. The same MG accepted `SELECT_WRIST` with arg=1 and refused it with arg=0 minutes apart in one session, which gives a success/failure pair on a single opcode — the shape that made the 4.0 result mapping credible when GET_EXTENDED_BATTERY_INFO was seen answering both ways, and which 5/MG had no equivalent of. It is also the first fixture for opcode 123 on either family. Both frames re-verified before committing: CRC16-Modbus header, CRC32 tail, declared length, and every field against the decoder rather than by hand. Provenance stated at the fixture per #903 — WHOOP 5 MG WS50_r03 build 215, posted with the raw bytes in #891 — and the payloads are inert (`01 01 00 00` and `00 01 00 00`): no name, serial or token. Swift and Kotlin assert the identical bytes. The refusal is deliberately not interpreted. The strap ran the same opcode successfully minutes earlier, so it is not "unknown command", but whether it refuses a mutation or whether 0 is simply not a valid argument is open — the right=0/left=1 mapping is inferred from the vendor client's enum order and has never been attested. The test pins the decode of both outcomes and leaves the meaning to #891. Also sharpens why byte 13 stays undecoded. The new run sent arg=0 and got byte 13 = 1 back, which refutes the echo reading outright. It does not establish the read-back reading: across all eight MG command responses now on record, over four opcodes and both result codes, byte 13 has taken exactly one value, so a constant fits every observation as well as stored state does and is simpler. Naming the slot would encode the unproven half of a result that only refuted the alternative. No production code changes — fixtures and rationale only.
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.
Fixtures and rationale only — no production code changes.
The gap
No real WHOOP 5/MG frame in the tree carries
result = FAILURE(0). The only non-SUCCESS fixtures on that family are the two syntheticGET_HELLOframes, so the FAILURE branch of the result decode added in #899 has never been exercised by bytes a strap actually sent.#891's latest hardware runs supply one. The same MG accepted
SELECT_WRISTwitharg=1and refused it witharg=0, minutes apart in one session:aa010c…d77b81010100007ce76722SELECT_WRIST(123)SUCCESS(1)aa010c…217bcc000100000213163dSELECT_WRIST(123)FAILURE(0)That is a success/failure pair on one opcode — the shape that made the 4.0 result mapping credible once
GET_EXTENDED_BATTERY_INFOwas seen answering both ways, and which 5/MG had no equivalent of. It is also the first fixture for opcode 123 on either family.Verification
Both frames re-decoded before committing — CRC16-Modbus header, CRC32 tail, declared length, and every field checked against the decoder rather than by hand. Provenance stated at the fixture per #903: WHOOP 5 MG
WS50_r03, build 215, posted with the raw bytes in #891. Payloads are inert (01 01 00 00and00 01 00 00) — no name, serial or token. Swift and Kotlin assert the identical bytes.What is deliberately not decided
The refusal is not interpreted. The strap ran the same opcode successfully minutes earlier so it is not "unknown command", but whether it refuses a real mutation or whether
0is simply not a valid argument is open —docs/PROTOCOL.mdflags right=0/left=1 as inferred from the vendor client's enum order, never attested. The test pins the decode of both outcomes and leaves the meaning to #891.Also sharpens why byte 13 stays undecoded. The new run sent
arg=0and got byte 13 =1, which refutes the echo reading outright. It does not establish read-back: across all eight MG command responses now on record — four opcodes, both result codes — byte 13 has taken exactly one value, so a constant fits every observation as well as stored state does and is simpler. Naming the slot would encode the unproven half of a result that only refuted the alternative.docs/PROTOCOL.mdis deliberately untouched: #896 is revising that same ECG section, and the hardware finding is already recorded in #891 and now at the fixture. Happy to add a paragraph there once #896 lands.