Some time ago, I decided to get into the world of SDR. I bought the RTL-SDR dongle, installed gnuradio, played around with it a little and then ran off to work on other things. This week, I decided to discover the possibilities of SDR further.

We have a remote temperature sensor at the front door which communicates with the master station over radio, so I decided that reverse engineering the protocol used would be fairly easy task given that I do not have almost any prior SDR experience.

Detection

To see, whether my SDR will be even able to receive the data, the frequency at which the station transmits needs to be acquired. For my station, this was easy — it is printed on a label on the front of the device and it is 433MHz. This works out nicely, as the SDR is able to receive frequencies between 24MHz – 1766MHz.

Now, let’s start up gqrx to isolate the exact value. Tuning the receiver to 433 MHz revealed, that in my area, there is one wide transmission at 433.3 MHz (it seems to persist over the cource of a few week, but I have no idea what it is) and a bunch of periodic bursts all over the place. To see, where the station transmits, I disconnected the antenna and brought the dongle closer to the station. It turns out, that the station transmits at 433.6 MHz every ~ 35 seconds. The status LEDs blinking correlates with the data being transmitted.

Analysis

Given the simplicity of the station, I assumed that it just uses on-off-keying with some simple line code to transmit the data. I therefore setup AM demodulation in gqrx and recorded few of the burst. After opening the wave file in Audacity, I got this:

Great! This definitely looks like some sort of data. How it is encoded though? It does not really look like any line code on the wiki. The time the signal is high is constant and the only thing that changes is the distance between the pulses. Perhaps the data is encoded in it (this is apparently called Differential Pulse Position Modulation)? So I wrote two python scripts, first to separate the packets to individual files and second to measure the distance between pulses.

$ python demod.py packet-0.wav
471
232
230
230
231
107
231
230
230
107
...

It seems that there are consistently three values of the delay — 110, 230 and 470 samples (@48 kHz this corresponds to ~ 2.3 ms, 4.8 ms and 9.8 ms respectively). I started with the (later proven correct) assumption that the shortest delay is binary 0, the middle is 1, the longest delimits packets and that the data is transmitted MSB-first.

[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]
[0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0]

Well, this looks somewhat useful! It works out to be around 8-9 full packets (and a few extra bits) per burst. By decoding and analyzing all the captured packets, I isolated the data fields in the packet:

Checksum

The checksum is the last four bits of the sum of all nibbles in the packet (with the checksum field set to 0b1111).

def calc_cksum(p):
    base = 0b1111
    for x in range(4, len(p), 4):
        base += to_int(p[x:x + 4])
    return base & 0b1111

Temperature

Temperature is twelve bit long signed integer which represents the measured temperature in tenths of degrees Celsius.

Channel

The channel bits can be 01, 10 or 11 depending on the channel set by a switch on the back of the device.

Force flag

The force flag bit is set when the packet transmission was forced by pressing a button on the back of the station.

Realtime

Now that I know how to decode the packets, it would be nice to have some way of getting the data on the fly without having to go through the long process of capture, split and decode. My first instinct was to go back to gnuradio, but then I remembered, that there is the rtl_fm utility in librtlsdr which does everything needed up to the point of outputting raw AM demodulated data on the stdout. So I rewrote the python decoding script to accept the data as a stream of signed shorts on the stdin.

$ rtl_fm -M am -f 433.632M -s 48k -g 50 -l 200 | ./streamdump.py
Found 1 device(s):
  0:  Realtek, RTL2838UHIDIR, SN: AtalSDR

Using device 0: Generic RTL2832U OEM
Found Rafael Micro R820T tuner
Tuner gain set to 49.60 dB.
Tuned to 433884000 Hz.
Oversampling input by: 21x.
Oversampling output by: 1x.
Buffer size: 8.13ms
Exact sample rate is: 1008000.009613 Hz
Sampling at 1008000 S/s.
Output at 48000 Hz.
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0]	179	5	5
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0]	178	4	4

Code

separation script
decoding script
stream decoding script

The first two scripts require numpy and scipy