A Little Off Code, Computers, Photography and Guns

27Nov/1218

Reverse Engineering the BodyBugg

I recently bought a BodyBuggSP after a lot of research into fitness measurement//recording devices which I wanted to use for recording some information about my sleeping habits.

The BodyBugg V3 and BodyBuggSP are both essentially rebranded flavors of devices designed and manufactured by BodyMedia. As far as I can tell all of the devices have more or less the same sensors although some come with bluetooth or RF communication built in for use with a smart phone or display.

The biggest drawback for me is that the way the devices are designed and the software to retrieve data from them was written you are required to pay for the online service used to store and interpret the data. You can view the data using bluetooth if you got a BodyBuggSP but this will only view. You must sync the device with the web service in order to erase the device's memory, which means if you no longer use the web service, your device will simply run out of memory and stop logging.

This sort of commercial douche-baggery really doesn't sit well with me and I immediately started looking around for any projects for reverse engineering the protocol used to communicate with the device and download//clear memory. There are as far as I can tell 3 people who have started projects like this.

One of these projects is a python script which took the approach of reverse engineering the protocol for communicating with the device. Since the device communicates with a computer via serial over USB you can pretty easily sniff the traffic on a serial port. This turned out to be a very messy endeavor as there are a lot of unknown fields and discovering the structure of requests and responses isn't very straight forward.

Another involved sniffing the HTTP traffic between the java applet used for downloading and clearing device memory. Unfortunately this method is only capable of recording data, not clearing device memory after the service period has elapsed.

The last project was a Java program which has basically no documentation or source available since it was distributed as a JAR file and lived on google docs. Once BodyMedia caught wind that class files from their Java applet were being distributed verbatim in this program DMCA's were filed with google to remove the files.

First thing I did was attempt to rewrite some of the python script in a more general fashion but quickly gave up since the protocol seems to have slight differences depending on the device used.

I eventually decided to look at the Java data upload applet used with the web service. Once the JAR's for the applet were downloaded I got to work disassembling the classes within. I found pretty soon that there was a class used strictly for serial communication and provides methods for writing commands to the device and retrieving the response which is exactly what I wanted. This class essentially wraps a dll in Java.

The bare minimum classes needed from the applet jars are shown below:

1
2
3
4
5
6
import com.bodymedia.device.usb.Usb;
import com.bodymedia.device.serial.SerialPort3;

import com.bodymedia.common.applets.logger.Logger;
import com.bodymedia.common.applets.CommandException;
import com.bodymedia.common.applets.device.util.LibraryException;

For ease of use the Usb class can discover serial ports any bodymedia devices are connected to. The code below simply finds ports the devices exist on and fails if there are no devices or more than one.

1
2
3
4
5
6
7
8
9
10
11
12
Usb usb = new Usb("bmusbapex5", Usb.ANY);
String[] ports = usb.getArmbandPorts();

if(ports.length < 1) {
    System.out.println("No BodyBuggs detected.");
    System.exit(1);
} else if(ports.length > 1) {
    System.out.println("Multiple BodyBuggs detected, re-run with only one connected.");
    System.exit(1);
}

String serialPort = ports[0];

Once the port the device is connected on is discovered it's fairly simple to connect to it and communicate with it.

1
2
3
4
5
SerialPort3 ser = new SerialPort3("bmcommapex5", serialPort);
ser.setAddr(0x0000000E, 0xFFFFFFFF);
ser.open();
...
ser.close();

The call to setAddr was the last key to getting communication to work. I'm not sure if the device address is constant on all devices or not. The values used were discovered by sniffing serial communication.

Dissassembling the class ArmbandCommand revealed several string constants which represent commands that can be issued to the device.

Executing a command on the device is as simple as:

1
2
    ser.writeCommand("command here");
    System.out.println(ser.readResponse());

Some testing revealed that the device responds much like a unix program by giving usage if the command is invalid (or a multi-part command). Using this I was able to compile as far as I can tell a complete list of commands the device understands. Two important commands are get and set.

