Last week as I was making my rounds at the supermarket, I came across this digital bathroom scale on sale. With some membership card, the discount was almost 50% and at S$16, I thought that was a pretty good deal. It is “wireless” in that it has a separate display unit that could be detached from the scale itself. This bathroom scale had “HACK ME” written all over it.
It turns out that this bathroom scale is the EB9121 made by a Chinese (OEM?) company called Zhongshan Camry Electronic Co. Ltd (or simply Camry). The box specifically mentions that it uses infrared for transmission, and given that I had some experience looking at IR signals, I thought it would be rather straightforward.
I hooked up an 38kHz IR receiver to the logic analyzer and placed it near the scale while I was standing on it. It looks like the scale was using pulse distance coding to encode the data, and each burst was separated by a ~75ms space. The data is represented by 500µs, followed by either 500µs or 1000µs, making the total duration of each bit either 1ms or 1.5ms.
I would probably die if I had to manually mark the 1’s and 0’s on the waveform for analysis. Since I already had some code for a toy Saleae analyzer plugin lying around, I hacked it up to mark 1’s and 0’s on the waveform depending on the pulse distance. Saleae analyzer plugins can also put bubbles above a group of individual bits in order to present the decoded data for easy interpretation. Having an analyzer plugin definitely helps with reverse-engineering the protocol:
At this point, I don’t know if the (500, 500) pair represents a 1 or 0, so we’ll just pick one for now. I also don’t know if the bits are reconstructed from left-to-right or right-to-left. I made all of these into configurable options so that I could easily try out various combinations. The data can also be exported into a text file for further analysis.
Decoding the Data
To decode the actual data, I needed to generate some known data first. I captured the waveform from 0.0kg and gradually applied my weight onto the scale to cause the values to advance in an ascending order.
I played with the decoding configuration and exported the values until the data looked like it was increasing and ended up with this:
time[s], values : 3.141592, AB 00 00 00 AA : 6.042467, AB 80 00 4B 76 6.170977, AB 80 00 57 82 6.812532, AB 80 00 6B 96 7.069715, AB 80 00 91 BC 7.198203, AB 80 00 A5 D0 7.326531, AB 80 00 B5 E0 7.455037, AB 80 00 C6 F2 7.583412, AB 80 00 DA 06 7.711770, AB 80 00 EC 18 7.840168, AB 80 01 03 30 7.968462, AB 80 01 24 50 8.096935, AB 80 01 34 60 8.225057, AB 80 01 34 60 8.353910, AB 80 01 5E 8A 8.482273, AB 80 01 89 B6 : :
The bytes are actually encoded from left-to-right (first bit is the MSB), and the
(500, 500) pair represents a
(500, 1000) represents a
0. The data encoding at this point looks like it’s in ascending order, but I still had no idea what they mean.
Next, I tried to get the bathroom scale to display different weights, so as to get a mapping of encoded values and the displayed weight. It would be best to record consecutive weight values (e.g. 10.0kg, 10.1kg, etc) but realistically it was difficult to control the amount of pressure I exert on the scale, so I just recorded a bunch of data as close to each other as possible.
: : AB 84 00 41 70 - 6.5kg AB 84 00 43 72 - 6.7kg : : AB 84 00 BE EE - 19.0kg AB 84 00 C0 F0 - 19.2kg AB 84 00 C7 F6 - 19.9kg AB 80 00 CC F8 - 20.3kg AB 84 00 D2 02 - 21.0kg AB 80 00 DC 08 - 22.0kg AB 84 00 DD 0E - 22.1kg : :
From these captures, notice that the representation for 22.0kg and 22.1kg differ by 1 in the second last byte; 21.0kg and 22.0kg differ by 10 (
0xA) in the same byte position. If you convert
0xDD to decimal, you will get
221 and that is the displayed weight multiplied by 10 to get rid of the decimal point.
Now that we have figured out how the weight is represented, let’s see what the other bytes are.
The first byte is a constant identifier (
0xAB), and the weight is represented by the 3rd and 4th bytes. Thus we are left with the 2nd and last bytes, which are also changing.
The 2nd byte seems to indicate the measurement status. When sufficient weight is on the scale, it changes from
0x80 and stays there until the measurement is stable. After the weight is no longer varying, this byte changes to
0x84 for a short period, followed by
0x8C causes the display unit to blink, so I’m not sure why there are 2 values to indicate a stable weight.
The last byte is the checksum byte. Notice that when the second byte changes, the last byte does too. Checksum bytes are typically computed by XOR-ing all or some bytes, but in this case, it’s an addition capped to a single byte using
mod. The following Python code snippet describes the checksum algorithm that I have reconstructed:
# input bytes bytes = [0xAA, 0x00, 0x00, 0x00] checksum = 0 for n in bytes: checksum += n checksum %= 0xFF # set last bit to zero checksum &= ~1
Note that the last byte only has 7 bits (total data length of each burst is 39 bits). The analysis plugin fills that missing last bit with zero, so the checksum algorithm also sets it to zero to make sure the values match up.
Being able to correctly compute the checksum ensures that the received IR data was not corrupted (to some extent).
Repurposing the Display?
With the protocol reverse-engineered, we could send the same IR signals to activate and display whatever numbers we want, although I haven’t really thought of a good use for this yet.
While the specifications say it could go up to 150kg max, the display can actually indicate up to 153kg (
0x05FA) before it displays
There’s also a button hidden inside the battery compartment that allows you to toggle the unit of measurement to pounds (lb). The display unit will then convert the transmitted values in kilograms to pounds internally and show that value.
Recording Measurements to the Cloud
The whole point of reverse-engineering the bathroom scale protocol is to automatically record the readings for easy plotting and exporting, inspired by the Adafruit tutorial for logging humidity & temperature values to Google Docs using the gspread Python library.
The only Internet-connected device I had was the Raspberry Pi. Recording bit transitions on the Pi’s GPIO pins is challenging (for instance, polling the pins would be detrimental to battery life) but fortunately I found the
lirc_rpi kernel module, which solved all of my problems at once. This kernel module will monitor an IR receiver hooked up directly to a GPIO pin using interrupts. It is also shipped with Arch Linux, so I don’t have to compile it myself.
You have to load the kernel module manually, like so:
modprobe lirc_rpi gpio_in_pin=4
This selects GPIO 4 as the pin connected to the IR receiver. Note that GPIO 4 is NOT pin 4. Refer to the wiki at RPi Low-level peripherals to see the pin mapping. I’m using GPIO 4 (pin 7) because it’s closest to pin 1 (3V3 supply) and pin 6 (GND).
lirc_rpi module creates the
/dev/lirc0 device, which LIRC uses to send and receive IR signals. To avoid installing LIRC (yes, I’m weird like that), I wrote a Python script that talks to the
/dev/lirc0 device directly. The protocol is rather straightforward if you see technical document for LIRC under the “Formats” section.
Now all I need to do is to place the Raspberry Pi near the bathroom scale (I’ve tested it works up to about 60cm) and stand on it. The Python script will only be active when infrared signals are received and data starts flowing out of
/dev/lirc0. The script receives all IR signals and parses them until it finds a transmission in the format we just decoded. To verify that the data is valid, it ensures that the transmission starts with
0xAB and that the checksum is correct.
The script waits until the status byte changes to
0x8C for a few consecutive measurements before it submits that weight to the Google Docs spreadsheet along with the current timestamp. No more standing on the bathroom scale, waiting for the result and copying it to some Excel spreadsheet for graphing — perfect for the lazy me!
Wiring It Up
I have since added a status LED to indicate the status of the measurement, as well as a systemd service file that starts the script automatically on boot. This wraps up the project into something that is usable by itself without any display. When infrared signals are being received from the bathroom scale, the script blinks the status LED. After the weight stabilizes, the status LED turns on solid for a few seconds until the data has been sent to Google Docs, then turns off.
This addon consists of a 26-way IDC connector that I got from Mouser, a ribbon cable salvaged from an old IDE cable, and the components soldered on directly. I’m using the Vishay TSOP4138 as the IR receiver and it works pretty well, unlike the Waitrony part that I took out from the junk box. I strapped a 0.1µF capacitor to the back of it, just in case. Notice that while I’m using only 5 connections, it is more robust to punch down all 12 wires into the IDC connector:
- Pin 1: 3V3
- Pin 6: GND
- Pin 7: GPIO 4 as IR input
- Pin 9: GND (for LED)
- Pin 12: GPIO 18 as LED output
Using an IDC connector is the best way to fit everything into the awesome Adafruit Pi case. The transparent top cover lets infrared through and the status LED is also visible, so there’s no need to drill holes or anything.
Initially I had some problems getting the Waitrony IR receiver to work with the Raspberry Pi. The circuit was made on a small breadboard and was causing spurious transitions. According to the datasheet, I had to insert a small value resistor (less than 100 ohms) in series with Vcc and a 47µF electrolytic capacitor across Vcc and GND “in case of noisy power supply”. After I did that, the IR receiver was functioning perfectly. Note that the Vishay IR receiver I used later was more tolerant of noise in the power supply.
To verify that the IR receiver is being correctly detected, you can pass
debug=1 to the
lirc_rpi module. It outputs debug messages to syslog like these:
kernel: lirc_rpi: auto-detected active low receiver on GPIO pin 4 kernel: lirc_rpi.0: lirc_dev: driver lirc_rpi registered at minor=0 kernel: lirc_rpi: driver registered!
You can also use a utility bundled with LIRC called
mode2 to check that the IR signals are correctly received, and that nothing is being received when idle.
The script and setup instructions are available for download on the cloud-bathroom-scale Bitbucket repo.