Skip to main content
Once you’ve discovered the Whoop’s BLE services, you can connect to the device and start sending commands. There are several methods available, each with different trade-offs.

Connection Methods

The Whoop 4.0 uses BLE random addressing, so you must specify the address type when connecting.
The Whoop’s MAC address changes periodically as part of BLE privacy features. You may need to rescan for the device.

Method 1: Python with pygatt

pygatt provides a Python interface to gatttool and works well for reading data and subscribing to notifications.

Basic Connection

from pygatt import GATTToolBackend, BLEAddressType

# Initialize the adapter
adapter = GATTToolBackend(hci_device='hci0')
adapter.start()

try:
    # Connect to Whoop (replace with your device's MAC)
    device = adapter.connect(
        'XX:XX:XX:XX:XX:XX',
        address_type=BLEAddressType.random
    )
    
    print("Connected to Whoop!")
    
    # Your code here
    
finally:
    adapter.stop()

Subscribing to Notifications

To receive data from the device:
from pygatt import GATTToolBackend, BLEAddressType
import time

def handle_data(handle, value):
    """Callback for notifications"""
    print(f"Received data on handle {handle}: {value.hex()}")

adapter = GATTToolBackend(hci_device='hci0')
adapter.start()

try:
    device = adapter.connect('XX:XX:XX:XX:XX:XX', address_type=BLEAddressType.random)
    
    # Subscribe to DATA_FROM_STRAP
    device.subscribe(
        "61080005-8d6d-82b8-614a-1c8cb0f8dcc6",
        callback=handle_data
    )
    
    print("Listening for notifications... (Ctrl+C to stop)")
    while True:
        time.sleep(1)
        
except KeyboardInterrupt:
    print("\nStopped")
finally:
    adapter.stop()
The subscribe() method automatically enables notifications on the characteristic.

Writing Commands (Limited Success)

While pygatt can write to characteristics, command execution is unreliable:
import struct
from pygatt import GATTToolBackend, BLEAddressType

adapter = GATTToolBackend(hci_device='hci0')
adapter.start()

device = adapter.connect('XX:XX:XX:XX:XX:XX', address_type=BLEAddressType.random)

# Example: Try to set an alarm
package = "aa10005723704201126d6566000000003d59d8fd"

device.char_write(
    "61080002-8d6d-82b8-614a-1c8cb0f8dcc6",  # CMD_TO_STRAP
    bytearray.fromhex(package),
    wait_for_response=False
)

adapter.stop()
pygatt writes often fail or work inconsistently. Use gatttool directly for more reliable command execution.
gatttool is a command-line tool that provides the most reliable way to send commands to the Whoop.

Basic Command Syntax

sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX --char-write -a 0x0010 -n <hex_data>
Parameters:
  • -i hci0 - Bluetooth adapter to use
  • -t random - Address type (required for Whoop)
  • -b XX:XX:XX:XX:XX:XX - Device MAC address
  • -a 0x0010 - Handle for CMD_TO_STRAP characteristic
  • -n <hex_data> - Command payload in hex

Example: Enable Heart Rate Broadcast

# Turn ON heart rate broadcast
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX \
  --char-write -a 0x0010 -n aa0800a823080e016c935474

# Turn OFF heart rate broadcast  
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX \
  --char-write -a 0x0010 -n aa0800a823070e00c7e40f08
After enabling heart rate broadcast, you can read heart rate from any BLE heart rate monitor app using the standard service UUID 00002a37-0000-1000-8000-00805f9b34fb.

Example: Set Alarm

To set an alarm to ring in 10 seconds:
import time
import struct
from crccheck.crc import Crc32Base

# Calculate CRC-32 for Whoop packets
def my_crc(message):
    crc = Crc32Base
    crc._poly = 0x4C11DB7
    crc._reflect_input = True
    crc._reflect_output = True
    crc._initvalue = 0x0
    crc._xor_output = 0xF43F44AC
    output_int = crc.calc(message)
    return struct.pack("<I", output_int).hex()

