Check out the code here: https://github.com/mostthingsweb/temperbridge-esphome
This is the first in a (probably) three part series.
About a year ago, I decided to upgrade my old spring mattress to a TEMPUR-Contour Elite Breeze and I cannot say enough good things about it. I opted to also get an adjustable base for it, which has also been great. It comes with a remote like the one pictured here:
Another advertised feature of the base is control via Android and iPhone apps. Unfortunately, the Android app hasn’t been updated since 2014 (update 6/6/2023: at the time in 2019 this was true, but the app was updated in 2022). I could not get it to work with my Google Pixel, and based on the reviews, I’m not the only one. Clearly, this is DIY territory.
- Build a gateway that communicates with the base using its own RF protocol
Develop an Android app that can control the base through the gateway (next time)
Other projects that achieve something similar fall into two categories:
- Talking to the onboard WiFi module (e.g. https://github.com/docwho2/java-alexa-tempurpedic-skill)
- Integrating relays into an existing remote control to simulate button presses (e.g. http://www.quadomated.com/technology/automation/diy-tempurpedic-ergo-adjustable-bed-automation/, https://github.com/tomchapin/tempurpedic-remote-relay)
The benefits of reversing the actual RF protocol are (a) no dependence on integrated WiFi and (b) non-destructivity.
Researching the FCC filing
Since the remote is a wireless device, it is regulated by the FCC. We can lookup the FCC ID (UNQTPTAES) to learn some useful information about the device. Confidentially requests by the manufacturer prevent us from accessing the BOM, schematic, and block diagram :/. In this case the most helpful document is the Test Report.
Some interesting excerpts from the test report follow.
One page 7, we learn there is some kind of mapping between the channel number and the frequency. The remote supports lots of channels, which could be useful for people with more than one adjustable base.
Page 12 gives us a glimpse at the signals we will soon be hunting:
Page 13-14 detail that the remote transmits in pulses, e.g. on each key press. This implies that there is no persistent connection between the base and the remote, and that communication is probably unidirectional (remote => base). Makes sense because the base shouldn’t need to talk back to the remote.
Initial exploration with software defined radio (SDR)
In addition to being practical(-ish), I intended to use this project as an opportunity to play with SDRs for the first time. That being said, I will not try to attempt to teach any of the concepts here.
The test report signal plot is overlaid on the right. We can see that the two signals are very close, so we’re on the right track.
It turns out that the type of modulation in use here is Gaussian frequency shift keying. A big thanks to my friend Tim for identifying it as FSK.
Listening in on the SPI bus
I got relatively far using GNU Radio to demodulate the signal (something I won’t go into here, mainly due to my lack of ability to speak on the subject). Eventually I got bored of it, and sought a faster way to reverse engineer the signal.
For this, I broke out my trusty Saleae logic analyzer (mine is an older model; here’s the newer kind on Amazon). Cracking open the remote, one finds a Si4431 RF transceiver:
And also a Renesas microcontroller:
It is tempting to try and dump the uC’s ROM and reverse the firmware, but the FCC test report from earlier reveals that ROM protection is active. Maybe next time we’ll try anyway, since PIN bypasses like this exist: https://github.com/q3k/m16c-interface.
But for now, the plan is to simply listen in on the SPI bus connecting the uC and RF transceiver. This bus is used for configuring the RF chip and for transmitting the data. The Si443x’s datasheet reveals an incredible number of configurable settings: https://www.silabs.com/documents/public/data-sheets/Si4430-31-32.pdf.
We will capture the traffic on the bus during initialization (i.e. right after installing the batteries) to learn how the uC configures the transceiver. This will reveal the modulation scheme (spoiler: GFSK) and any other parameters we need to impersonate a remote control. To make this easier, I wrote a small Python script that interprets the SPI traffic (exported from Saleae logic as CSV) and displays it as register reads and writes:
This traffic reveals the following details of the modulation scheme. Note that in this case, my remote was set to channel 9345, which is how it came delivered.
- Nominal carrier frequency: 434.5856250 MHz (again, this is dependent on the channel)
- Data rate: 12.8 kbps
- Frequency deviation: 25 kHz (corresponds to 50 kHz bandwidth)
Nominal carrier frequency is another term for the center frequency. Therefore, channel 9345 occupies ~[434.585 MHz – 25 kHz, 434.585 MHz + 25 kHz].
Figuring out the channel mapping
So how does the remote get 434.5856250 MHz from “channel 9345”? This is a question I wrestled with for about 2 days. There is no easy linear relationship. I collected a few dozen initialization sequences, each time with a different (random) channel number and looked for a relationship.
Without further ado, the final answer is the following function:
It’s not nearly as bad as it looks. In fact, the only part I had to figure out was the piecewise portion which corresponds to the fc (frequency center) register in the Si443x. So, when the user changes the RF channel, all the uC has to do to tune the transceiver is change the fc register.
The full formula I have presented above was derived from this equation in the datasheet:
Aside: Si443x frequency hopping
The Si443x has a neat feature that lets you do channel-tuning from a single channel select register. You first set the nominal carrier frequency, then the channel step size (increments of >= 10 kHz). It’s generally used for timing critical applications like frequency hopping. So why didn’t the Tempurpedic remote use it? Since the system supports 9999 channels, the 10 kHz channel step is way too big. The remote’s channel scheme uses ~156.2 Hz channel step.
Si443x packet structure
Recall that we’re dealing with burst communications. This means that the receiver will always need to be ready and listening for data. How can a device like this remote control do so in a power-efficient manner? Answer: Combination of preamble (to “wake up” the receiver) and a sync word.
The preamble is designed to be both “easy” to detect and something that is unlikely to be received randomly. A common choice is an alternating series of 1 and 0. The Tempur-Pedic remote uses this sequence, 40 digits long.
The preamble detector may wake up the transceiver in the middle of the preamble. How can it know where the data actually begins? This is what the sync word is for. After detecting the preamble, the receiver searches for sync word for a (configurable) period of time.
The Si443x can operate in fixed or variable length mode. In our case, the remote uses variable length mode, even though all of the data I’ve seen come from it is the same length. So, we’ll need to handle the packet length header.
Data and CRC
The CRC is a sanity check to detect transceiver errors. It’s handled transparently by the Si443x.
Into the data
It’s finally time to investigate what data is actually sent by the remote. Here’s an excerpt of the table I used to decode the protocol. I tuned my remote to various channels and pressed each button.
Immediately, we can see that, independent of channel and command, each transmissions begins with 0x96:
The next three bytes are independent of channel, dependent on command.
The next two bytes are the channel number. E.g. 0x03F2 == 101010
But what about the last part? There is no obvious correlation between neither the command nor channel.
Reverse engineering CRCs
Turns out, the last byte in each transmission is another CRC. But which one? Well, none of the common ones. Thankfully, there are tools for reversing CRCs. I chose this one: http://reveng.sourceforge.net/
All you need to do is feed it a map of inputs (data) => outputs (CRC) and it will brute force the CRC parameters. The output I got is pictured below (the actual command got truncated):
Parameters: width=8, poly=0x8D, init=0xFF, refin=false, refout=false, xorout=0x00, check=0xFD, residue=0x00, name=(none)
The “name=(none)” part means that this set of parameters doesn’t correspond to any well-known CRC.
Recall that the Si443x already has its own CRC. Why does the remote use another one? My best guess is that the engineers chose to add this application-level CRC to guard against data corruption between uC and transceiver. For example, suppose some noise on the SPI bus caused the transceiver to “see” different data that what the uC intended to transmit. The transceiver has no way of knowing this, and so it will dutifully transmit it. The receiver in the base may or may not recognize the corrupted command. An application-level CRC ensures transmission errors on the SPI bus can be detected.
Most of the buttons on the remote don’t seem to be debounced. For example, when pressing a command like STOP or FLAT, the remote will actually transmit it between one to three times (in my experience). This is a good thing, since it increases the probability that the receiver actually receives the command. Those types of commands are idempotent, so even if the base does receive multiple of the same, it doesn’t matter.
Buttons that are meant to raise/lower the legs and head are intended to be held down, which causes the respective command to be repeatably transmitted until the button is released.
The massage buttons are an interesting case. These six buttons increase/decrease the massage intensity of the legs, lumbar, and head region. Initially I had expected that they would be implemented using one command per button, just as the leg/head raise/lower buttons. But in fact, it is more complicated.
There are eleven massage levels, between 0 and 10, with 0 being off. The remote control keeps track of the current massage level for each region. When the user hits the “+” or “-” button, the remote transmits a massage command that encodes the region and the new level. In other words, it transmits the absolute massage level. This means it actually 33 discrete commands to implement the remote’s massage buttons (11 levels * 3 regions). Contrast this to the leg/head raise/lower buttons, which merely transmit relative positioning commands (either “+1” or “-1”).
So why does the massage feature have this complication? Here’s my theory. From the previous section “Command bursting”, we understand that the remote transmits commands multiple times to improve the probability that they are received correctly. This is fine for idempotent commands like STOP, FLAT, etc. It’s also fine for buttons that raise/lower the heads/legs since their positioning is so granular that the user probably won’t notice the difference between a button press that raises their legs 1 step or 3 steps. But since the massage only has 11 levels, the user would definitely notice if one press of “Lumbar message increase” added +1 to the level whereas the next added +3.
A mildly interesting feature of the system is the ability to automatically choose a channel. To do this, you press and hold the FLAT and STOP buttons for ten seconds, then press the STOP button. The remote’s LCD will blink while displaying the new RF channel. While it is blinking, you unplug and then replug the base. If done properly, a relay in the base will click to confirm the new channel has been learned.
While the remote is blinking the new channel, what it’s actually doing internally is transmitting the channel number on a special broadcast channel 5568 (fc = 25088, frequency = 433.9200000 MHz). Presumably, the base listens on this channel immediately after being plugged in.
Side note: A limitation of the system is that the RF channel is chosen purely randomly, and although there are 9999 possible channels, collisions are possible.
Prototyping a receiver
With the protocol understood, I sought to build a receiver that could decode commands from my physical Tempur-Pedic remote. I chose the following Si443x transceiver breakout for prototyping: https://www.tindie.com/products/modtronicsaustralia/rfm22b-wireless-breakout-board-800m-range/ ($22.95). Eventually I am actually going to switch to the Si446x (example breakout), since the Si443x is end-of-life.
The microcontroller I am using is the ESP32 ($21.95): https://www.sparkfun.com/products/13907. It is the perfect choice because it supports Bluetooth Low Energy and WiFi,
one/both of which I’m planning to use for the next part of this project wherein I build my own Android app for controlling my base.
A product based on this work is now available to buy! https://www.temperbridge.com/
Or clone the code on GitHub and build your own: https://github.com/mostthingsweb/temperbridge-esphome