noop — what is the strap actually sending?
noop (27 PRs merged upstream) + noop-cloud (creator)
Decode the bytes off a WHOOP, corroborate them against Oura and Apple Health, then make the result queryable by an agent without giving it silent write access.
What it is
NOOP is an offline, on-device WHOOP companion app by @ryanbr and contributors. I work in its sensor-protocol and analytics layers, with 27 pull requests merged upstream. noop-cloud is mine: a self-hostable MCP server that mirrors the health database and serves it to Claude, ChatGPT, or any agent.
Together they answer the question I actually cared about. Can I bring my own strap, ring, or watch, keep the data mine, and still have an agent reason over the full-resolution signals? Yes, but only if the signals first agree and the agent cannot silently rewrite them.
The hard parts
What are these bytes? WHOOP 5.0 speaks an undocumented Bluetooth Low Energy protocol. I decoded the raw six-axis IMU offload into activity features (#455), captured the high-rate R22 deep buffers (#454), and persisted the raw optical PPG waveform (#415). Then I went a layer deeper. The strap’s 2,140-byte optical record resolves into five measurement blocks, each with its own LED drive and two photodiode readouts, sampling at 25 Hz. I mapped it byte by byte: 54 structural assertions across all 29,203 captured records, zero failures, all 2,140 bytes accounted for, with a CRC that pins the frame exactly. From one readout I recovered a cardiac pulse that estimates the strap’s own heart rate to within about 3 bpm at rest. My first pass labelled the channels by wavelength: two green, red, infrared. A structural audit killed that. The two slots are photodiodes of a single illumination, not two colours, so I retracted the labels publicly and kept the offsets, which held. The figures below are that signal, not a mockup.
Which vendor is right? None of them gets to be ground truth by default. WHOOP, Oura, and Apple use different sensors and algorithms, so I make the signals comparable and look for the contradiction. A heart-rate spike on a motionless wrist should not score as wake. I changed the sleep logic to require the IMU to corroborate it (#465, #402). An Oura importer was storing a 0–100 readiness score as beats per minute. Cross-device comparison exposed it, and I fixed the Swift and Kotlin twins (#365, #368, #376).
Does the cleaned result go anywhere useful? It writes back to Apple Health: sleep stages, heart rate, and workouts (#249). I also shipped Recovery Index and Activity Balance terms in the Charge score (#417), an opt-in workout classifier (#414), and a predictive low-battery alert (#250).
Can an agent help without becoming another source of bad data? noop-cloud separates read and write tokens and routes edits through propose, confirm, journal, and undo. The agent can find a bad score and propose a correction. Nothing changes until I confirm it, and nothing escapes the audit trail. The full server is its own study: MCP tooling.
What I’d do differently
I reverse-engineered the BLE protocol from captured buffers without official docs. Next time, I would build the replayable capture-and-decode harness first. Firmware will change. I should be able to rerun the evidence instead of chasing the whole protocol again.
Raw bytes. What is the strap actually sending? WHOOP 5.0 speaks undocumented Bluetooth Low Energy. What arrives is an opaque buffer: no schema, no field names, no help.
Save it first. I do not want to discard a signal just because I cannot decode it yet. Capture enough buffers, line them up, and the structure starts showing itself: fixed offsets, repeating strides, values that move together.
The optical signal. One buffer carries the raw v26 PPG, the optical pulse of blood flow. The strap emits it as a one-second burst. I made sure the signal survives longer than the reading that produced it.
Was that the whole signal? No. The persisted burst was one readout. The raw optical record is 2,140 bytes: five measurement blocks, each with its own LED drive and two photodiode readouts, sampled twenty-five times a second. I mapped it byte by byte: 54 structural assertions across all 29,203 records, zero failures, every one of the 2,140 bytes accounted for.
Does the decode tie out? One readout becomes a clean cardiac pulse over time. Estimate heart rate from it and it lands on the strap's own within about 3 bpm at rest. But an estimator that hunts for a peak will always find one. The real question is whether it finds a pulse in something that has none. One block samples with its LEDs switched off. Run it through the identical pipeline and it misses by 42 bpm. The signal is a signal because the null stays null.
Now corroborate it. Another buffer resolves into six axes of motion at 100 Hz. The optical signal says what the heart did; the IMU says whether the wrist actually moved. One second of it looks like this.
Eight hours of it looks like this. 2.9 million samples per axis, banked one second at a time. Accelerometer magnitude has gravity subtracted out, so a still wrist sits near zero however the arm is turned; the lower lane is walking cadence, and it is blank wherever the movement had no rhythm to measure. The gaps are real: those are seconds the strap never banked, and I would rather show the holes than interpolate across them.
From signal to sleep. The old scoring saw elevated heart rate and called it WAKE, even on a motionless wrist. That does not make sense. A racing heart is not proof that I woke up.
Corroboration. What does the IMU say? No motion, no wake. The heart-rate call only survives when the independent motion signal supports it. Now the hypnogram is honest.
So which device is right? Neither, by default. Two devices ride the same wrist and count the same steps on the same day, and they disagree by a median of 70%. The strap reads higher on 22 of 24 days, until the two days I took it off and the gap flips the other way. That is the whole problem in one figure. No vendor gets to be ground truth because it shipped first, and a number is not a measurement until something independent agrees with it.
Did it actually ship? Twenty-seven pull requests merged into the base app. Not a private fork and not a demo. Real users, real straps, and real signals like these: