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}")
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:
Connect to Whoop
Open BLE Scanner and connect to your Whoop device.
Navigate to CMD_TO_STRAP
Find the custom service and select the 61080002 characteristic.
Write Hex Data
Tap the write button and enter your command in hex format.
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
Recommended Approach
For the most reliable reverse engineering workflow:
- Use BLE Scanner app for initial command discovery and testing
- Use
gatttool for reliable command execution from scripts
- Use Python (
pygatt or bleak) for reading notifications and data analysis
- 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.