Table of Contents

  • Obtaining the carrier frequency
  • Obtaining the bits of the frame
  • Obtaining bitrate
  • Saving data into files
  • Plot data to analyze the streams
  • Conclusions

Obtaining the carrier frequency

First of all, we need to find out the exact frequency in which the remote key is working. I know that all of these devices work in the ISM band, so the emiting frequency must be around 433 MHz or 868 MHz.

On GNU Radio, we are going to add th block UHD: USRP Source. This module reads the data received from the B206mini. The configuration of this module is quite simple, for the first approach, we just need to configure the center frequency, which will be 433 MHZ in the first attempt, and the sampling frequency. that will be configured in 5 Msps. Finallt, we will configure the gain to 30 dB. This level must be adjusted in case that the amplitude of signal received is too small.

Since the center frequency will be used in some other blocks, we can use a Variable block to define it, and then use the name of the variable.

USRP source block configuration

Now, we need to add the blocks neede to watch the received signal. In this case we are going to add from the Instrumentation group a QT GUI Frequency Sink, that will calculate the FFT, and allow us to see the power received and the frequency, and alfo a QT GUI Waterfall Sink, that will act as a persistence for the frequency, allowing us to see in an easy way the frequency received. In the configuration of these two modules, we will need to set the center frequency using the variable created before.

Basic GNURadio diagram

At this point, we can generate the python code and run.

When the code is running, the B206mini is reading samples and sending them to GNU Radio, so we can press the button of the remote key to read the signal.

In the next figure we can see that after pressing the buttong six times, the waterfall shows red points in the frequency 434 MHz. Also, in the FFT sink, at the moment that I press the button, we can see the power received.

Waterfall for finfing out the carrier frequency

After this test, we can ensure that the remote key is emitting in the 434 MHz frequency.

Now, we know the emitting frequency, but we still don’t know the type of modulation in which the data is sent.

To find the modulation type, we can take a look to the signal received by using a QT GUI TIME Sink, that will show us the shape signal received like an oscilloscope.

Notice that the out port of the UHD: USRP Source will give us the samples in the format complex float32, so we are going to read the real and the imaginary parts of the signal.

To configure this module, we need to configure two input ports, and name the two channels in the config tab. Also, we can turn on the Control Panel, so it will add a confgiuration panel in the GUI to configure the basic options of an oscilloscope from the GUI.

Time sink configuration

Now, after some test, we can arrive a valid configuration for the trigger level, and X and Y ranges, so we will be able of capturing the signal.

Te modulation used for all these devices is ASK or OOK. This means like the digital data 1 or zero is represented by the presence of the absence of a signal.

Signal received from remote

OOK modulation

Increasing the X range, we will be able to see the complete frame reveived. In this case, the frame is divides into three different streams of data. This streams of data have to be translated into a set if bits that contains the information.

Complete frame from remote

Obtaining the bits of the frame

As I mentioned before, the OOK modulation uses the presence of the signal to send a ‘1’, and the absence of any signal to send a ‘0’. With this in mind, we can create a simple decoder in GNURadio obtaining the magnitud the the signal received.

We will modify the flowgraph by adding a Complex to Mag block, and also, since the GUI TIME Sink will work this time with float32 data, we can add also a Complex to Float to convert the IQ data from the source to a complex float32 data.

OOK demodulation

Running this flowgraph, and pressing the button of the remote key, we will see the IQ signal, and also the signal envelope, that represents the binary data.

Decoded bits

In the next figure we can see the second stream received from the thee streams sent by the remote key.

Remote key frame

Now, to convert this into a binary signal, add a Threshold block and set the low and high thresholds according to the received signal. In my case, it outputs ‘0’ when the input is below 0.010 and ‘1’ when it is above 0.016.

Threshold configuration

Running this flowgraph, we will obtain am stream of zeros and ones that are the frame sent by the remote key.

Binary data

First frame binary data

Obtaining bitrate

At this point, we have the binary data, but in order to execute the demodulation, we need to have only one point per bit, so we need to obtain the bitrate of the signal to perform a decimation later.

To obtain the bitrate, we can use the GUI Time Sink, and measure manually the bit time.

Bitrate calculation

In this case, the bitrate is around 3816 bps.

Saving data into files

GNURadio is an excellent tool even for decoding data, however I feel more comfortable using a Python code to decode it, so I need to capture frames received from the key, and store it into a file. We can do this in different ways, the first one, using a File Sink block, and save all the data received. Then, in Python, find where the frames were received, and analyze them. This results in a huge file that will fill your RAM, so this is not a valid option.

