Skip to main content
The ADS-B Demod (adsb_demod) is the second block in the gr-adsb pipeline. It reads the tagged float stream produced by the Framer, demodulates each ADS-B burst using Pulse Position Modulation (PPM) detection, and publishes a PMT PDU containing the raw demodulated bits and burst metadata on the demodulated message port.

Block info

PropertyValue
GRC block IDadsb_demod
GRC labelADS-B Demod
GRC category[ADS-B]
Python classadsb.demod
Constructoradsb.demod(fs)
Block typesync block

Parameters

fs
float
default:"2e6"
Sample rate in samples per second. Must be an integer multiple of 1 MHz (fs % 1e6 == 0). The block computes sps = int(fs // 1e6) to determine the number of samples per symbol, and uses sps // 2 as the half-symbol offset for PPM sampling.

Ports

Stream ports

LabelDirectionDomainData typeVector length
ininputstreamfloat321
outoutputstreamfloat321
The output stream is a direct pass-through of the input with no sample modification. This allows the signal to be routed to other consumers (e.g., a sink or display) in parallel.

Message ports

LabelDirectionDescription
demodulatedoutputPMT PDU containing raw bits and burst metadata

PPM demodulation

ADS-B encodes each bit as a pair of half-symbol intervals. A 1 bit has a high pulse in the first half and a low pulse in the second half; a 0 bit has the reverse. For each of the 112 bits in a maximum-length ADS-B frame, the Demod block samples the input stream at two positions relative to the start of the burst:
  • bit1_idx: sob_idx + bit_number * sps — first half of the symbol (expected high for a 1)
  • bit0_idx: sob_idx + bit_number * sps + sps // 2 — second half of the symbol (expected high for a 0)
A bit is decoded as 1 if the amplitude at bit1_idx exceeds the amplitude at bit0_idx, and 0 otherwise. The block also computes a confidence metric for each bit:
bit_confidence[i] = 10 * (log10(bit1_amp[i]) - log10(bit0_amp[i]))
Positive values indicate higher confidence of a 1; negative values indicate higher confidence of a 0; values near zero are ambiguous.

Demodulated PDU structure

Each detected burst produces one PMT PDU published on the demodulated port. The PDU is a PMT cons cell: (metadata_dict . data_vector).

Metadata dict

timestamp
float
UTC Unix timestamp (seconds since 1970-01-01 00:00:00 UTC) of the burst, derived from the block’s startup time plus the burst sample offset divided by fs.
snr
float
Signal-to-noise ratio in dB, as estimated by the Framer and propagated via the burst stream tag.

Data vector

A numpy array of uint8, always 112 elements long. Each element is 0 or 1 representing one demodulated PPM bit. Bits are in transmission order: the first 5 bits encode the Downlink Format (DF), followed by the message payload and 24-bit CRC.

Extracting bits in a custom block

import pmt
import numpy as np

def handle_demodulated(self, pdu):
    meta = pmt.to_python(pmt.car(pdu))
    bits = pmt.to_python(pmt.cdr(pdu))  # numpy uint8 array, length 112

    timestamp = meta["timestamp"]
    snr       = meta["snr"]

    # Downlink Format is in the first 5 bits
    df = int("".join(map(str, bits[0:5])), 2)
    print(f"DF={df}, SNR={snr:.1f} dB, timestamp={timestamp:.3f}")

Straddled-packet handling

If the end of a burst falls beyond the current work() call’s input buffer, the block sets an internal straddled_packet flag and skips demodulation for that burst. The flag is cleared at the start of the next work() call. Straddled packets are not recovered; they are silently dropped.

Instantiation example

import gnuradio.adsb as adsb

demod = adsb.demod(fs=2e6)
Connect the Framer’s out stream port to the Demod’s in stream port, then connect the Demod’s demodulated message port to the Decoder’s demodulated message port.

Build docs developers (and LLMs) love