Repository navigation
Description
Summary
This proposes an exception to the "no firmware/DFU" rule in the BLE safety contract: a Test Centre–only developer tool for testing NOOP's interoperability with specific WHOOP 5/MG firmware versions. The tool transfers a firmware image the developer supplies for their own test strap, lets the strap verify it, and activates it only after an explicit confirmation. NOOP would neither download nor ship firmware.
The motivation is interoperability. How NOOP talks to a strap depends on the firmware it runs, and today a developer cannot choose which firmware a test strap runs. That makes firmware-specific problems hard to reproduce and fixes hard to verify.
A working implementation exists (Android, validated on a real 5/MG strap; iOS at code parity with Android). Per docs/CONTRIBUTING.md, this issue comes first. No PR will be opened unless this approach is acceptable.
Work in progress: a branch rebased on current main will be linked here shortly.
Why
1. Firmware changes affect interoperability
Ordinary firmware development changes how a strap behaves on the wire, and it has affected NOOP before:
- WHOOP 4.0 straps on the "v25" offload layout synced history that NOOP could not decode. Sleep and recovery stayed empty until the decoders caught up (v1.95 decoder, v1.97 retro-decode of the archived records).
- Some WHOOP 4.0 straps on 41.17.x silently failed to set their clock, so they banked no history at all until NOOP learned to send both clock-command formats.
- Before NOOP stopped acknowledging undecodable chunks, a strap could trim history that NOOP had accepted but could not read.
- WHOOP 5/MG firmware updates reset the Bluetooth bond, which needed dedicated reconnect help.
All four are in the app changelog.
A realistic 5/MG case looks like this. A routine release changes the layout of an offloaded history record, or the fields in the hello/version response. NOOP's fail-closed decoders then produce no sleep, recovery or strain for straps on that version until someone maps the new layout.
To map and test it, a developer needs control over the firmware on a test strap. Today they have none:
- The vendor app offers only the release that is currently rolling out, with no older versions and no downgrade.
- A developer therefore cannot put a test strap on the version a report came from.
- They also cannot return a test strap to a defined baseline after testing.
A transfer tool does not fix decoders. It lets developers test them against a chosen firmware version.
Timing. This only helps if the tool exists and is proven on hardware before a layout change needs investigating. Building it now, while the current procedure is understood and testable on real straps, is the way to have it ready.
2. Reproducible test setups
Firmware differences are a real source of "works for me" bug reports. Test straps on the same defined version make developers' logs comparable, and some bugs become reproducible.
3. Developer testing across versions
Contributors who want to support a new firmware can move a test strap between versions deliberately. That makes decoder work and regression checks possible without waiting for rollouts.
Placement
This is a tool for developers, not an end-user feature. It belongs in the Test Centre, clearly marked, and is never reachable from normal flows. NOOP does not recommend firmware versions.
- No firmware distribution. NOOP must not ship, fetch, host or link any firmware image. The developer uses an image they obtained for their own device.
- No circumvention. The tool uses the strap's own update procedure with an unmodified image. It does not bypass any check the strap performs. If the strap rejects an image, the tool reports that and stops.
What the tool does (wire level)
These commands are documented for firmware 50.42.1.0 and observed on-device. Frames go through the existing framing and CRC path, and every response is CRC-gated and correlated to its request.
- Preflight quiesce. 500 ms settle, then:
3 [0]stop realtime HR106 [1,0]stop IMU20 []abort history
- Prepare.
142 [1]opens the update slot. - Write.
143 [1, offset LE32, count, data]in 220-byte chunks, with the exact final remainder.- A missing response is retried at the same offset with the same bytes, at most 7 attempts in total.
- An explicit device rejection stops immediately and shows the result code.
- Verify.
83 [1]. The strap checks the complete staged image and returns a correlated final result. - Activate.
144 [1]. This is sent only after a successful verify and an explicit confirmation. It is never automatic and never retried. The strap restarts and reconnects, and the app then shows the version the strap reports.
During the session, sync, backfill, realtime re-arming and keep-alive are suspended. They are restored afterwards.
Safeguards
- The file is the gate.
- Anyone exploring the Test Centre cannot trigger anything that writes to the strap, whatever they tap.
- Nothing is sent until the developer supplies a firmware image file that passes every structural and CRC check below.
- That makes this path safer than a warning text could: the tool is inert without a valid image.
- Scope.
- Test Centre only.
- WHOOP 5/MG only for now, with an encrypted bond. WHOOP 4.0 and other devices are refused.
- WHOOP 4.0 support is conceivable later, once its update procedure is documented and validated on hardware the same way.
- No automatic sending of any kind.
- Image checks before transfer.
- Container header, length and alignment, header CRC and payload CRC.
- For the compressed OTA container: exactly one gzip member with a valid trailer, then the nested raw image's length, type, CRCs and version must match.
- Size limits apply both before and after decompression.
- Anything else is refused.
- The strap checks again. The strap verifies the complete staged image before activation is even offered.
- Separate, confirmed activation.
- The confirmation dialog shows the current and target version.
- It warns explicitly on downgrade or re-flashing the same version.
- Resume only on the same session. A paused transfer can resume only on the same connection, device and image. Any disconnect or change starts over. There is no prepare-and-skip fallback.
- Nothing in the repository.
- No firmware, no URLs, no download code.
- The developer picks a local file.
- Optional test fixtures are read from environment variables only.
Residual risk (stated plainly)
-
The transfer sends the same unmodified file through the same verify-then-activate sequence as a regular firmware update. For an unmodified image, the risk should be comparable to a regular update.
-
The remaining risk is the same as for any update: power or link loss around activation.
-
The UI asks the developer to keep the strap nearby and charged. The app does not read the battery level first.
-
Not established:
- whether the bootloader accepts downgrades or enforces a rollback or signature policy;
- whether the previous slot is preserved.
Matching CRCs prove integrity, not authenticity or compatibility. The UI does not claim more than that.
Validation so far
- Android. On a WHOOP 5.0 strap, a transfer from
50.41.1.0to50.42.1.0completed. The strap verified the image, activated it, reconnected, and reported the new version. Unit tests cover the image validation, the transfer engine (retry, pause/resume, rejection, verify failure, cancel) and the UI policy, optionally against the original image. - iOS. Brought to code parity with Android by a line-by-line comparison of the transfer path, and uses the same acknowledged-write flow control. It compiles on the macOS and iOS CI. It is not hardware-tested on iOS.
- Parity. The pure parts (image validation, engine, states) live in
Packages/WhoopProtocolandcom.noop.protocol, paired through the parity ledger. - Not tested yet: WHOOP MG (planned before any PR, on the PR build); downgrade on hardware.
Questions for the maintainer
- Is a Test-Centre-only, confirmation-gated firmware transfer for developers acceptable as an exception to the "no reflash" rule?
- If yes, are there additional conditions you would want? Examples: an extra Experimental toggle, or a typed confirmation.

Activity
ryanbr commentedon Sep 15, 2026
Thanks @bhelm, this is well researched. I checked all four precedents and they
are accurate verbatim in the changelog: the v25 layout (#151), the 41.17.x clock
failure (#120), acknowledge-then-trim (#77, #91), and the 5/MG bond reset.
Two things before the questions can be answered.
The rule is softer than your summary allows, so you undersell your own case.
The ban is on destructive, and reversibility is the test.
powerCycleStrap(32) is already in the curated enum despite power-cycle being named in the
exclusion list, admitted because stored data survives in flash, behind Test
Centre plus a confirmation.
rebootStrap(#166) and the Oura feature-mode write(#2105) went the same way. The list is a default, not a wall.
The condition that decides it is the one you have not met, and it is the one
your own argument rests on. The contract asks for four things: justify
reversibility, confirmation-gate, never automatic, document payload and
on-device verification. You have three, thoroughly. The missing one is
reversibility, and
CLAUDE.mdis explicit that a write whose effect cannot beshown to reverse is treated as destructive until proven otherwise.
Your value proposition IS reversibility: staying on, or returning to, a
known-good version. The run you validated was an upgrade, 50.41.1.0 to
50.42.1.0. Downgrade sits under "not tested yet", next to the two unknowns that
are the same question: whether the bootloader accepts downgrades, and whether
the previous slot is preserved.
So one experiment decides this proposal. A downgrade back to 50.41.1.0 on your
5.0. If it works, reversibility is demonstrated and this looks like the other
three exceptions. If the bootloader refuses, the tool delivers "update without
an account", which does not address the motivation you argue for, and it is
still a firmware write that cannot be shown to reverse. Your strap, your call on
whether that risk is worth running.
Two smaller points for whenever you get there:
being genuine, and CRCs prove integrity, not authenticity, as you say. If the
bootloader turns out not to enforce signatures, a tampered image is a
different risk conversation from "the same bytes as an official update".
fine, decompiled expression is not. Your preflight using only existing curated
commands (3, 106, 20) is a good sign, but the four new ones need their source
stated.
Holding question 2 until then: conditions only matter once reversibility is
settled.
[-]Proposal: Test Centre firmware transfer for WHOOP 5/MG (user-supplied image, confirmation-gated)[/-][+]Proposal: Test Centre firmware transfer for WHOOP 5/MG — developer tool for interoperability testing[/+]bhelm commentedon Sep 15, 2026
Thanks, that is a much fairer reading of the contract than mine, and the issue numbers help. I've reworded the issue to describe it as what it is: a developer tool for interoperability testing.
Reversibility. Agreed, that is the deciding experiment. I can't run it yet because I don't have a
50.41.1.0image. The next chance is the next release: once my 5.0 is on a newer version, flashing50.42.1.0back is the same experiment. I'll report the result here either way. I agree it is the whole point: a tool that can only move forward has little reason to exist. If the bootloader refuses the downgrade, I'll say so plainly and withdraw the proposal. Until then, reversibility is unproven, and I understand question 2 is on hold.Source of 142/143/83/144.
CommandNumbertable:VERIFY_FIRMWARE_IMAGE,START_FIRMWARE_LOAD_NEW,LOAD_FIRMWARE_DATA_NEWandPROCESS_FIRMWARE_IMAGE_NEW.docs/PROTOCOL_UPDATES.md.ATTRIBUTION.md. Only numbers, layouts and order are reimplemented. The code is written independently, and no code or other expression was copied. The sequence was then run end to end on a real 5.0.I'm happy to put this attribution into the code comments and
ATTRIBUTION.mdin the same form as the existing entries."The file is the gate" cuts both ways. Agreed. The tool adds no authenticity check and bypasses none: whatever the strap enforces stays in force. It is meant for an unmodified image from the developer's own device. If the bootloader turns out not to enforce signatures, a tampered image is a separate risk, and the proposal doesn't claim to cover it.
ryanbr commentedon Sep 20, 2026
Two things from checking the code, neither reopening reversibility.
The four opcodes are not in
CommandNumber. They are only in the label table(
Enums.kt:376, 404-406), the decode-side lookup for logging, which also carriesFORCE_TRIM(25). Nameable is not sendable: there are zero enum entries for83/142/143/144 today.
So this needs two barriers removed before "the file is the gate" applies:
CommandNumberentries. That is the gate used on WHOOP 5/MG: the strap advertising name cannot be changed, so a second-hand band keeps the previous owner name #2338, whereSET_ADVERTISING_NAME(140)was deliberately left out so no code path couldexpress the write whatever an allow-list said.
WhoopBleClient.kt:4448), which todayadmits live HR, haptics, the offload pair, clock and a short in-flight-only set.
Worth naming, since both are deliberate barriers rather than oversights.
Battery. The proposal names power loss around activation as the residual risk
and then says the app does not read the battery level first. It can:
batteryPctfrom
0x2A19on a 5/MG, plus the ~8 minuteBATTERY_LEVELevent(
WhoopBleClient.kt:120, 172). A minimum-charge precondition on activation onlycosts nothing and reduces the one risk listed as unmitigated.
POWER_CYCLE_STRAP(32)checks out as a precedent:Enums.kt:185, reachable onlythrough
rebootProbebehind Test Centre.