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

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

Our Treadmill ‘Hello World’

Enough wild guessing, how do we test this? Remember, one of us (me) has minimal Bluetooth knowledge, so we aren’t going to be shackled by ‘hard-won knowledge’ and ‘best practices’. Let’s just do some quick searching for linux bluetooth and try to find the simplest way to send Bluetooth LE packets.

Setting up gatttool

I’ll spare you (side note: who will ever read this?) an overview of Bluetooth LE/GATT/etc. - there are plenty of other much better resources for that. To make it brief, gatttool seems to be popular (and deprecated ?), and after a quite a bit of bumbling around, we can do some stuff!

We can start an interactive session with:

$ gatttool -b <treadmill MAC> -I -t random

The -t random came from the comments here . Unfortunately I couldn’t find a clear (to me) explanation for what it means - it sets the LE address to random per the docs, but nothing about why that is needed (I assume it means it is randomizing the address of the client when connecting, which may mean there is different behavior when providing a constant address?). We could dig into the code itself, but that doesn’t feel useful.

Starting with the basics, we can connect and list the services (primary):

$ gatttool -b <MAC> -I -t random
[MAC][LE]> connect
Attempting to connect to MAC
Connection successful
[MAC][LE]> primary
attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0008, end grp handle: 0x0008 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0009, end grp handle: 0x000e uuid: 00001533-1412-efde-1523-785feabcd123
attr handle: 0x000f, end grp handle: 0x0016 uuid: 00001530-1212-efde-1523-785feabcd123
attr handle: 0x0017, end grp handle: 0xffff uuid: 00001400-555e-e99c-e511-f9f4f8daeb24

From the packet capture, we know that:

  • The treadmill is sending values back on handle 0x000b

    phone-writing

  • The phone is sending commands on handle 0x000e

    phone-writing

Based on the handle ranges from the primary command, this is from service 00001533-1412-efde-1523-785feabcd123. Let’s inspect all of the characteristics in that service:

[MAC][LE]> characteristics 0x0009 0x000e                                          
handle: 0x000a, char properties: 0x12, char value handle: 0x000b, uuid: 00001535-1412-efde-1523-785feabcd123
handle: 0x000d, char properties: 0x0a, char value handle: 0x000e, uuid: 00001534-1412-efde-1523-785feabcd123

Per this O’Reilly article we know that a Client Characteristic Configuration Descriptor allows us to enable/disable server-initiated updates and has a standard UUID of 2902 (plus the standard trailing 16 bits, see here ).

Let’s see if one is sitting within our service:

[MAC][LE]> char-read-uuid 2902
handle: 0x000c 	 value: 03 00 
handle: 0x0014 	 value: 03 00 
handle: 0x001c 	 value: 03 00 
handle: 0x0021 	 value: 03 00

0x000c fits the bill, and if we look back at the very first packet in our phone->treadmill conversation, we can see it is writing the value 0x0100 to that handle:

cccd

That makes sense - this is the phone telling the treadmill to subscribe to notifications, and based on the characteristics above / the Example service from the O’Reilly docs, these notifications must be for handle 0x000b. From this we can assume that those 0x000b values probably contain the treadmill state, but we’ll get to that later.

Replaying a session

We have a general feel for the service, so let’s wrap this up with a test - can we actually use this to write a value? Let’s see if we can send the two packets that increased the speed to 1.0:

fe020d02 ff0d020402090409020101a00000b10000000000

Remember how our capture had a lot of chatter at the beginning, prior to the commands being sent? It looked like this:

0100
fe020d02 ff0d020402090409020101c10000d20000000000
fe020802 ff08020402040204818700000000000000000000
fe020802 ff08020402040404808800000000000000000000
fe020802 ff08020402040404889000000000000000000000
fe020a02 ff0a0204020602068200008a0000000000000000
fe020a02 ff0a0204020602068400008c0000000000000000
fe020802 ff08020402040204959b00000000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe021903 001202040215041502000f001000d81c480000e0 ff070000001000086e0000000000000000000000
fe021903 0012020402150415020e00000000000000000000 ff070000001001003a0000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe022003 00120204021c041c020900004002184000008030 ff0e2a0000c720580200b400580200ee00000000

