Wireless glass break sensor protocol decoding

Posted by Łukasz Hawryłko on May 20, 2021 · 9 mins read

It is quite common that you can find some devices that are intended to be scrapped, but after speaking to the correct person you can intercept that process. I had a similar situation with sensors for alarm systems, to be precisely - glass break and PIR sensors that operates wirelessly. Unfortunately, I received only devices; no documentation, no specs, no gateway that works with them. Nevertheless, I decided to do a little reverse engineering and try to build such a gateway by my own.

I have started with glass break sensor and in this post I will try to briefly describe my findings.

Let’s receive something

Sensor is dedicated to European market, so it is highly possible that it uses one of the common ISM bands - 433MHz or 868MHz. To confirm that I dismantled it and look for RF part. Without much effort I found crystal oscillator rated at 433MHz so it looks like that my predictions regarding communication frequency were correct.

RF part of glass break sensor PCB

After short analysis of RF part I discovered that it is very simple and there is no receiving module. Sensor only transmits its current state and whole communication is one-way. That makes my work much simpler than I expected.

I need only a receiver that has enough bandwidth to cover 433MHz band, ideal solution for this is very common and cheap SDR dongle based on RTL2832U chip. After configuring it to proper frequency I was able to see signal from the sensor.

Received signal

I passed received signal through AM demodulator and record output to wav file.

Decoding

To continue my analysis I opened recorded file with Audacity to check how its waveform looks like. Without much effort I get an image where single bits are clearly visible. Looking at entropy of that data it is obvious that some kind of coding is applied. This is an expected operation when sending data using RF.

Raw bits received

Now it’s time to solve a puzzle. First part of received signal is likely a preamble, so I don’t look at it. I expect that somewhere should be an ID of the sensor that is printed on the box: 0975F0D. After a few minutes of investigation I figured out the coding. It’s very simple - it is based on position of the pulses. If distance from previous pulse (time of low state) is long, this is 1, if short - 0. This a variation of PPM (pulse position modulation).

Here is the signal from previous picture with annotated decoded values. After a preamble there is an expected sensor ID transmitted.

How to decode

Building a receiver

I can now receive, demodulate and decode data from sensor. I don’t want to use RTL2832 in final solution because it requires PC to operate. It’s time to build a receiver based on transceiver IC. I decided to use CC1101 chip as I am quite familiar with it.

Following CC1101 configuration allows to properly receive data:

  • frequency - 433.92MHz
  • channel width - 100kHz
  • modulation - OOK
  • data rate - 8kbps
  • packet size - infinite
  • sync mode - carrier sense
  • preamble detection - disabled
  • CRC - disabled

PPM decoding and framing will be done by software.

Framing

Next step is to assign decoded values to the numbers and define a frame. Here are few consecutive frames received after triggering tamper alarm on the sensor.

0D 5F 97 A0 AF 00 0D
0D 5F 97 A1 A0 01 07
0D 5F 97 A2 2F 00 0F
0D 5F 97 A3 20 01 09
0D 5F 97 A4 AF 01 0F
0D 5F 97 A5 A0 00 09
0D 5F 97 A6 2F 01 01
0D 5F 97 A7 20 00 0B
0D 5F 97 A0 AF 00 0D
0D 5F 97 A1 A0 01 07 
0D 5F 97 A2 2F 00 0F
0D 5F 97 A3 20 01 09

First 3 bytes are device ID as shown before. Next 4 bits look like frame counter from 0 to 7. Then there is something that I called state (12 bits) and ending 2 bytes are parity and checksum.

Here is the code that extracts values from the frame:

struct Frame {
    dev_id: u32,
    seq: u8,
    state: u16
}
Frame {
    dev_id: data[0] as u32 | (data[1] as u32) << 8 | (data[2] as u32) << 16,
    seq: data[3] & 0xf,
    state: (data[3] as u16 & 0xf0) >> 4 | (data[4] as u16 & 0x3f) << 4
}

State value is a combination of 2 bit values for each input pin of the IC in the sensor. There is also an information about low battery. Each 2 bit values holds two information: 1. actual state of the pin and 2. if the state has been changed since previous transmission. Transmission is triggered by pin level change.

Parity and checksum is a different story. I don’t know what both values are present, maybe for some backward compatibility reason. Parity was easy to calculate, however I spent much of time to figure out how checksum is generated. Finally I ended up with the following code:

fn check_frame(data: &[u8]) -> Result<(), Error> {
    let mut ones = 0;
    for b in &data[0..5] {
        ones += b.count_ones();
    }
    
    if data[5] as u32 != ones & 1 {
        return Err(Error::ParityError);
    }

    if data[4] & (1 << 7) == 0 {
        ones += 1;
    }

    if data[6] as u32 >> 1 != ones & 7 {
        return Err(Error::ChecksumError(data[6] as u32 >> 1, ones & 7));
    }

    Ok(())
}

Working receiver

Here are data received and interpreted.

Frame { dev_id : 9920269 , seq : 3 , state : 522 }
GlassState { in0_state : false , in0_change : false ,
             in1_state : false , in1_change : false ,
             in2_state : true , in2_change : false ,
             in3_state : true , in3_change : false ,
             in4_state : false , in4_change : false }

Frame { dev_id : 9920269 , seq : 4 , state : 762 }
GlassState { in0_state : true , in0_change : true ,
             in1_state : true , in1_change : true ,
             in2_state : true , in2_change : false ,
             in3_state : true , in3_change : false ,
             in4_state : false , in4_change : false }

At this point I have fully working receiver for this type of sensors, it receives data, validates if checksum is correct and recognizes tamper or glass break alarm.