get [value [value value]] set value [value [value]]
age modrunning age modthreshold
banner modtarget birth modtoday
battery modthreshold bluetoothaddr modyesterday
birth modtoday bluetoothname offbodydelay
bluetoothaddr modyesterday boardnum operationmode
bluetoothname offbodydelay boardseries orientation
boardnum operationmode bootdiagnostic password
boardseries orientation bt_link_data personalized
bootdiagnostic password bt_local_port productcode
bt_link_data personalized bt_mysterybyte radiotimeout
bt_local_port productcode bt_remote_address rrcollect
bt_mysterybyte radiotimeout deepsleeptimeout sampletimeout
bt_remote_address rrcollect defaultconfig secondactivity
capabilities sampletimeout dialstring serialnum
charge secondactivity disable sex
checksum serialnum display1224 smoker
deepsleeptimeout sex eedebug spewmode
defaultconfig smoker eegoalmsg stepgoalmsg
dialstring spewmode eerunning stepsrunning
disable stepgoalmsg eetarget stepstoday
display1224 stepsrunning eetoday stepsyesterday
eedebug stepstoday eeyesterday steptarget
eegoalmsg stepsyesterday epoch subject
eerunning steptarget gsrthresh timezonechange
eetarget subject handed usbspeed
eetoday timezonechange height username
eeyesterday usbspeed hostname viggoalmsg
epoch username hrtarget vigrunning
gsrthresh value iapbundleseedid vigtarget
handed version iapprotocol vigthreshold
height viggoalmsg ipaddress vigtoday
hostname vigrunning lastdataupdate vigyesterday
hrtarget vigtarget led volume
iapbundleseedid vigthreshold modgoalmsg weight
iapprotocol vigtoday modrunning welcomemsg
ipaddress vigyesterday modtarget
lastdataupdate volume
led weight
memory welcomemsg
modgoalmsg

Commands available to deal with recorded data are as follows:

1
file init | clear | size | msgerase

I'm not sure what the exact difference between init and clear is, in my source I've been using file init which appears to only clear data memory, not configuration.

The possible types of data recorded by the device are shown using channel show:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Chan#   Name
-----   ----
    0   RAWACCFW
    1   RAWTSKIN
    2   RAWGSR
    3   RAWACCTR
    4   RAWACCLO
    5   RAWVBAT
    6   RAWTCOV
    7   RAWON
    8   EE
    9   MOVTSKIN
   10   MOVGSR
   11   MOVACCTR
   12   MOVACCLO
   13   MOVVBAT
   14   MOVTCOV
   15   MOVON
   16   MADACCTR
   17   MADACCLO
   18   F0CROSS
   19   HRATE
   20   PEDO3
   21   PLATEAU
   22   MADECG
   23   TRPEAKS
   24   MOVTHETA
   25   FWPEAKS
   26   FCOUNT
   27   MOVACCFW
   28   MADACCFW
   29   TCOUNT
   30   LCOUNT
   31   PEDO3TOE
   32   TIMESTMP
   33   HEARTBT
   34   T0CROSS
   35   L0CROSS
   36   RAWECG
   37   LOGSWEEP
   38   MADTHETA
   39   LOPEAKS
   40   COMPGSR
   41   RAWCGSR

One channel listed here which is conditionally recorded is TIMESTMP. This channel records a timestamp when the button on the device is pressed while being worn. This is very useful for delimiting important events during use.

The information recorded per unit time can be modified using the record command.

1
2
3
record show
record define <index> <type> <name> <divisor> <channels[1..8]> <bytes>
record commit

The output of record show is shown below:

1
2
3
4
5
6
7
#  Type   Name   Div               Channels         Bytes
-  ----  --------    ---  -------------------------------  -----
0   16   V6RES1 1920    9  11  12  27  14  16  17  40   13
1   17   V6RES2 1920   20  21  23  24  38  29  37 254   12
2   18   V6RES3 1920   30  34  35  31  39  13  28 254   12
3   19   V6RES4 1920   26  18  25  10   8 254 254 254    9
4   20   UNUSED    0  254 254 254 254 254 254 254 254    0

