Decoding a garage door opener with an RTL-SDR
On a recommendation, I got a €10 SDR; it’s a digital TV receiver which happens to function as a radio that can receive a wide range of frequencies. It’s pretty cool.
After whiling away a few evenings listening to dockworkers on walkie-talkies loading up cargo ships, I wanted to get my hands dirty with something a little more practical. This is a record of my process, including false starts or dead-ends. Having not played with any signal processing or radios seriously before, there are probably some parts which are factually incorrect.
The only device I owned that would be liable to have a “simple” radio was my garage door opener. I reckoned it wouldn’t be too complex because you can pop open the cover to access a DIP switch.
So, first things first, we want to find the frequency the gate opener transmits on and listen. For visualizing/listening to the radio, I’ve been using CubicSDR; even though it has less functionality than -say- gqrx, it’s much prettier and click-dragging UI elements does what I expect.
While I’m sure I could find the frequency by opening up the box and examining the components, I had an inclination that the transmission would be around 433Mhz. It’s one of the few unlicensed bands I can remember. Wikipedia’s page for the LPD433 band explicitly cites keyless entry systems, which was also a good hint. I started listening around 400Mhz, and alternated increasing the frequency and pressing the button on the door opener. This process was pretty fast, so it would still be practical even if I had no idea what frequency to look near. At some point, I started seeing a signal corresponding to my button presses:
OK, so now I’ve got a signal in CubicSDR and I can hear it — at least, using FM demodulation to listen to the signal, it sounds sufficiently like data. Now I’d like to do something with this data. CubicSDR doesn’t have any recording functionality, so I made a fork and added some. This was probably misguided, but either way; I had a WAVE file. Open it up in Audacity to take a look at the data:
That definitely looks like bits. Eyeballing the length of these high/low values, I wrote down what I thought the signal consisted of:
36 digits; 12 dip switches. 3 bits per switch. Splitting that up:
010 010 011 010 010 010 011 010 011 011 010 010
And comparing to the configuration of the DIP switches, a 010 corresponded to an up switch, while a 011 matched the down switches. Feels pretty much done and decoded at this stage. Admittedly, having three bits per switch matches no line code that have been able to find.
Now that I can decode this by hand, I’d like to be able to automate it. Rather than writing something from scratch a-lá dump1090 or trying to tune rtl_fm and pipe it into something, I went with GNURadio.
GNURadio has about a zillion options and any one particular incorrect setting will render mess up the incoming data, so the first challenge was to tune into the door opener again and verify it was working. Any time I was concerned about the validity of the settings/data, I found it useful to throw in a “GUI Waterfall Sink” or a “GUI Frequency Sink” where I could [usually] visualize what was happening. To tune in, I ended up with this:
That’s the SDR source providing radio data; the waterfall sink to visualize the data and a pair of variables to allow me to tune the radio while the graph was running. Once I was happy that I had the radio parameters correct, I enable the file sink to write the data into a file.
This file was important. Now, I could use the file contents reliably without having to reach over and press the gate opener button every single time.
Right. Now for the decoding part. Make another GNURadio graph that reads the recorded data; this is the next challenge. I want to get a signal that I can listen to and audibly sounds the same as the WAVE recorded from CubicSDR. Again, this is a step to verify I haven’t made a mess of any of the other settings.
Playing the file back by itself is pretty noisy. So I add a low-pass filter; this guy will remove any signal outside a certain frequency range:
It’s going to keep signals that are around the center frequency, but my recorded data is in the upper frequency range of what I’ve recorded. The incoming radio data is a stream of complex numbers, so we can rotate a sample in the plane by multiplying another number. With a complex sine wave, we can increase (or decrease, in this case) the frequency which the incoming signal rotates. With a variable assigned to a sine source’s frequency, I multiply the wave with the incoming signal and, using a frequency sink eyeball it until it looks like it’s in the center.
And then, use the low-pass filter to get rid of all the noisy, out-of channel signals that we recorded; again, this is setup by making variables and eyeballing until it looks close enough.
So, now I’ve got what I think is clean data incoming, ready for processing. At this point, I wasn’t entirely sure that the bits were sent over FM; I tried using some of the other demodulator blocks in GNURadio (in particular, I gave the “Quadrature Demod” block a good shot) but a combination of
- No success
- The refresh rate of the GUI output sinks
- Already having something that looked correct
Contributed to giving up this approach. I powered on with FM.
Throwing an FM Demod block onto the output of the low-pass filter and sending that to my audio device got me the same sound I heard in CubicSDR. Next, write this out to a file for testing, as this is what I’m going to have to process. I also wrote out the magnitude of the original signal, to help determine if there’s an actual transmission (a trick I picked up from a similar project to decode a Temperature Sensor, but I suspect is not the correct way to do this.)
Once more, to verify the data; open it up in Audacity and check what it looks like. This time it’s a raw signal, rather than a WAVE, so import as such. It looks fine, so let’s start processing.
I normally write code that runs for less than 1ms, and throwing some debug visualizations in there is a good way to get an idea of what’s happening. I thought I could do the same for this audio data (after all, 44K samples with >1K pixels per-frame is well within 60FPS.) This was pretty far from usable.
So, I wrote a short script in Python, using matplotlib to visualize the data; this way, I could scroll through the data at my leisure and it allows for fast iteration to tune thresholds and other assorted, hard-coded constants without having to shutdown/recompile/restart GNURadio.
I have no doubt there is a more robust way to do this but the mechanism of the decoding is pretty straightforward. I’m simply reading the demodulated signal in parallel with the magnitude. While the magnitude is some arbitrarily high value; gather samples of the demodulated signal, counting the number above/below some threshold. Since both the “on” and “off” switch start with a “low” followed by a “high,” I can use that first sample to reconstruct the clock and detect when we see two high/low values in a row.
Once I was happy the algorithm was working [approximately] correctly, I fire up the GNURadio docs on building your own blocks. This block isn’t doing anything terribly complex, so it was a straightforward read-the-manual-and-port-the-code job. Finally; wire up the input pins to the same data that we were generating the Python files from and we’ve got a block that writes detected codes out to the console.
Easy. Now I finally know the code to open my garage door.
Having done this, I feel I’d apply a bunch of the same techniques to another project. Some things I’m unsure about, though.
Was FM demodulation the right thing to do at all? I feel I should have at least plot the IQ signal in 3d to get a feel for what it looked like, rather than looking at the FM demodulation signal straight away.
Should the decoding block be that sensitive to the tuning? There was a whole lot of fiddling necessary to get the frequency centered. When running dump1090, it JustWorks, even without having to account for a frequency offset in the USB stick. I suspect this block is not very robust in that regard.
The source code for the above isn’t necessarily very useful, but if you’re interested, I made a small package.