Skip to content

Protocol

NoopApp edited this page Jun 10, 2026 · 1 revision

Protocol & Reverse Engineering

This page documents how NOOP communicates directly with WHOOP straps over Bluetooth Low Energy, how the wire protocol was reverse-engineered, and how to extend the decoder for new packet types or sensors.

Key context: NOOP is a standalone, fully-offline companion for straps you own (WHOOP 4.0 and 5.0/MG). It pairs over BLE, decodes the strap's own data streams on-device, and stores everything locally in SQLite. There is no cloud, no account, and no connection to WHOOP's servers involved.

Interoperability & Safety

  • Not affiliated with WHOOP. NOOP is an independent project. "WHOOP" is used only to identify the hardware and is nominative fair use.
  • Not a medical device. Heart rate, HRV, recovery, strain, sleep, SpO₂, and temperature are local approximations only — not clinically validated and not for medical use.
  • Safe subset of commands. NOOP sends only reversible, non-destructive commands; dangerous operations (reboot, firmware load, force-trim, DFU) are deliberately excluded from the codebase so they cannot be sent.
  • §1201(f) interoperability exemption. This work is protected under 17 U.S.C. § 1201(f) and comparable provisions in other jurisdictions. It achieves interoperability with the user's own device without circumventing any technological protection measure or bypassing any login/account control.

Credits

This reverse-engineering effort builds directly on two community projects:

Project Generation Contribution
johnmiddleton12/my-whoop WHOOP 4.0 GATT service UUID, CRC8/CRC32 frame envelope, command numbers, type-40/43/47 stream layouts
b-nnett/goose WHOOP 5.0 GATT service UUID, CRC16-Modbus header, static CLIENT_HELLO frame, puffin packet types

Where code is a direct transcription (e.g., the crc16Modbus function), the source file documents it. Sensor scales and field offsets have been re-verified on real WHOOP 4.0 hardware.


1. Discovery & Bonding

GATT Topology

Each WHOOP generation advertises a vendor-specific custom service alongside two standard BLE services:

WHOOP 4.0 — service 61080001-8d6d-82b8-614a-1c8cb0f8dcc6

Characteristic UUID Direction Purpose
Command write 61080002-… app → strap send commands
Command-response notify 61080003-… strap → app command replies
Event notify 61080004-… strap → app wrist on/off, double-tap, battery, etc.
Data notify 61080005-… strap → app fragmented frames (history, realtime)

WHOOP 5.0/MG — service fd4b0001-cce1-4033-93ce-002d5875f58a

Characteristic UUID Purpose
Command write fd4b0002-… send commands
Notify (4 channels) fd4b0003, fd4b0004, fd4b0005, fd4b0007 responses, events, data

Standard SIG services (both generations)

Service Characteristic UUID Notes
Heart Rate (180D) HR Measurement 2A37 HR + R-R intervals; works unbonded at ~1 Hz
Battery (180F) Battery Level 2A19 battery percent; works unbonded

The standard 2A37 Heart Rate Measurement is the reliable HR/R-R source (parsed by StandardHeartRate.parse(_:)) and is treated as ground truth even during custom-stream mapping.

The Bond Handshake

WHOOP 4.0 uses a discovered "one confirmed write" trick: a .withResponse write to the command characteristic triggers just-works bonding with no PIN or pairing UI. NOOP performs this with a benign GET_BATTERY_LEVEL command. Once the write acknowledgement arrives, the link is bonded and the custom notify channels become active.

A key discovery (learned the hard way): didWriteValueFor fires on every .withResponse write, so the connect handshake is gated behind a connectHandshakeDone flag — re-running the handshake mid-offload was found to stop the strap from serving historical data.

WHOOP 5.0/MG requires the link to be bonded before touching the fd4b… service — subscribing to a notify channel without an encrypted link simply stalls indefinitely. NOOP writes the static CLIENT_HELLO frame with response, which triggers just-works bonding transparently on Apple platforms.