Theoretically every channel available may be recorded. However to define a new record the bytes parameter must be calculated for each record index. The bytes parameter is calculated as follows; where n is the number of non-empty channels (maximum of 8 channels) in the record:

$$bytes = \left\lceil\frac{n\cdot 12}{8}\right\rceil + 1$$

The divisor is used to modify how often data is recorded for the particular record. The default value of 1920 produces records once per minute, halving this value produces records once every 30 seconds. Any given interval may be calculated using divisor of 32 = 1 second.

The transient command appears to be used for getting simple overview information about recent data for use with the mobile phone application. Two parameters are available: get and reset.

The last important command I've found is retrieve PDP, which will return information about each channel's recorded data including a starting timestamp. Example output is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000080c0000000300200780EE  
fff0bf232
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000090c00000005002003c0MOVTSKIN
fff8a58b58c28cc
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000000a0c0000000300200780MOVGSR  
fff56857b
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000000b0c00000005002003c0MOVACCTR
8ad8a986d8ad885
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000000c0c00000005002003c0MOVACCLO
9139088f38ef913
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000000d0c0000000300200780MOVVBAT
bf4bedbee
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000000e0c00000005002003c0MOVTCOV
fff7967a87b87c6
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000100c00000005002003c0MADACCTR
00003903d0420ec
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000110c00000005002003c0MADACCLO
00002b02f0290d5
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000120c0000000300200780F0CROSS
00001a06f
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000140c0000000300200780PEDO3  
000000000
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000150c0000000300200780PLATEAU
000000000
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000170c0000000300200780TRPEAKS
0000d3130
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000180c0000000300200780MOVTHETA
8007f8823
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000190c0000000300200780FWPEAKS
00009e0e2
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000001a0c0000000300200780FCOUNT  
00004814a
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000001b0c00000005002003c0MOVACCFW
6cc6c86a56b7723
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000001c0c0000000300200780MADACCFW
00002a08f
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000001d0c0000000300200780TCOUNT  
000083193
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000001e0c0000000300200780LCOUNT  
000070148
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf000000001f0c0000000300200780PEDO3TOE
000000000
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000220c0000000300200780T0CROSS
00003f092
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000230c0000000300200780L0CROSS
000026091
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000250c0000000300200780LOGSWEEP
0004e76c2
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000260c0000000300200780MADTHETA
00136e3a3
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000270c0000000300200780LOPEAKS
0000a0107
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf00000000280c00000005002003c0COMPGSR
fff00a01301b026
SESSION-BEGIN0502Firefly2_00000000e00e3ec50b48bbf0000000068100000000100000002DIAGNSTC
1354009655 5 1

Each SESSION-BEGIN statement contains a hexadecimal string which contains at least the timestamp recording was started at, this is shown below delimited by pipes. I'm not sure what other information is recorded in the hexadecimal string:

1
SESSION-BEGIN0502Firefly2_00000000e00e3ec|50b48bbf|00000000080c0000000300200780EE

Each line following the SESSION-BEGIN statement contains a hexadecimal string which represents each data point recorded for the channel. Data points are represented as unaligned 12-bit integers.

There are several other commands which I've not tested yet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bin show
bin set <index> <value>
bin reset

log text <listen_timeout> <text>
log data <listen_timeout> <mfg> <units> <data ...>

a <audible-number> <count>
wav <audible-number> <count>
visual <visual-number> <count>

word address [value]
long address [value]

remind N at hhmm
remind N cancel
remind list
remind N gettext
remind N settext <text>

noop
system
reboot

The remaining logic for the tool I've written for downloading data and clearing memory is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ser.writeCommand("get lastdataupdate");

Pattern lastUpdateRegex = Pattern.compile("Last Data Update: ([0-9]+)");
Matcher matcher = lastUpdateRegex.matcher(ser.readResponse().toString());

matcher.find();
String lastUpdate = matcher.group(1);

String logPath = String.format("%s.log", lastUpdate);
System.out.printf("Writing data to: %s\n", logPath);
FileWriter out = new FileWriter(logPath);