# Create alarm packet
unix_time = int(time.time()) + 10
unix_hex = struct.pack('<I', unix_time).hex()

package_data = f"aa10005723704201{unix_hex}00000000"
checksum = my_crc(bytearray.fromhex(package_data))
package = f"{package_data}{checksum}"

print(f"Command: {package}")
print(f"Run: sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX --char-write -a 0x0010 -n {package}")
All commands require a valid CRC-32 checksum. See the Protocol Reference for details.

Method 3: Python with bleak

bleak is a cross-platform BLE library that works on Windows, macOS, and Linux.

Reading Heart Rate

Using the example from the bleak repository:
import asyncio
from bleak import BleakClient

def notification_handler(sender, data):
    """Callback for heart rate notifications"""
    # Heart rate is the second byte
    heart_rate = data[1]
    print(f"Heart Rate: {heart_rate} bpm")

async def main():
    address = "XX:XX:XX:XX:XX:XX"
    
    async with BleakClient(address) as client:
        print(f"Connected: {client.is_connected}")
        
        # Subscribe to heart rate service
        await client.start_notify(
            "00002a37-0000-1000-8000-00805f9b34fb",
            notification_handler
        )
        
        print("Listening for heart rate... (10 seconds)")
        await asyncio.sleep(10.0)
        
        await client.stop_notify("00002a37-0000-1000-8000-00805f9b34fb")

asyncio.run(main())
The Whoop must have “Broadcast Heart Rate” enabled for this to work. By default, it’s disabled to save battery.

Writing Commands with bleak

import asyncio
from bleak import BleakClient

async def send_command():
    address = "XX:XX:XX:XX:XX:XX"
    
    async with BleakClient(address) as client:
        # Enable heart rate broadcast
        command = bytes.fromhex("aa0800a823080e016c935474")
        
        await client.write_gatt_char(
            "61080002-8d6d-82b8-614a-1c8cb0f8dcc6",  # CMD_TO_STRAP
            command,
            response=False
        )
        
        print("Command sent")

asyncio.run(send_command())
Like pygatt, bleak may have reliability issues when writing commands. Results vary by platform and Bluetooth adapter.

Method 4: BLE Scanner App (Quick Testing)

For quick command testing without code, you can use the BLE Scanner app:
1

Connect to Whoop

Open BLE Scanner and connect to your Whoop device.
2

Navigate to CMD_TO_STRAP

Find the custom service and select the 61080002 characteristic.
3

Write Hex Data

Tap the write button and enter your command in hex format.
4

Send Command

Tap send and observe the device response.
This method is proven to work reliably and is great for testing commands before implementing them in code.

Troubleshooting

”Could not find device” Error

  • Ensure the device is nearby and not connected to the app
  • The Whoop can only maintain one BLE connection at a time
  • Try rescanning - the MAC address may have changed

Commands Not Working

  • Verify the CRC-32 checksum is correct
  • Check that you’re using the right handle (0x0010 for CMD_TO_STRAP)
  • Ensure you specified -t random for the address type
  • Try using gatttool or the BLE Scanner app instead

Connection Drops

  • Stay within ~10 meters of the device
  • Avoid Bluetooth interference from other devices
  • The Whoop may disconnect after inactivity

Permission Denied

# Add your user to the bluetooth group
sudo usermod -a -G bluetooth $USER

# Or run with sudo
sudo python3 your_script.py
For the most reliable reverse engineering workflow:
  1. Use BLE Scanner app for initial command discovery and testing
  2. Use gatttool for reliable command execution from scripts
  3. Use Python (pygatt or bleak) for reading notifications and data analysis
  4. Use Wireshark to capture and verify all communication
You can combine methods - for example, use gatttool via Python’s subprocess module for reliable writes while using pygatt for notifications.

Next Steps

Now that you can connect to the device, learn about the packet structure and available commands.

Build docs developers (and LLMs) love