Connection Issues
Commands Work from Phone but Not Computer
Symptoms:
- Commands succeed when sent via BLE scanner app on Android
- Same commands fail or work inconsistently when sent from Linux computer
- Python libraries like
pygatt and bleak fail to send commands reliably
Root Cause:
Unknown. The device exhibits different behavior depending on the BLE stack used.
Workaround:
Use gatttool directly instead of high-level Python libraries:
# Replace with your device MAC address
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX --char-write -a 0x0010 -n aa0800a823080e016c935474
This method works “randomly” - commands may need to be sent multiple times. Success rate varies but is generally sufficient for experimentation.
Cannot Find Device for Heart Rate Monitoring
Symptoms:
bleak or other BLE libraries report “could not find device with address ‘XX:XX:XX:XX:XX:XX’”
- Scanning finds no devices or doesn’t find your Whoop
Solution:
Enable heart rate broadcasting first:
# Enable broadcast using gatttool
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX --char-write -a 0x0010 -n aa0800a823080e016c935474
# Now the device can be found
python3 enable_notifications.py --address XX:XX:XX:XX:XX:XX 00002a37-0000-1000-8000-00805f9b34fb
The device must have heart rate broadcasting enabled (category 0x0e command) before it advertises the standard BLE Heart Rate Service.
Python Libraries Fail to Write Characteristics
Symptoms:
pygatt commands execute but device doesn’t respond
- No errors thrown but alarm doesn’t vibrate / broadcast doesn’t enable
Example of failing code:
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)
# This may not work reliably:
device.char_write(
"61080002-8d6d-82b8-614a-1c8cb0f8dcc6",
bytearray.fromhex("aa0800a823080e016c935474"),
wait_for_response=False
)
Workaround:
Use gatttool with handle notation:
sudo gatttool -i hci0 -t random -b XX:XX:XX:XX:XX:XX --char-write -a 0x0010 -n aa0800a823080e016c935474
Command Reliability
Random Command Failures
Symptoms:
- Same command works sometimes, fails other times
- No clear pattern to success/failure
- More common with computer-based tools than phone apps
Known Workarounds:
-
Retry logic: Send commands 2-3 times with small delays
import time
import subprocess
def send_command_reliable(mac, command_hex, retries=3):
for i in range(retries):
subprocess.run([
'sudo', 'gatttool',
'-i', 'hci0',
'-t', 'random',
'-b', mac,
'--char-write',
'-a', '0x0010',
'-n', command_hex
])
time.sleep(0.5)
-
Use phone for critical operations: For production use cases, Android BLE stack appears more reliable
There is no guaranteed fix for random failures when using desktop BLE stacks. This appears to be a quirk of the device’s BLE implementation.
Packet Count Validation
Symptoms:
- Concerned that reusing packet count values will cause issues
- Documentation mentions packet count increments
Solution:
Packet count is not validated. Testing confirms:
# These both work, even with same packet count
sudo gatttool [...] -n aa0800a823070e00c7e40f08 # count: 0x07
sudo gatttool [...] -n aa0800a823070e016c935474 # count: 0x07 (reused)
You can safely ignore the packet count field when sending commands. Use any value (e.g., always use 0x00).
Checksum Issues
Commands Rejected / No Response
Symptoms:
- Device doesn’t respond to custom commands
- Works when using exact hex from Wireshark capture
- Fails when trying to modify timestamp or parameters
Cause:
Incorrect checksum calculation.
Solution:
Ensure you’re using the correct CRC-32 parameters:
import struct
from crccheck.crc import Crc32Base
def whoop_checksum(data):
crc = Crc32Base
crc._poly = 0x4C11DB7
crc._reflect_input = True
crc._reflect_output = True
crc._initvalue = 0x0
crc._xor_output = 0xF43F44AC # Critical: not standard CRC-32!
output_int = crc.calc(data)
return struct.pack("<I", output_int)
# Example: Set alarm 10 seconds from now
import time
unix_time = int(time.time()) + 10
unix_hex = struct.pack('<I', unix_time).hex()
packet = f"aa10005723704201{unix_hex}00000000"
checksum = whoop_checksum(bytearray.fromhex(packet))
full_command = packet + checksum.hex()
print(f"Command: {full_command}")
Standard CRC-32 calculators (like crccalc.com) will not work. The XOR output value 0xF43F44AC is non-standard and required.
Online CRC Calculators Don’t Match
Symptoms:
- Used crccalc.com or similar tool
- Checksum doesn’t match captured packets
Explanation:
Whoop uses custom CRC-32 parameters, specifically a non-standard XOR output value.
Solution:
Use the Python code above or reverse-engineer it yourself:
- Clone crcbeagle
- Feed it multiple packet examples
- It will output the custom CRC parameters
See the Packet Formats page for complete checksum implementation.
Data Sync Issues
Sync Fails on First Attempt
Symptoms:
- Official app requests journal data
- Sync fails after first request
- App requests journal again and succeeds on second try
Explanation:
This is normal behavior observed in the official app. The device may need initialization before full sync.
Workaround:
Implement retry logic in your sync code:
def sync_device(device):
for attempt in range(2):
try:
request_journal(device)
data = retrieve_batches(device)
return data
except SyncError:
if attempt == 0:
continue # Retry once
else:
raise
Missing Batch Numbers
Symptoms:
- Notifications received on
DATA_FROM_STRAP
- Cannot find batch number in the data
Solution:
The batch number is in the last notification before sending category 0x17 commands:
Header Packet Flag Unix Time Unknown Batch Number Unknown Checksum
aa1c00ab31 18 02 f65c7066 804043000000 2e470100 04000000000000 7f873cf3
└─ Extract this (little-endian)
Extract bytes at offset 0x11 (4 bytes, little-endian):
last_notification = bytearray.fromhex("aa1c00ab311802f65c70668040430000002e47010004000000000000007f873cf3")
batch_number = struct.unpack('<I', last_notification[0x11:0x15])[0]
print(f"Batch: {batch_number}") # Output: 83758
Development Tips
Debugging BLE Traffic
Recommended setup:
-
Enable Android BLE HCI snoop logging:
- Settings → Developer Options → Enable Bluetooth HCI snoop log
-
Extract logs:
adb bugreport logs
unzip logs.zip
wireshark FS/data/log/bt/btsnoop_hci.log
-
Or use live capture:
adb devices -l # Verify connection
sudo wireshark # Select "Android Bluetooth Btsnoop"
-
Filter for relevant packets:
btatt && btatt.handle == 0x10 # Commands to device
btatt && btatt.handle == 0x18 # Data from device
Testing Commands Safely
Before sending erase or reboot commands, ensure you’ve backed up your data or are working with a test device.
Safe testing order:
- Start with read-only commands (category 0x16 data retrieval)
- Test harmless toggles (category 0x0e heart rate broadcast)
- Test with short alarm times (10-30 seconds away) for quick feedback
- Only test erase/reboot after confirming other commands work
Verifying Command Success
Enable notifications to verify:
import asyncio
from bleak import BleakClient
async def monitor_responses(address):
async with BleakClient(address) as client:
# Monitor command responses
await client.start_notify(
"61080003-8d6d-82b8-614a-1c8cb0f8dcc6", # CMD_FROM_STRAP
lambda sender, data: print(f"Response: {data.hex()}")
)
# Monitor data stream
await client.start_notify(
"61080005-8d6d-82b8-614a-1c8cb0f8dcc6", # DATA_FROM_STRAP
lambda sender, data: print(f"Data: {data.hex()}")
)
await asyncio.sleep(60) # Monitor for 60 seconds
asyncio.run(monitor_responses("XX:XX:XX:XX:XX:XX"))
Still Having Issues?
If you’re experiencing issues not covered here:
- Verify your BLE adapter: Some Bluetooth adapters have better Linux support than others
- Check device battery: Low battery can cause erratic behavior
- Try airplane mode: Disconnect from official app to prevent conflicts
- Capture traffic: Use Wireshark to compare your packets with official app packets
- Consult the community: This is reverse-engineered research - community knowledge is evolving
For hardware-specific issues, confirm your Bluetooth adapter supports BLE 4.0+ and random address types.