Reversing Treadmill Bluetooth - Getting Data
This is part of a few posts on trying to understand my treadmill’s Bluetooth.
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:
- Increase the speed in 0.1 MPH increments from 1.0 to 1.3
- Decrease the speed in 0.1 MPH increments from 1.3 to 1.1
- 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 .