Rather than trying to understand what this does, let’s take a shortcut - let’s just connect and replay the entire session of phone->treadmill packets. We’ll do this because if we can successfully replay the session:

  1. We don’t actually need to understand the beginning pieces - we can just repeat them whenever we start a connection
  2. We can add/remove sequences and see what breaks

How do we do this? Manually typing is out for two reasons:

  1. I don’t want to type everything manually
  2. Even if I did, I can’t type fast enough - the connection drops after ~10 seconds of not receiving something, so we’d need to constantly send something while also sending commands

We already have all of the packets, and gatttool allows for writing values within an interactive session:

char-write-req 0x000e fe020d02
char-write-req 0x000e ff0d020402090409020101c10000d20000000000

So one hacky way comes to mind:

  1. Hook up the stdin of gatttool to a Unix socket
  2. Write a shell script that writes the commands to the socket

This would solve both problems - no need to type manually and should be fast enough to keep the session alive. There are probably simpler ways, but I don’t do a lot of socket-related stuff so this should be a fun experiment (fun/enjoyable are top priorities - if we were doing this whole thing for practical reasons we’d just buy the app).

Set up the sockets

We’ll use nc to set up the socket. -l + /tmp/treadmill.sock will create a /tmp/treadmill.sock socket and listen for connections. -k keeps the session alive (since we want to keep sending commands).

$ nc -lkU /tmp/treadmill.sock | gatttool -b <MAC> -I -t random

Now we can communicate with the socket:

$ echo "connect" | nc -U /tmp/treadmill.sock -N

Where -N means ‘Shut down after EOF on the input’. We need this because the socket is kept open for gatttool, and our echo command will run but then wait forever for a response. Now, let’s put together a simple shell script that sends commands to the socket, something like:

echo "connect" | nc -U /tmp/treadmill.sock -N
# Give it time to connect.
sleep 2

echo "char-write-req 0x000e fe020d02" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e ff0d020402090409020101c10000d20000000000" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e fe020802" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e ff08020402040204818700000000000000000000" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e fe020802" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e ff08020402040404808800000000000000000000" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e fe020802" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e ff08020402040404889000000000000000000000" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e fe020a02" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e ff0a0204020602068200008a0000000000000000" | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e fe020a02"  | nc -U /tmp/treadmill.sock -N
echo "char-write-req 0x000e ff0a0204020602068400008c0000000000000000" | nc -U /tmp/treadmill.sock -N

Not the prettiest, but…it works?? Madness! However this surfaces something interesting: we are pumping these commands through with zero delay, and although it works, there is significant lag. For example, the above script completes but the changes only register about five seconds later. One explanation - the treadmill buffers packets and uses some internal logic to determine when to apply them. Seems reasonable - this probably helps ensure that random state changes can’t inadvertently damage the treadmill itself.

Simple enough - we can drop a sleep in between commands (which we can now identify based on our analysis). The result is a rough simulation of our completely non-scientific sample.

Using socat

Another fun way to do this is socat. There is a great post on the various ways to use socat, and borrowing from that we can do:

$ socat -u OPEN:/tmp/commands.txt UNIX-CONNECT:/tmp/treadmill.sock

That will open socat in unidirectional mode (-u) and write each line of the file to our socket. This also works, but runs into the same issue we hit above (commands are issued in rapid succession). Since we want to inject some higher-level control flow (sleeps, logging, etc.), we’ll use the simple approach above, but always fun to explore options.

Tweaking the inputs