Connect Handshake (WHOOP 4.0)

Once bonded, the one-shot handshake runs:

  1. GET_HELLO_HARVARD (35) — version/identity hello
  2. GET_ADVERTISING_NAME_HARVARD (76) — advertised device name
  3. SET_CLOCK (10) — set the strap RTC to UTC with an 8-byte payload [seconds u32 LE][subseconds u32 LE] (a wrong-length payload is ack'd but not latched, leaving the RTC lost and breaking history)
  4. GET_CLOCK (11) — empty payload; read RTC → establishes device↔wall clock correlation used for realtime decoding
  5. SEND_R10_R11_REALTIME (63) — payload [0x00] to stop the ~2/s type-43 raw flood (this is the only control that persists across reconnect; STOP_RAW_DATA does not affect it)
  6. GET_DATA_RANGE (34) — refresh the strap's stored record range for the liveness watchdog
  7. After ~1.5 s settle, request the first historical offload via SEND_HISTORICAL_DATA

WHOOP 5.0/MG mirrors this flow on the puffin transport, accepting the same command numbers. The CLIENT_HELLO write is static and fixed (16 bytes, pre-computed CRC16/CRC32).


2. Frame Format & Checksums

WHOOP 4.0 Envelope (CRC8 Header)

┌──────┬─────────────┬──────┬───────┬──────┬──────┬──────────┬─────────┐
│ 0xAA │ length u16  │ crc8 │ type  │ seq  │ cmd  │ payload  │ crc32   │
│ [0]  │ LE [1..3]   │ [3]  │ [4]   │ [5]  │ [6]  │ [7..]    │ LE [..] │
└──────┴─────────────┴──────┴───────┴──────┴──────┴──────────┴─────────┘
        └─ crc8 over 2 length bytes ─┘
                        └─────── crc32 (zlib) over [type][seq][cmd][payload] ────────┘
  • 0xAA — Start Of Frame marker
  • lengthu16 little-endian, equals inner_bytes_count + 4 (total frame size on wire = length + 4)
  • crc8 — table-driven, poly 0x07, over only the two length bytes (cheap header integrity check)
  • type — packet type (command, response, event, historical data, metadata, etc.)
  • seq — sequence/version byte (rolling on commands; doubles as record version on historical packets)
  • cmd — command or event number
  • crc32 — standard zlib CRC-32 (reflected, poly 0xEDB88320), u32 LE, over inner bytes

Total frame: 1 (SOF) + 2 (length) + 1 (crc8) + N (inner) + 4 (crc32) = length + 4 bytes

WHOOP 5.0/MG Envelope (CRC16-Modbus Header)

The 5.0 transport swaps the header checksum and shifts the inner record:

┌──────┬────────┬──────────────┬────────┬────────┬───────┬──────┬──────┬──────────┬─────────┐
│ 0xAA │ format │ declLength   │ header │ header │ crc16 │ type │ seq  │ cmd      │ crc32   │
│ [0]  │ [1]    │ LE u16 [2-4] │ [4-6]  │ [4-6]  │ LE [6]│ [8]  │ [9]  │ [10]     │ LE [..] │
└──────┴────────┴──────────────┴────────┴────────┴───────┴──────┴──────┴──────────┴─────────┘
                                          └ crc16 over frame[0..6] ┘
                                                           └──── crc32 over frame[8 .. declLength+4] ────┘
  • format — always 0x01
  • declLengthu16 LE; counts the payload plus the 4-byte CRC32 trailer (so payload_length = declLength − 4)
  • header — 2 bytes at [4..6]
  • crc16 — CRC16-Modbus (poly 0xA001, init 0xFFFF, reflected) over frame[0..6], stored LE at frame[6..8]
  • Inner record — starts at offset 8 (not 4): type, seq, cmd, payload
  • crc32 — same zlib algorithm, LE, over payload frame[8 .. declLength+4)

Total frame: declLength + 8 bytes

The static WHOOP 5.0 CLIENT_HELLO (16 bytes, fully-formed type-35 frame with valid CRC16 and CRC32) is:

AA 01 08 00 00 01 E6 71 23 01 91 01 36 3E 5C 8D

Checksums

Algorithm Poly Init Reflected XOR final Use
CRC8 0x07 0x00 no no WHOOP 4.0 header (length bytes only)
CRC16-Modbus 0xA001 0xFFFF yes no WHOOP 5.0 header (6 bytes)
CRC32 (zlib) 0xEDB88320 0xFFFFFFFF yes 0xFFFFFFFF Payload integrity (both generations)

CRC32 is the only payload integrity guarantee and is enforced: any frame with a CRC32 mismatch is rejected, preventing garbled or hostile peers from forging chunk-end markers and corrupting the trim cursor.

Reassembly

BLE notifications arrive as MTU-sized fragments. The Reassembler accumulates bytes, finds the 0xAA SOF, reads the length field, and emits a complete frame once enough bytes have arrived. All custom-channel notifications are fed through one reassembler per connection before routing to the decoder.


3. Packet Types & Stream Overview

Type Name Direction Purpose
35 COMMAND app → strap send a command
36 COMMAND_RESPONSE strap → app command reply (battery, clock, version, data range)
37–38 PUFFIN_COMMAND / RESPONSE 5.0 only re-framed commands on puffin transport
40 REALTIME_DATA strap → app live HR + R-R intervals (~1 Hz)
43 REALTIME_RAW_DATA strap → app raw IMU/optical flood (~2/s, ~1.9 KB/frame)
47 HISTORICAL_DATA strap → app 14-day biometric store (primary source)
48 EVENT strap → app wrist on/off, double-tap, battery, alarms, double-tap, etc.
49 METADATA strap → app chunk boundary + trim cursor for safe offload
50 CONSOLE_LOGS strap → app plain-text firmware log lines
51–56 extended 5.0 extensions IMU streams, puffin metadata

Puffin type aliasing: WHOOP 5.0 introduces parallel types (e.g., 38 PUFFIN_COMMAND_RESPONSE, 56 PUFFIN_METADATA) that carry 4.0 semantics on the new transport. To avoid "unknown" packet types, canonicalTypeName aliases them onto their 4.0 equivalents (36, 49, etc.) so the decoder treats them the same.


4. Historical Data Offload (Type 47)

The type-47 store is a rolling ~14-day biometric history and NOOP's primary metric source. It is re-offloaded every ~15 minutes while connected, mirroring the official app's sync.

Session State Machine

SEND_HISTORICAL_DATA([0x00], .withResponse)
        ↓
HISTORY_START ──→ open chunk, accumulate type-47 records
   │
   ├─ HISTORICAL_DATA … HISTORICAL_DATA …  (frames buffered)
   │
   ├─ HISTORY_END(unix, trim) ──→ finishChunk:
   │    1. decode & insert rows into SQLite
   │    2. [if enabled] enqueue raw IMU/optical batches
   │    3. persist strap_trim cursor
   │    4. send HISTORICAL_DATA_RESULT([0x01] + end_data) with response
   │    (chunk clears; chunkOpen stays true for repeated ENDs)
   │
   └─ HISTORY_COMPLETE ──→ close offload session

Safe-Trim Invariant

A chunk is forgotten by the strap only after it is locally durable end-to-end:

decode → await insert(decoded) → await setCursor("strap_trim") → ackTrim

Any error short-circuits before the ack, so an unpersisted chunk is never trimmed. The ack itself is a .withResponse write, so the strap discards the chunk only once the write is confirmed. The strap_trim cursor persists across app restarts, so the next session resumes exactly where the last one stopped — no cloud needed.

Metadata Frame (Type 49)

MetadataType value Name Meaning
1 HISTORY_START offload beginning
2 HISTORY_END chunk boundary; carry the trim cursor — ack to advance
3 HISTORY_COMPLETE offload finished

The HISTORY_END payload (starting at frame offset 7 on 4.0, or 11 on 5.0) is decoded as:

Payload offset Field Type Meaning
0 unix u32 LE record time (seconds)
4 subsec u16 LE sub-seconds
6 (unmapped) u32 LE (reserved)
10 trim_cursor u32 LE ack with this to advance the strap's trim

The 8-byte end_data the ack echoes is the frame's last 8 bytes at the trim-cursor location.

Watchdog & Liveness

  • Idle watchdog (backfillIdleTimeoutSeconds = 60): re-armed on every genuine offload frame (types 47/48/49/50) only. If the strap goes silent the session exits and resumes next time via the durable cursor.
  • Stuck detector (StuckStrapDetector): after an offload, if the strap reports records newer than NOOP's frontier and that frontier has been frozen, the detector flags strapNeedsReboot and attempts defensive recovery (reset the clock and exit high-freq mode).

The type-43 raw flood is intentionally dropped during offload so it cannot starve chunk acks on the BLE link.


5. Biometric Records (Type 47)

WHOOP 4.0 Type-47 (Version 24)

Verified against 762 real device captures. The DSP record at version 24 decodes:

Offset Field Type Sensor / Meaning
11 unix u32 real unix seconds
21 heart_rate u8 bpm
22 rr_count u8 # of R-R intervals that follow
23 + 2i rr[i] u16 LE R-R interval in milliseconds
33 / 35 ppg_green / ppg_red_ir u16 optical ADC counts
40/44/48 gravity_x/y/z f32 LE accel-derived gravity (g)
55 skin_contact u8 0 = off-wrist; capacitive detection
56/60/64 gravity2_x/y/z f32 LE second accel/gravity triplet
68 / 70 spo2_red / spo2_ir u16 raw ADC; SpO₂ % computed on-device
72 skin_temp_raw u16 raw ADC; °C computed on-device
74 / 76 / 78 ambient / led_drive_1 / led_drive_2 u16 optical configuration
80 resp_rate_raw u16 raw; respiratory rate computed on-device
82 signal_quality u16 DSP quality metric

Versions 5, 7, and 9 are HR/R-R-only records with no DSP sensor block. Version 12 shares the v24 layout. The decoder handles version resolution via a ref chain in the schema.

WHOOP 5.0 Type-47 (Versions 18 & 26)

Real WHOOP 5 hardware emits version 18 (124-byte DSP record) and version 26 (88-byte PPG waveform record) — not a shifted v24 layout.

Version 18 (per-second DSP summary):

  • Offset 15unix (u32 LE): real unix seconds
  • Offset 22heart_rate (u8): bpm; verified exact match with live 2A37 ground truth across 96 overlapping timestamps
  • Offset 23rr_count (u8): # of valid R-R intervals
  • Offset 24 + 2irr[i] (u16 LE): R-R in ms; 88% match 60000 / mean(R-R) ≈ HR
  • Offset 45/49/53gravity_x/y/z (f32 LE): 1.0 g magnitude for 100% of records (v18 has one triplet, not v24's two)
  • Offset 73skin_temp_raw (u16): AS6221 sensor, native 7.8125 m°C/LSB; stored raw, divided by 128 for °C on-device

Version 26 (24 Hz optical PPG waveform):

  • Offset 15unix (u32 LE): real seconds
  • Offset 21ppg_channel (u8): optical channel index (1–26); strap time-multiplexes 26 channels over ~20 min, never sampling two simultaneously
  • Offset 27–74ppg_waveform (24× i16 LE): AC-coupled PPG samples at 24 Hz; autocorrelates to heart rate (lag 14 ≈ 103 bpm, matching v18 HR)

SpO₂ is not recoverable on WHOOP 5 offline — it requires simultaneous red+IR, but the strap time-multiplexes channels and never samples two at once. The raw optical region is left undecoded.

Offload Throughput (Firmware-Paced, Not Link-Bound)

The offload streams ~10 type-47 records per second — that is 10× real-time (a full day ≈ 40 min). This is a firmware pacing property, not link throughput:

  • Raising BLE MTU from 23 → 247 bytes: no change (a 104-byte type-47 still streamed at ~10/s)
  • Lowering connection interval from 50 ms → 7.5 ms: no change

Matching the official app's bulk sync (24 hours in 1–3 min) would require a different flash-page or bulk-transfer command, not link tuning. For unattended periodic sync, ~10× real-time is adequate and resumable via the trim cursor if interrupted.


6. The Type-43 Raw Stream

REALTIME_RAW_DATA (type 43, "R10/R11") is the strap's high-rate sensor flood, streaming continuously at ~2 packets/second with each packet ~1.9 KB. Two variants exist, distinguished by payload length:

Payload length Kind Contents Sample rate
1917 IMU 6 axes (accelX/Y/Z, gyroX/Y/Z), 100 samples per axis ~100 Hz
1921 Optical AC-coupled green PPG waveform ~437 Hz

IMU Variant (Well-Characterized)

  • Accel scale: 1/4096 g/LSB (verified sphere-fit: |g| ≈ 0.99 g, 0.0% residual)
  • Gyro scale: 2000/32768 = 0.06104 deg/s/LSB (full-scale ±2000 dps, verified with 720° rotations)
  • Axis offsets: accelX@89, accelY@289, accelZ@489, gyroX@692, gyroY@892, gyroZ@1092
  • Unmapped region: ~36% of frame (header gap + tail from offset 1292) kept raw — an honest gap, not an invented field

Why NOOP Disables It

The type-43 flood is expensive:

  • BLE airtime: ~2 × 1.9 KB/s dominates the link and starves the historical offload
  • Strap flash: keeping raw data on blocks dense biometric retention

The only control that persists across reconnect is SEND_R10_R11_REALTIME (63). Sending it with payload [0x00] stops the flood (verified on-device: 2.1/s → 0/s, persists across reconnect). The STOP_RAW_DATA (82) command does not affect it.

For research, captureRawAccel(seconds:) captures IMU for a bounded window with START_RAW_DATA + TOGGLE_IMU_MODE, then re-disables it. This is opt-in only; the global enableRawCapture toggle defaults off.


7. Events (Type 48)

EVENT frames carry an EventNumber at frame offset 6 (4.0) or 10 (5.0) and a u32 event_timestamp. Selected frequently-used event types:

Code Name Code Name
3 BATTERY_LEVEL 46 RAW_DATA_COLLECTION_ON
7 CHARGING_ON 47 RAW_DATA_COLLECTION_OFF
8 CHARGING_OFF 56 STRAP_DRIVEN_ALARM_SET
9 WRIST_ON 57 STRAP_DRIVEN_ALARM_EXECUTED
10 WRIST_OFF 58 APP_DRIVEN_ALARM_EXECUTED
13 RTC_LOST 60 HAPTICS_FIRED
14 DOUBLE_TAP 63 EXTENDED_BATTERY_INFORMATION
17 TEMPERATURE_LEVEL 96 HIGH_FREQ_SYNC_PROMPT
23 BLE_BONDED 97 HIGH_FREQ_SYNC_ENABLED
33 BLE_REALTIME_HR_ON 98 HIGH_FREQ_SYNC_DISABLED
34 BLE_REALTIME_HR_OFF 100 HAPTICS_TERMINATED

FrameRouter maps physical events to UI: BLE_BONDED confirms bonding, DOUBLE_TAP fires onDoubleTap, WRIST_ON/WRIST_OFF toggle worn state.

The BATTERY_LEVEL event has a fixed decoded layout:

  • soc% = u16@17 / 10 (WHOOP 4.0) or direct at payload offset 2 (WHOOP 5.0)
  • mV = u16@21 (4.0) or u16 LE at payload+4 (5.0)
  • charging = u8@26 & 1 (4.0) or u8 at payload+8 (5.0)

8. Commands (Safe Subset)

NOOP exposes a deliberately safe command set in WhoopCommand. Commands are built as frames and written to the command characteristic.

Common Commands

Code Command Payload Purpose
1 LINK_VALID link keep-alive
3 TOGGLE_REALTIME_HR [0x01] or [0x00] start/stop live HR (type-40)
7 REPORT_VERSION_INFO firmware versions
10 SET_CLOCK [secs u32 LE][subsecs u32 LE] set strap RTC (UTC)
11 GET_CLOCK empty read RTC → clock correlation
22 SEND_HISTORICAL_DATA [0x00] begin offload of type-47 store
23 HISTORICAL_DATA_RESULT [0x01] + end_data(8) ack a HISTORY_END chunk / advance trim
26 GET_BATTERY_LEVEL [0x00] battery %; also the bond write
34 GET_DATA_RANGE [0x00] strap's stored oldest/newest record range
35 GET_HELLO_HARVARD [0x00] identity/version hello
63 SEND_R10_R11_REALTIME [0x00] off / [0x01] on real type-43 raw-stream control
66 SET_ALARM_TIME [0x01] + epoch u32 LE + [0x00, 0x00] arm firmware alarm
67 GET_ALARM_TIME [0x01] read armed alarm
68 RUN_ALARM [0x01] app-driven alarm now
69 DISABLE_ALARM [0x01] disarm firmware alarm
76 GET_ADVERTISING_NAME_HARVARD [0x00] advertised name
79 RUN_HAPTICS_PATTERN [patternId, loops, 0, 0, 0] buzz a preset haptic (pattern 2 = alarm buzz)
80 GET_ALL_HAPTICS_PATTERN enumerate preset patterns
81 / 82 START_RAW_DATA / STOP_RAW_DATA [0x01] raw-data collection toggle (note: doesn't control type-43)
96 / 97 ENTER_HIGH_FREQ_SYNC / EXIT_HIGH_FREQ_SYNC [0x00] high-freq offload mode
98 GET_EXTENDED_BATTERY_INFO extended battery info (mV, etc.)

Destructive Commands (Deliberately Excluded)

These exist on the wire but are not exposed by WhoopCommand because they are irreversible:

Code Command Hazard
25 FORCE_TRIM discards stored data
29 REBOOT_STRAP reboots
32 POWER_CYCLE_STRAP power-cycles
36–38 Firmware load firmware write
45 ENTER_BLE_DFU DFU bootloader
99 RESET_FUEL_GAUGE resets battery fuel gauge

9. Sensor Inventory

The WHOOP strap (4.0 and 5.0) exposes these sensors and actuators:

Sensor / Actuator Protocol Surface NOOP Use
PPG optical (green, red/IR LEDs) type-47 ppg_green / ppg_red_ir / ambient; type-43 optical variant (~437 Hz) HR, SpO₂, respiratory rate (derived on-device)
Accelerometer (3-axis) type-47 gravity_x/y/z (f32); type-43 IMU accelX/Y/Z (~100 Hz, 1/4096 g/LSB) gravity, motion detection, HR variability context
Gyroscope type-43 IMU gyroX/Y/Z (±2000 dps @ 0.061 deg/s/LSB) motion; optionally captured for research
Skin temperature type-47 skin_temp_raw; event TEMPERATURE_LEVEL derived °C on-device
Double-tap (capacitive) event DOUBLE_TAPonDoubleTap callback user input / app trigger
Wrist detection events WRIST_ON/WRIST_OFF; type-47 skin_contact worn/unworn state
Haptic motor RUN_HAPTICS_PATTERN / STOP_HAPTICS notifications, alarms
Battery standard 2A19; event BATTERY_LEVEL; command responses battery % and voltage (mV)

Not present: microphone, speaker, GPS, display, cellular. All feedback to the wearer is via the single haptic motor.


10. Extending the Decoder

The decoder is data-driven: most protocol definition lives in whoop_protocol.json, not in code.

Adding a New Field

  1. Static field on an existing packet type: add an entry to that packet's fields array in whoop_protocol.json with off (byte offset), len, dtype (u8/u16/u32/i16/f32), name, cat (category), optional enum, optional note. The parser picks it up automatically.

  2. New enum value: add it under enums.PacketType, enums.EventNumber, enums.CommandNumber, or enums.MetadataType. Names are resolved via the schema.

  3. Irregular layout (variable R-R count, IMU/optical blocks, per-version records): add a closure to registerPostHooks() in PostHooks.swift and reference it via the packet's post key. The hook receives (FieldBuilder, frame, length, schema) and writes into fb.parsed.

  4. New historical record version: add a key under HISTORICAL_DATA.versions with the version byte as the key. Use "ref" to inherit another version's layout, or define new fields.

  5. New durable row: define the struct in Streams.swift, add it to Streams, and emit it from extractStreams / extractHistoricalStreams. The GRDB persistence layer lives in WhoopStore.

  6. New command: add a case to WhoopCommand in Commands.swift with its raw wire value. Keep it reversible and non-destructive.

Best Practices

  • Back every change with a real-device capture. Golden-frame fixtures live in Tests/WhoopProtocolTests/Resources/ (frames.json, golden.json, historical_golden.json, etc.). Parity tests assert the Swift decoder reproduces them exactly.
  • Prefer real captures over invented offsets. Unmapped regions are kept raw (unit: "raw") and documented rather than guessed.
  • Map WHOOP 5.0 offsets from real captures. The 5.0 inner record starts at offset 8 (not 4). Don't assume a 4.0 field transfers with a +4 offset shift — many do, but some (like version 18 HR) don't. Always verify against a real capture.

11. Code Organization

Path Responsibility
Packages/WhoopProtocol/Sources/WhoopProtocol/Framing.swift CRC8/CRC16/CRC32, verifyFrame, Reassembler
Packages/WhoopProtocol/Sources/WhoopProtocol/Interpreter.swift parseFrame (4.0 + 5.0), ParsedFrame, field builder
Packages/WhoopProtocol/Sources/WhoopProtocol/DeviceFamily.swift UUID strings, header-CRC kind, CLIENT_HELLO, puffin type aliasing
Packages/WhoopProtocol/Sources/WhoopProtocol/Schema.swift JSON schema model + loadSchema()
Packages/WhoopProtocol/Sources/WhoopProtocol/PostHooks.swift per-type irregular-field decoders
Packages/WhoopProtocol/Sources/WhoopProtocol/HistoricalMeta.swift classifyHistoricalMeta (START/END/COMPLETE)
Packages/WhoopProtocol/Sources/WhoopProtocol/Resources/whoop_protocol.json canonical enums + packet layouts
Strand/BLE/BLEManager.swift CoreBluetooth transport, bond, connect, backfill orchestration
Strand/BLE/Commands.swift safe WhoopCommand set + frame builder
Strand/BLE/FrameRouter.swift decode → LiveState (UI)
Strand/BLE/StandardHeartRate.swift 2A37 HR/R-R parser
Strand/Collect/Backfiller.swift historical-offload state machine + safe-trim invariant

The WhoopProtocol package never imports CoreBluetooth — it exposes UUIDs as plain strings so the protocol code runs unchanged in tests, CLI tools, and simulators. Only BLEManager turns strings into CBUUIDs.


12. Related Pages


Reverse-engineering credit: johnmiddleton12/my-whoop (WHOOP 4.0) and b-nnett/goose (WHOOP 5.0). This is an independent interoperability project for the user's own device and data; it is not affiliated with WHOOP and is not a medical device.

Clone this wiki locally