# A Little OffCode, 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:

 123456 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.

 123456789101112 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.

 12345 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:

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.

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:

 1234567891011121314151617181920212223242526272829303132333435363738394041424344 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.

 123 record show record define record commit

The output of record show is shown below:

 1234567 #  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:

 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556 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:

 1234567891011121314151617181920212223 bin show bin set bin reset log text log data a wav visual word address [value] long address [value] remind N at hhmm remind N cancel remind list remind N gettext remind N settext noop system reboot

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

 123456789101112131415161718192021 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

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.

Just a silly error :)

• I corrected the link. Grover was right, one too many slashes in the url.

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!

• Got it fixed now, forgot the syntax highlighting plugin takes care of that. The editor I wrote this in doesn’t handle any of that, so i used the html encodings.

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.net.URLClassLoader.access$000(URLClassLoader.java:58) at java.net.URLClassLoader$1.run(URLClassLoader.java:197)