Python provides powerful libraries for Bluetooth Low Energy communication. The Whoop reverse engineering project primarily uses pygatt and bleak for device interaction.
pygatt
pygatt is a Python library that wraps BlueZ’s gatttool, providing a simple API for BLE device interaction.
Installation
Scanning for Devices
Discover nearby BLE devices:
from pygatt import GATTToolBackend, BLEAddressType
adapter = GATTToolBackend(hci_device='hci0')
adapter.start()
for device in adapter.scan(timeout=5):
print(device)
This scans for 5 seconds and prints all discovered devices with their MAC addresses and names.
Connecting to Whoop
Once you have the device MAC address:
device = adapter.connect('XX:XX:XX:XX:XX:XX', address_type=BLEAddressType.random)
Whoop 4.0 uses a random BLE address type. Always specify address_type=BLEAddressType.random when connecting.
Writing Commands
Send commands to the CMD_TO_STRAP characteristic:
import struct
import time
# Set alarm to ring in 10 seconds
unix_time = int(time.time()) + 10
unix_hex = struct.pack('<I', unix_time).hex()
package = f"aa10005723704201{unix_hex}00000000f226a8bd"
device.char_write(
"61080002-8d6d-82b8-614a-1c8cb0f8dcc6", # CMD_TO_STRAP UUID
bytearray.fromhex(package),
wait_for_response=False
)
CRC Calculation
Whoop packets include a CRC-32 checksum. Use crccheck to calculate:
import struct
from crccheck.crc import Crc32Base
def calculate_checksum(message):
"""Calculate Whoop CRC-32 checksum for a packet."""
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()
# Example: building an alarm packet
unix_time = int(time.time()) + 10
unix_hex = struct.pack('<I', unix_time).hex()
package_data = f"aa10005723704201{unix_hex}00000000"
checksum = calculate_checksum(bytearray.fromhex(package_data))
full_package = f"{package_data}{checksum}"
device.char_write(
"61080002-8d6d-82b8-614a-1c8cb0f8dcc6",
bytearray.fromhex(full_package),
wait_for_response=False
)
Known Limitation
Direct writes from Python using pygatt do not consistently work with Whoop 4.0. Commands may:
- Throw exceptions
- Silently fail
- Work intermittently
For reliable command execution, use gatttool instead. pygatt is still useful for scanning, connecting, and receiving notifications.
bleak
bleak is a modern, cross-platform Python BLE library that works on Windows, macOS, and Linux.
Installation
Receiving Notifications
bleak excels at receiving notifications from BLE characteristics. Example script for monitoring heart rate:
python3 enable_notifications.py --address XX:XX:XX:XX:XX:XX 00002a37-0000-1000-8000-00805f9b34fb
This connects to the device and subscribes to the standard BLE Heart Rate Service characteristic (0x2A37).
Heart rate broadcasting must be enabled on the device first. If the device is not discoverable, you’ll get: ERROR: could not find device with address
Listening to Custom Characteristics
Monitor the DATA_FROM_STRAP characteristic for activity data:
python3 enable_notifications.py --address XX:XX:XX:XX:XX:XX 61080005-8d6d-82b8-614a-1c8cb0f8dcc6
After enabling activity mode with gatttool, this will print sensor data every second:
aa1800ff2802ad896566f06542016706000000000000010101...
aa1800ff2802ae896566f86043000000000000000000010101...
aa1800ff2802af896566085c42000000000000000000010101...
Writing with bleak
Like pygatt, bleak’s write operations are unreliable with Whoop 4.0. Commands sent via bleak may not trigger device responses consistently.
Recommended approach:
- Use bleak for notifications and data reception
- Use gatttool for sending commands
The most reliable workflow combines both tools:
Start notification listener with Python
python3 enable_notifications.py --address XX:XX:XX:XX:XX:XX 61080005-8d6d-82b8-614a-1c8cb0f8dcc6
Send command with gatttool
In another terminal:sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX --char-write -a 0x0010 -n aa0800a8238c03017d5ec627
Observe notifications
The Python script will receive and print notifications triggered by the gatttool command.
Example: Toggling Heart Rate Broadcast
from pygatt import GATTToolBackend, BLEAddressType
import subprocess
import time
# Connect with pygatt to verify device presence
adapter = GATTToolBackend(hci_device='hci0')
adapter.start()
MAC_ADDRESS = 'XX:XX:XX:XX:XX:XX'
device = adapter.connect(MAC_ADDRESS, address_type=BLEAddressType.random)
print("Device connected")
# Use gatttool to send enable command
enable_cmd = "aa0800a823080e016c935474" # Enable heart rate broadcast
subprocess.run([
'sudo', 'gatttool',
'-i', 'hci0',
'-t', 'random',
'-b', MAC_ADDRESS,
'--char-write',
'-a', '0x0010',
'-n', enable_cmd
])
print("Heart rate broadcast enabled")
time.sleep(2)
# Now heart rate service should be available
print("Device is now broadcasting heart rate")
Reverse Engineering CRC
If you need to reverse engineer checksums for other BLE devices, use crcbeagle:
git clone https://github.com/colinoflynn/crcbeagle
cd crcbeagle
touch examine.py
Create examine.py with your packet samples:
from crcbeagle.crcbeagle import CRCBeagle
crc = CRCBeagle()
def hex_to_int(hex):
return [i for i in bytearray.fromhex(hex)]
data = """
aa100057236d4201d036656600000000f62deb81
aa100057236e42010c376566000000001023ccef
aa100057236f4201207d656600000000fea1e060
aa100057237042015011656600000000f226a8bd
""".split('\n')
data = [i for i in data if i]
checksums = [i[len(i) - 8:] for i in data]
data = [i[:len(i) - 8] for i in data]
crc.search(
[hex_to_int(i) for i in data],
[hex_to_int(i) for i in checksums]
)
Run the script to discover CRC parameters:
Summary
| Task | Best Tool |
|---|
| Scanning for devices | pygatt |
| Connecting | pygatt or bleak |
| Receiving notifications | bleak |
| Sending commands | gatttool |
| CRC calculation | crccheck (Python) |
| CRC reverse engineering | crcbeagle |