The second way, and the way I used, is store in files only the received frame. This method has several advantages, the main ones, files are small, and also we don’t need to find the frame in the file, since all the files only contains the frame.

To do this, we need first to detect when a frame is received, which is not difficult because we can use the output of the Threshold block to detect the first ‘1’ of the frame. Then using this signal as trigger, we need to hold this signal in ‘1’ during the reception of the frame. I do this using a moving average filter, so while the received data is ‘1’, the filter will output at least a 1 during the length of the filter. Then, when the last ‘1’’ is received, it will keep the output diferent to zero the time corresponding with the length of the filter time the sampling period. In my case, I configured a lenght of 10k to obtain good results.

Finally, we need to use the output of the filter as a switch to create a new file, and store data in this file as long as the output of the filter is 1, then, once this signal is 0, we need to close the file and wait for the next stream.

This can be done using the blocks Burst Tagger and Tagged File Sink. The first block will send a tag while the Trigger input is True, and the second one will store data in file when the value of the burst tag is True. So we just need to make that the Burst Tagger sends the burst tag to True when we want store data into file.

The resulting flowgraph is shown in the next figure. I also added the Tag Debug just to make GNU Radio show in the log when the value of the tag changes.

Block diagram with file creation

Running this flowgraph, we can see that the value of the tag is added, and it becomes false (burst: #f) when the frame ends.

File generation trigger

By sending diferent frames, we can check that they are stored in different file in the same location where the GNU Radio project is located.

Different files generated

Plot data to analyze the streams

Now it is time to analyze the data. The next Python script take the data from the file, reduce the number of points in a factor of 100, and plot data usinf the Plotly library, that generates interactive plots.

import numpy as np
import glob
import plotly.graph_objs as go
from plotly.offline import plot

# Decimation factor
decimation_factor = 100

# Search .dat files in the current directory
data_files = glob.glob("*.dat")

# generate plotly traces for each data file
traces = []
for file in data_files:
    data = np.fromfile(open(file), dtype=np.float32)
    
    data_decimate = data[::decimation_factor]
    trace = go.Scatter(
        x=np.linspace(0, len(data_decimate)-1, len(data_decimate)),
        y=data_decimate,
        mode='lines+markers',
        name=file
    )
    traces.append(trace)
# Create layout for the plot
layout = go.Layout(
    title='Data from .dat files',
    xaxis=dict(title='X-axis'),
    yaxis=dict(title='Y-axis'),
)
fig = go.Figure(data=traces, layout=layout)

# Save the plot as an HTML file
plot(fig, filename='data_plot.html')

The next plot shows 4 different frames captures.

Data decimated plot

These keys use Manchester encoding to transmit data, which improves receiver synchronization. To decode the signal, we first need to determine the number of samples per bit. Earlier we measured a bit rate of 3816 bps; given the 5 MS/s sampling rate and the decimation applied earlier, we can compute the samples-per-bit value. To make it work reliably, I changed the decimation factor to 2 so there are enough samples per bit.

SPS=5e6DecimationFactor3816
# Manchester decoding function with dynamic synchronization
def manchester_decode(data, samples_per_bit):
    decoded_bits = []
    i = 0

    while i < len(data) - samples_per_bit:
        first_half = data[i:i + samples_per_bit // 2]
        second_half = data[i + samples_per_bit // 2:i + samples_per_bit]
        
        if np.mean(first_half) > np.mean(second_half):
            decoded_bits.append(1)
        else:
            decoded_bits.append(0)
        
        i += samples_per_bit
    return decoded_bits

Then, by detecting the edges of the binary data, we can recover the transmitted bits and select a single sample per bit.

In the next figure, we can see the data obtained from the key. If we analyze the signal, we find that the first data segment is the preamble, which is the same for all transmissions. The second and third segments carry the actual data. In these frames, the first third contains fixed data, where the key sends the command, and the remainder contains variable data, the rolling code of the key.

Data decoded plot

Conclusions

In this article I showed that, with a USRP B206mini-1 and a straightforward workflow, it’s feasible to capture, synchronize, and decode a car key’s RF frame directly from I/Q samples. The essentials are measuring the symbol rate accurately, setting sampling/decimation accordingly, and using a threshold with hysteresis to extract a stable binary signal.

Using the flowgraph presented—which detects new frames and writes each one to a separate file—it’s easy to automate the workflow by invoking the second Python script. How about running the whole setup on a battery-powered Raspberry Pi?