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.
Wake the band up
asleepA band stays silent until an app talks it into streaming. Press play to watch the real sequence go by.
Drag its heart rate
Heart rate is a single byte in the packet. Drag the slider and watch it change.
Tilt it in your hand
The accelerometer always feels gravity. Drag to tilt it; each axis is a 32-bit float.
drag to tilt
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.
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.
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
| service | characteristic | access | what |
|---|---|---|---|
| 0x180D Heart Rate | 0x2A37 | notify | standard HR Measurement. No bond needed. |
| 0x180F Battery | 0x2A19 | notify | battery percent. No bond needed. |
| 0x180A Device Info | — | read | model / hardware / firmware. No bond needed. |
| fd4b0001 (custom) | fd4b0002 | write | command channel. Authenticated bond required. |
| fd4b0001 | fd4b0003 | notify | command responses. |
| fd4b0001 | fd4b0004 | notify | events. |
| fd4b0001 | fd4b0005 | notify | data + biometric streams (r22 lands here). |
| fd4b0001 | fd4b0007 | notify | identity / "hello" blob. |
Frame envelope
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.
| cmd | name | b3 | payload |
|---|---|---|---|
| 0x91 | GET_HELLO | 0x01 | none |
| 0x8d | GET_ADVERTISING_NAME | 0x01 | none |
| 0x78 | SET_CONFIG | 0x01 | [name NUL-padded to 32B][value u8][7×00] = 40 bytes |
| 0x22 | GET_DATA_RANGE | 0x00 | none |
| 0x16 | SEND_HISTORICAL | 0x00 | none — starts the 0x2f stream |
| 0x17 | HISTORICAL_DATA_RESULT | 0x01 | 8-byte cursor — the per-chunk ack |
| 0x42 | SET_ALARM_TIME | 0x04 | do 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.
| flag | value | flag | value |
|---|---|---|---|
| enable_r22_packets | 0x32 | hr_ch_switching | 0x32 |
| enable_r22_v2_packets | 0x32 | ir_hw_switching | 0x32 |
| enable_r22_v3_packets | 0x32 | enable_passive_strap_fit_gen5 | 0x31 |
| enable_r22_v4_packets | 0x31 | enable_sig11_during_sleep | 0x32 |
| enable_r22_v5_packets | 0x32 | dorset_inhibit_wpt | 0x32 |
| enable_r22_v6_packets | 0x32 | make_hrfm_visible | 0x32 |
| enable_r22_v8_packets | 0x32 | disable_pip_r26_packets | 0x32 |
| wear_detect_bias | 0x32 |
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)
| type | on | content |
|---|---|---|
| 0x2f | fd4b0005 | r22 biometric. cmd 0x80/0x82 = 112-byte HR + accel packet. A 76-byte variant carries an int16 run (likely PPG, uncalibrated). |
| 0x30 | fd4b0004 | events; device-tick timestamps. |
| 0x31 | fd4b0005/4 | metric / status; u32 fields + device timestamp. Subtype 0x02 carries the offload cursor. |
| 0x32 | fd4b0005 | console log; ASCII debug text. |
| 0x36 | — | identity blob, 44-byte inner (observed, not fully classified). |
r22 packet field map (112-byte, cmd 0x80 / 0x82)
| offset | type | field |
|---|---|---|
| [7:11] | u32 | device timestamp (band RTC, unix seconds) |
| [14] | u8 | heart rate, channel 1 (bpm) |
| [29] | u8 | heart rate, channel 2 (hr_ch_switching) |
| [37] | f32 | accelerometer X (g) |
| [41] | f32 | accelerometer Y (g) |
| [45] | f32 | accelerometer 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).