ser.writeCommand("retrieve PDP");
out.write(ser.readResponse().toString());

out.close();

System.out.println("Clearing device memory and updating timestamps.");
ser.writeCommand("file init");
ser.writeCommand(String.format("set lastdataupdate %d", System.currentTimeMillis() / 1000L));
ser.writeCommand(String.format("set epoch %d", System.currentTimeMillis() / 1000L));

This will get the last timestamp data was cleared at to name the output file. Output of retrieve PDP is then written to this file. Once this is complete file init is executed to clear recorded data and finally the device's time and last data clear times are updated.

To avoid the same fate as FreeTheBugg, I am not packaging any of the jars this program needs to run, instead you'll need to download and place them in the directory bodybuggbypass_lib which must be in the same directory as the executable jar bodybuggbypass.jar:

Full source presented here as well as sample data and a few other tools can be found at: https://github.com/bemasher/BodyBuggBypass

Comments (18) Trackbacks (7)
  1. Interesting. Not sure I can make it work, but I love the idea that you are opening this device.. I need a device such as this, if this “BodyBug” is the best one or most cost effective one I’ll figure out how to do this. be well.

  2. The link above:
    common-applets-1.10.0-SNAPSHOT.jar, is not downloading a file, just taking me to the site login. I do have a username, psw put still no file.

  3. So as I understand it, what you’ve managed to do is to pull raw sensor data from the device, leaving you with a fairly useful data logger. Any plans to try and interpret that data into calories burned, or anything else? (IE duplicate their services functionality)

    • Not currently. I’ve written a parser for the data and discovered a few of the divisors and offsets to convert recorded data to meaningful values (like temperature to degrees celcius or fahrenheit) but beyond that I have not written any classifiers or algorithms for determining activity from the data.

      I’m primarily using this to record during sleep, so my classifiers will have a significantly different set of criteria than what their service offers.

  4. I have an old BodyBug with a bad batterie and I’m interested in using its sensors. Is there a way to read the live sensor data?

  5. Just a note, but presumably the gt and lt signs in your code, the ones with the ampersand and semicolon, need changing to their actual symbols. Of course I can’t just TYPE the symbols I mean, since I may well end up with the problem I’m telling you about. Stupid nested encoding!

    Here, I mean your quoted code says “<” where it should be the “<" symbol. See if that works!

  6. It didn’t work! Hah!

  7. hah it’s cool you were able to reverse engineer the sensor. i guess the website is another revenue stream for the company, be cool if you could open source a similar UI for people to run their own server.

  8. Did you figure out which channel represents skin temperature and ambient temperature? I’m also interested in the heat transfer sensor data that should be somewhere in here.

    Thanks

    • Either RAWTSKIN or MOVTSKIN appear to be the skin temperature, though from my logs it doesn’t appear as though MOVTSKIN is any different from RAWTSKIN. As far as ambient temperature goes I don’t believe it records this. For my purposes I’ve been using a maxim digital temperature sensor and a custom 1-wire to RS232 board I made (in the same room while sleeping and such).

  9. When I try to run it (using the 1.11 version of the jars, and “java -jar bodybuggbypass.jar”), I get the following error:

    “Exception in thread “main” java.lang.NoClassDefFoundError: com/bodymedia/common/
    applets/CommandException”
    “Caused by: java.lang.ClassNotFoundException: com.bodymedia.common.applets.CommandException”

    Is this due to the new jars? Or just me doing something wrong?

    • I am receiving the same error.

    • I’ve tried running it with the new version of the jars and I get the same error. I haven’t had a chance to debug things yet though. Might be that they’ve caught wind of this project and encrypted the jars so I can’t call the methods I’m using anymore.

  10. i got the error
    Could not find the main class: com.bypass.BodyBuggBypass

  11. This is the error I get.

    Exception in thread “main” java.lang.UnsupportedClassVersionError: com/bypass/BodyBuggBypass : Unsupported major.minor version 51.0
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:283)
    at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:247)


Leave a comment