Interactive · Experiment

Talk to a WHOOP 5.0

A toy band that runs in your browser. Wake it up, drag its heart rate, tilt it, and watch the real Bluetooth bytes change.

simulated · runs locally ← How this was reverse-engineered
1

Wake the band up

asleep

A band stays silent until an app talks it into streaming. Press play to watch the real sequence go by.

2

Drag its heart rate

Heart rate is a single byte in the packet. Drag the slider and watch it change.

61beats / min
the heart-rate byte the band sends:
3

Tilt it in your hand

The accelerometer always feels gravity. Drag to tilt it; each axis is a 32-bit float.

drag to tilt

X
0.00 g
Y
0.00 g
Z
1.00 g
the accelerometer bytes (X, Y, Z as floats):
See the whole packet you're building updates as you play above

A complete, valid r22 frame built from the values above. Heart-rate byte in blue, accelerometer floats in violet.

4

Try to corrupt a packet

Two checksums guard every frame. Click a byte to flip a bit and watch them catch it. Re-seal recomputes both, the way the sender does.

start/verlengthfieldheader CRCinnerCRC-32you flipped this
Decode a real capture paste your own frame hex

Paste a frame's hex; it's decoded in plain language with both checksums verified.

The full spec

Confirmed against a decrypted HCI capture and the shipping decoder. Little-endian throughout; [7:11] means bytes 7 to 10.

Device identity

The 4.0 advertises the 61080001-… custom service; the 5.0 advertises fd4b0001-…. Device Info reports model MG, hardware WS50_r03, firmware 50.38.1.0.

GATT map

servicecharacteristicaccesswhat
0x180D Heart Rate0x2A37notifystandard HR Measurement. No bond needed.
0x180F Battery0x2A19notifybattery percent. No bond needed.
0x180A Device Info—readmodel / hardware / firmware. No bond needed.
fd4b0001 (custom)fd4b0002writecommand channel. Authenticated bond required.
fd4b0001fd4b0003notifycommand responses.
fd4b0001fd4b0004notifyevents.
fd4b0001fd4b0005notifydata + biometric streams (r22 lands here).
fd4b0001fd4b0007notifyidentity / "hello" blob.

Frame envelope

Outer envelope — little-endian total frame = len + 8 · len counts bytes [4:−4]
0xAASOF
0x01ver
lenu16
fieldu16
crc16MODBUS
innervariable
crc32zlib(inner)
Inner — reuses 4.0 command numbering
type0x23 = CMD
sequ8
cmd0x91, 0x78, …
b3per-cmd
payload0x2f r22 · 0x31 metric · 0x32 console

len counts frame[4:-4], so total length is len + 8. The header check is CRC-16/MODBUS over the first 6 bytes; the trailer is a zlib CRC-32 over the inner region. Both must pass.

Command set

b3 is the fourth inner byte and it's per-command; the wrong value gets silently ignored. All commands go to fd4b0002 with write-with-response.

cmdnameb3payload
0x91GET_HELLO0x01none
0x8dGET_ADVERTISING_NAME0x01none
0x78SET_CONFIG0x01[name NUL-padded to 32B][value u8][7×00] = 40 bytes
0x22GET_DATA_RANGE0x00none
0x16SEND_HISTORICAL0x00none — starts the 0x2f stream
0x17HISTORICAL_DATA_RESULT0x018-byte cursor — the per-chunk ack
0x42SET_ALARM_TIME0x04do NOT send, it buzzes the band

SET_CONFIG feature flags

The 15 flags the app sets, in order, to turn the stream on. Each is a 40-byte SET_CONFIG payload.

flagvalueflagvalue
enable_r22_packets0x32hr_ch_switching0x32
enable_r22_v2_packets0x32ir_hw_switching0x32
enable_r22_v3_packets0x32enable_passive_strap_fit_gen50x31
enable_r22_v4_packets0x31enable_sig11_during_sleep0x32
enable_r22_v5_packets0x32dorset_inhibit_wpt0x32
enable_r22_v6_packets0x32make_hrfm_visible0x32
enable_r22_v8_packets0x32disable_pip_r26_packets0x32
wear_detect_bias0x32

The offload ack loop

After SEND_HISTORICAL the band sends one chunk and waits. Status frames (type 0x31, subtype 0x02) carry an 8-byte cursor at inner[13:21]; echo it back verbatim as a 0x17 command and the next chunk releases. Miss it and the offload dies after one chunk.

Data streams (type byte)

typeoncontent
0x2ffd4b0005r22 biometric. cmd 0x80/0x82 = 112-byte HR + accel packet. A 76-byte variant carries an int16 run (likely PPG, uncalibrated).
0x30fd4b0004events; device-tick timestamps.
0x31fd4b0005/4metric / status; u32 fields + device timestamp. Subtype 0x02 carries the offload cursor.
0x32fd4b0005console log; ASCII debug text.
0x36—identity blob, 44-byte inner (observed, not fully classified).

r22 packet field map (112-byte, cmd 0x80 / 0x82)

offsettypefield
[7:11]u32device timestamp (band RTC, unix seconds)
[14]u8heart rate, channel 1 (bpm)
[29]u8heart rate, channel 2 (hr_ch_switching)
[37]f32accelerometer X (g)
[41]f32accelerometer Y (g)
[45]f32accelerometer Z (g)

Confirmed vs. still open

Confirmed: the envelope, both CRCs, inner layout, command set and b3 bytes, the enable sequence, the ack loop, stream classification, and the r22 HR + accelerometer fields. The codec round-trips all 8,031 captured frames byte for byte.

Open: the 76-byte r22 int16 variant (PPG); per-axis accel calibration; the meaning of every field value; full decode of 0x30 events and 0x31 metrics; the 0x36 identity blob; and R-R from the offload (it carries derived metrics, not beat-to-beat intervals, so live R-R still comes from 0x2A37).

← Back to the writeup