Skip to main content

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:
  1. 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)
    
  2. 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:
  1. Clone crcbeagle
  2. Feed it multiple packet examples
  3. 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:
  1. Enable Android BLE HCI snoop logging:
    • Settings → Developer Options → Enable Bluetooth HCI snoop log
  2. Extract logs:
    adb bugreport logs
    unzip logs.zip
    wireshark FS/data/log/bt/btsnoop_hci.log
    
  3. Or use live capture:
    adb devices -l  # Verify connection
    sudo wireshark  # Select "Android Bluetooth Btsnoop"
    
  4. 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:
  1. Start with read-only commands (category 0x16 data retrieval)
  2. Test harmless toggles (category 0x0e heart rate broadcast)
  3. Test with short alarm times (10-30 seconds away) for quick feedback
  4. 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:
  1. Verify your BLE adapter: Some Bluetooth adapters have better Linux support than others
  2. Check device battery: Low battery can cause erratic behavior
  3. Try airplane mode: Disconnect from official app to prevent conflicts
  4. Capture traffic: Use Wireshark to compare your packets with official app packets
  5. 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.

Build docs developers (and LLMs) love