This is part of a few posts on trying to understand my treadmill’s Bluetooth.

  1. Getting Data (this post)
  2. Analyzing Writes
  3. Sending Data
  4. Analyzing Reads
  5. Wrapping Up (this post)

This will be split across a few posts since reading my writing for more than 10 minutes would give anyone a headache.

Where we begin

We recently got a treadmill ( NordicTrack T Series 6.5S ) that came with a free iFit subscription. I waited until day 28 of the 30-day trial subscription to open it, and although it was cool, I didn’t see much value in a paying for it (knowing me, I’d end up looking at the data, nodding and then continuing my pattern of randomly changing the speed/incline).

However just before the subscription lapsed, I realized I had never really thought much about how the app and treadmill communicated. How did the treadmill report its current state to the app? How did the app tell the treadmill to change the speed? Sure it was over Bluetooth, but I had never done anything with Bluetooth and therefore had no idea what that looked like.

Therefore rather than pay $15 a month for a product built by a professional team with direct knowledge of the protocols, I decided to spend an inordinate amount of time seeing if I could make my own version. The final goal - write a program that dumps the state of the treadmill to a CSV, e.g.:

incline,speed,distance,time
3.5,6.0,0.0,0
3.5,6.0,0.0,0
3.5,6.0,0.0,0
3.5,6.0,0.001,1

Getting the data

The first challenge is getting the data. To aid analysis we’re going to need to capture some sample communication between the app and the treadmill. First, let’s enable Bluetooth HCI snoop logging to dump a capture of traffic that we can analyze in Wireshark. Next, we’ll connect the app to the treadmill and:

  1. Increase the speed in 0.1 MPH increments from 1.0 to 1.3
  2. Decrease the speed in 0.1 MPH increments from 1.3 to 1.1
  3. Increase the incline in 0.5% increments from 0 to 3.0

Why these nonsensical ranges with minimal data? Well:

  • Imagine this being the first sample and putting it in /home
  • Now imagine putting every subsequent sample (with more ‘scientific’ measurements) in /tmp, for some reason
  • <iFit subscription expires>
  • Then restart your computer for unrelated reasons, clearing /tmp
  • Last, realize you can no longer get samples without paying for a subscription

So that’s what we got as a sample - not a ton but should be enough to analyze, so let’s boot up Wireshark. I don’t expect anything to make sense (I have roughly 30 minutes of Bluetooth ‘experience’ at this point), but there should be enough to go on.

After filtering the traffic to just the packets from the phone to the treadmill and looking around, it seems like there is enough chatter to indicate we are actually looking at the right data. However we immediately hit another snag: it looks like the commands to change the state are split across multiple packets (this is a snag because our intense 10 minutes of research only returned cases where all of the information was in a single packet).

Looks like this will require some deeper analysis (read: guessing) in bulk, so let’s switch to tshark and dump all the Bluetooth ATT packets (0x0004 below) sent from the phone to the treadmill:

$ tshark \
    -r ./btsnoop_hci.log \
    -Y '(btl2cap.cid == 0x0004) && (bluetooth.src == <phone MAC>) && (bluetooth.dst == <treadmill MAC>)' \
    -2 \
    -R btatt.handle \
    -T fields \
    -Eheader=yes \
    -e frame.time_relative -e btatt.handle -e btatt.value \
    | head
frame.time_relative	btatt.handle	btatt.value
49.334003000	0x0000000c	0100
49.617787000	0x0000000e	fe020802
49.764954000	0x0000000e	ff08020402040204818700000000000000000000
50.032920000	0x0000000e	fe020802
50.163543000	0x0000000e	ff08020402040404808800000000000000000000
50.452475000	0x0000000e	fe020802
50.521963000	0x0000000e	ff08020402040404889000000000000000000000
50.787802000	0x0000000e	fe020a02
50.880956000	0x0000000e	ff0a0204020602068200008a0000000000000000

The majority of the file follows this pattern. Following the theory that a single command is split across multiple packets, let’s toss in more conjecture - 0xfe and 0xff appear to signal the start/end of a sequence. Some sequences contain other packets in between, but for every 0xfe there is an 0xff nearby. Let’s write a script that puts each sequence on its own line:

with open('packets.txt') as f, open('output.txt', 'w') as o:
    current_line = []
    for line in f.readlines():
        if line.startswith('fe'):
            if current_line:
                o.write(f'{" ".join(current_line)}\n')
            current_line = [line.strip()]
        else:
            current_line.append(line.strip())

Which yields a file like:

$ head output.txt
0100
fe020802 ff08020402040204818700000000000000000000
fe020802 ff08020402040404808800000000000000000000
fe020802 ff08020402040404889000000000000000000000
fe020a02 ff0a0204020602068200008a0000000000000000
fe020a02 ff0a0204020602068400008c0000000000000000
fe020802 ff08020402040204959b00000000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe021903 001202040215041502000f001000d81c480000e0 ff070000001000086e0000000000000000000000
fe021903 0012020402150415020e00000000000000000000 ff070000001001003a0000000000000000000000

Alright, that’s a decent start - the data is much easier to read/visually diff, so with some mildly educated guessing we can probably find some patterns. Next up, let’s try to figure out which commands are sending writes .