To wrap this up, let’s put our analysis to a not-very-rigorous test. Rather than replay the original session’s commands, let’s try to set the speed/incline to values we did not observe. In increments of 1.0, let’s try:

  • Changing the speed from 5.0 to 8.0 (and back)
  • Changing the incline from 3.0 to 5.0 (and back)

If our theorems are correct, the command should be:

fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 25 03 00 ?? 0000000000

This is because:

  • 5.0 MPH == 8.05 KPH
  • 8.05 KPH == 805 in treadmill speak
  • hex(805) == 0x325
  • Treadmill is little-endian -> 2503

Why the ??’s? Recall that we didn’t figure out what those values meant, but they were always 0x10 higher than the low byte. However we never took a measurement where the value was two bytes, so unclear what the behavior should be. It certainly means something, but will it still work if we ignore it? Let’s see!

# Increase the speed to 5.0
echo char-write-req 0x000e fe020d02 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e ff0d020402090409020101250300000000000000 | nc -U /tmp/treadmill.sock -N

…and it works! Not knowing what that final byte means is a little annoying, but it looks like we don’t actually need it for our case. Repeating the same pattern and throwing a sleep 1 in between each one confirms that we’re on the right track. We can play this off like ‘Yes yes, naturally this works, it is simply trivial’, but between the two of us, this was more than a little satisfying.

Oh and if not knowing what the final byte means isn’t annoying enough, it still works if we remove everything in between the notification subscription and the first command except this:

echo char-write-req 0x000e fe022c04 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e 0012020402280428900701cec4b0aaa2a8949696 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e 0112aca8a2bad0dccefe14003a52786486a6fc18 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e ff08324aa0880200004400000000000000000000 | nc -U /tmp/treadmill.sock -N

For posterity, here is the final test script:

# Start a socket with and connect to gatttool:
#
#   nc -lkU /tmp/treadmill.sock | gatttool -I -t random -b MAC
#
# Then run this.
echo "**** NordicTrack T6.5S Test Program ****" 

echo -n "Connecting..." 
echo connect | nc -U /tmp/treadmill.sock -N
sleep 2
# Yeah yeah, this doesn't check whether the connection succeeded, lay off.
echo "done"

# Subscribe to notifications.
echo -n "Subscribing to notifications..." 
echo char-write-req 0x000C 0100 | nc -U /tmp/treadmill.sock -N
echo "done"

# Unclear what this is, but it is required for the rest to work.
echo -n "Writing magic incantation..." 
echo char-write-req 0x000e fe022c04 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e 0012020402280428900701cec4b0aaa2a8949696 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e 0112aca8a2bad0dccefe14003a52786486a6fc18 | nc -U /tmp/treadmill.sock -N
echo char-write-req 0x000e ff08324aa0880200004400000000000000000000 | nc -U /tmp/treadmill.sock -N
sleep 4
echo "done"

echo
echo "Starting test program" 
echo 

# End at 1.0 since it is annoying leaving it at 5.0.
echo -n "Speed 5.0 -> 8.0 -> 5.0 -> 1.0..."
for speed in "2503" "c003" "6704" "0705" "6704" "c003" "2503" "a0"; do
  echo char-write-req 0x000e fe020d02 | nc -U /tmp/treadmill.sock -N
  echo char-write-req 0x000e ff0d020402090409020101${speed}00000000000000 | nc -U /tmp/treadmill.sock -N
  sleep 2
done
echo "done"

echo -n "Incline 3.0 -> 5.0 -> 3.0..."
for incline in "2c01" "9001" "f401" "9001" "2c01"; do
  echo char-write-req 0x000e fe020d02 | nc -U /tmp/treadmill.sock -N
  echo char-write-req 0x000e ff0d020402090409020102${incline}00000000000000 | nc -U /tmp/treadmill.sock -N
  # Give it some time to settle.
  sleep 4
done
echo "done"

Next up, let’s jump back to analysis and figure out how to get/interpret the current state of the treadmill.