Hacking Bluetooth Sports Tech for Fun and Profit — Part 6: What's in the Overnight Buffer (and Why Reading It Erases It)
The WHOOP records all night and uploads nothing until you ask. Here's what's actually in that buffer, the field map that everyone — me included — got slightly wrong, and the command that wiped five hours of my data.
Parts 4 and 5 documented the WHOOP 4.0's protocol and computed HRV from it. Both ran on the same raw material: the strap's internal recording buffer, downloaded over Bluetooth in the morning. The strap is not a live sensor you read in real time; it's a logger that hoards everything internally and hands it over in one burst when a trusted client asks. This post is about that hoard: what each record contains, and why the act of reading it is sharper than it sounds.
One record per second, ninety-three bytes each
When you sync, the strap streams its history as packet type 0x2F. On the WHOOP 4.0 (the Harvard firmware) each record is 93 bytes and covers one second. Decoded, a single second looks like this:
| Offset | Field |
|---|---|
| 0–7 | sequence counter + Unix timestamp |
| 14 | heart rate (bpm) |
| 15 | RR-interval count (0–4) |
| 16–23 | up to four RR intervals (ms) |
| 26–29 | PPG photodiode, green and red/IR |
| 33–44 | accelerometer gravity vector (3× float32) |
| 48 | skin contact (0 = off-wrist) |
| 60 | motion indicator (≤ 0x40 at rest) |
| 61–64 | SpO2 raw ADC, red and infrared |
| 65–66 | skin temperature (raw × 0.04 = °C) |
| 67–76 | ambient light, LED drive, respiratory rate, signal quality |
| 77–92 | device config (constant) |
That's a great deal more than a heart rate: every second of the night carries beat-to-beat timing for HRV (at a resting pulse you rarely see more than one or two RR intervals filled, but the field holds four), a gravity vector you can read posture and movement from, the raw red and infrared photodiode values SpO2 is computed from, skin temperature to a fraction of a degree, and a respiratory-rate estimate. The WHOOP app shows you a handful of morning numbers; the buffer underneath holds the per-second signal those numbers were boiled down from. A night is tens of thousands of these records — the dataset Part 5's HRV ran on.
The field map everyone got slightly wrong
Reading a field map is easy, but trusting it can be the mistake...
An early version of my decoder, inherited from plausible-looking community notes, had two fields wrong. Offsets 69–70 were labelled "skin temperature." They aren't; they're LED drive current, which tracks perfusion and rises and falls with it, which is why it looks like a temperature signal until you check. The real skin temperature sits at 65–66, and only reads sanely (about 31–36 °C on the bicep) once you multiply the raw value by 0.04. Offsets 61–68 were labelled "PPG amplitude." They're the red and infrared SpO2 ADC channels plus that skin-temperature word, which is a different thing.
I only caught it by cross-checking every offset against openwhoop's independently-written parser, byte for byte, against records where the ground truth was obvious: skin temperature can't read 8 °C, and a perfusion signal climbs when you move rather than when you warm up. The lesson is the one that runs through this whole series: a label that fits the data is not the same as a label that's correct. Anchor every field on something you can verify independently, or mark it unverified and leave it.
The correlation between reading and erasing
Three sync commands matter here, and their use is critical:
0x17(acknowledge) advances a read pointer. It tells the strap "I've got everything up to here." It deletes nothing.0x31cmd0x03(HistoryComplete) is the strap saying "I've sent you everything I have." Only the strap emits it; the client can't ask for it.0x19(FORCE_TRIM) deletes the buffer up to the write head. Irreversible.
The official app's order is strict: stream records, acknowledge them, wait for HistoryComplete, and only then trim. The trim is the cleanup step that frees space for the next night, and it's gated entirely on the strap having confirmed it sent everything first. If you incorrectly sequence, you destroy data. Ask me how I know...
Five hours, gone
On 2026-04-12 I walked out of Bluetooth range with the strap still on. It kept recording. Over the next five hours it accumulated about 19,000 records internally while my Pi-side daemon failed to reconnect 354 times. When the strap finally came back, I hit "Sync Now," the browser request timed out, and the daemon's cancellation handler did the one thing it should never do: it sent FORCE_TRIM anyway.
The buffer cleared. 132 records survived; roughly 19,000 — about 11:00 to 16:20, heart rate, HRV, motion, skin temperature, the lot — were gone for good.
The root cause was a single unconditional line. The daemon trimmed on every exit path (complete, cancelled, errored) with no check for whether the sync had actually finished. The fix gates FORCE_TRIM on the two facts the official app waits for: HistoryComplete was received, and at least one record came back. On any other exit the daemon leaves sync mode and keeps its hands off the buffer. Worst case the next sync re-reads a few records, and since the backend's inserts are idempotent, duplicates cost nothing. That trade — a little wasted Bluetooth time against five lost hours — isn't close.
The rule now applies to every client I write: never trim unless the strap has said it's done.
Where do we go from here?
Everything above is the WHOOP 4.0, the Harvard firmware and hardware. The strap I moved to next, the MG, speaks the same handshake but stores its buffer in a different codec entirely: per-second records and separate 24 Hz raw-PPG bursts, with a different byte map and several inverted polarities (the rest-versus-motion test flips, which is its own trap waiting for anyone who assumes the layouts match). And there's a finding that makes the buffer matter more, not less, on the newer hardware: there's no live optical stream to fall back on. The sync buffer isn't one way in. It's the only way in.
That's where Part 7 picks up.
This is part 6 of "Hacking Bluetooth Sports Tech for Fun and Profit" — a series documenting BLE reverse-engineering of fitness wearables. Tools: VESTIGATOR (Python + bleak), Pi Lab (FastAPI + BlueZ D-Bus). All research conducted on personally-owned devices.
Comments ()