ADS-B
Est. read time: 6 minutes | Last updated: March 09, 2026 by John Gentile
Contents
- Mode S Extended Squitter Message Format
- Pulse Position Modulation (PPM)
- Phase Correction for Failed CRC Packets
- References
Automatic Dependent Surveillance-Broadcast (ADS-B) is a surveillance technology where aircraft broadcast their position, altitude, velocity, and identification to ground stations and other aircraft. It is “automatic” (no pilot input required), “dependent” (relies on onboard GPS for position), and uses a “broadcast” model (any receiver in range can listen). ADS-B operates on 1090 MHz, sharing the existing Mode S transponder frequency used by secondary surveillance radar, and transmits at a data rate of 1 Mbps.
Mode S Extended Squitter Message Format
An ADS-B transmission is a Mode S “Extended Squitter” (Downlink Format 17) consisting of:
- Preamble (8 µs): Four 0.5 µs pulses at fixed positions (0, 1.0, 3.5, 4.5 µs) used for detection and synchronization.
- Data block (112 µs): 112 bits containing the Downlink Format (5 bits), transponder Capability (3 bits), ICAO aircraft address (24 bits), message payload (56 bits), and CRC parity (24 bits).
Short squitter messages (DF 4, 5, 11, etc.) carry only 56 data bits (the first 56 of the 112-bit frame).
Pulse Position Modulation (PPM)
ADS-B data bits are encoded using Pulse Position Modulation (PPM). Each bit occupies a 1 µs window divided into two 0.5 µs halves:
| Bit Value | First 0.5 µs | Second 0.5 µs |
|---|---|---|
| 1 | Pulse (HIGH) | No pulse (LOW) |
| 0 | No pulse (LOW) | Pulse (HIGH) |
The bit value is determined by which half of the symbol period contains the pulse. This makes demodulation straightforward: compare the energy in the first half against the second half of each 1 µs bit period.
At a sample rate of 2 MHz (the minimum for 1 Mbps PPM), each bit period spans exactly 2 samples, and each pulse occupies 1 sample.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
from scipy.io import wavfile
from rfproto import plot, utils
input_iq = utils.open_iq_file('./data/modes1.bin', np.uint8)
fc = 1.09e9 # ADS-B carrier frequency of 1090 MHz
baud = 1e6 # ADS-B has 1 Mbps data/symbol rate
fs = 2e6 # Sample rate (Hz)
sps = fs / baud # samples per symbol
plot.samples(np.real(input_iq))
plt.title("ADS-B Input I/Q Samples")
plt.show()
plt.specgram(input_iq, Fs=fs, Fc=fc)
plt.title("ADS-B Input I/Q Spectrogram")
plt.xlabel("Time (s)")
plt.ylabel("Frequency (Hz)")
plt.show()


Since ADS-B uses PPM, one of the first steps of demodulation is converting I/Q samples into magnitude detection samples.
# Magnitude detect input signal = sqrt(re^2 + im^2)
mag = np.abs(input_iq)
plot.samples(mag)
plt.show()

# ADS-B preamble: 8 µs with pulses at 0, 1, 3.5, 4.5 µs
# At 2 MHz (0.5 µs/sample), pulses are at sample indices 0, 2, 7, 9
# Each chip = one sample (0.5 µs), no upsampling needed
preamble = np.array([1,0,1,0,0,0,0,1,0,1,0,0,0,0,0,0], dtype=np.float64)
# Zero-mean the template for correlation: subtract mean(preamble) = 4/16 = 0.25
# This gives weights [+0.75, -0.25, +0.75, -0.25, ...] which:
# - Rewards energy at pulse positions (+0.75)
# - Mildly penalizes energy at non-pulse positions (-0.25)
# - Keeps correlation centered near zero for noise (good threshold behavior)
# - Avoids the large negative spikes of full bipolar [+1, -1] since
# the negative weights are only -0.25, not -1.0
preamble_zm = preamble - np.mean(preamble)
print(f"Preamble ({len(preamble)} samples = {len(preamble)/fs*1e6:.1f} µs):")
print(f" Binary: {preamble}")
print(f" Zero-mean: {preamble_zm}")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 3))
ax1.stem(preamble)
ax1.set_title("Binary preamble template")
ax2.stem(preamble_zm)
ax2.set_title("Zero-mean preamble template (for correlation)")
ax2.axhline(y=0, color='k', linewidth=0.5)
plt.tight_layout()
plt.show()
Preamble (16 samples = 8.0 µs): Binary: [1. 0. 1. 0. 0. 0. 0. 1. 0. 1. 0. 0. 0. 0. 0. 0.] Zero-mean: [ 0.75 -0.25 0.75 -0.25 -0.25 -0.25 -0.25 0.75 -0.25 0.75 -0.25 -0.25 -0.25 -0.25 -0.25 -0.25]

# Cross-correlate magnitude signal with zero-mean preamble template
corr = np.correlate(mag, preamble_zm, mode="same")
plot.samples(corr)
plt.title("Preamble Cross-Correlation (zero-mean template)")
plt.show()
print(f"Correlation stats: mean={np.mean(corr):.2f}, std={np.std(corr):.2f}")
print(f"Max correlation at sample {np.argmax(corr)}, value = {np.max(corr):.1f}")

Correlation stats: mean=0.00, std=26.09 Max correlation at sample 106576, value = 293.4
# Find preamble peaks above a threshold
# With zero-mean template, correlation is centered near 0 for noise with clear
# positive peaks for preambles. Use mean + N*std for adaptive thresholding.
# ADS-B long message = 8 µs preamble + 112 µs data = 120 µs = 240 samples
corr_mean = np.mean(corr)
corr_std = np.std(corr)
threshold = corr_mean + 5 * corr_std
peaks, properties = signal.find_peaks(corr, height=threshold, distance=int(120e-6 * fs))
print(f"Correlation mean: {corr_mean:.2f}, std: {corr_std:.2f}")
print(f"Threshold (mean + 5*std): {threshold:.1f}")
print(f"Found {len(peaks)} candidate preambles at samples: {peaks}")
Correlation mean: 0.00, std: 26.09 Threshold (mean + 5*std): 130.5 Found 124 candidate preambles at samples: [ 43482 43984 44693 44965 45788 46084 46521 46901 47561 47858 48207 48641 49073 49563 50007 50313 50601 53061 53562 53878 55335 56835 57106 57522 57831 58140 58564 59299 59608 60097 60469 60915 61211 63028 63461 63755 64154 64425 64860 65131 69661 76009 78851 82753 83024 83297 83569 84189 84657 86799 87919 88234 94405 94678 95270 101056 101328 105870 106303 106576 106847 107157 107466 107775 108083 108393 109010 109319 109591 109936 112245 112517 113431 120066 120337 120683 121104 121424 121814 127368 128109 129123 131952 132225 134816 135213 135582 136101 136419 137087 137903 139205 139704 142319 142664 144501 145303 146238 147753 177922 183680 241357 241629 244078 244351 245913 246420 250211 260469 276090 280077 280349 281228 281773 282278 282587 282897 283712 284131 289094 289367 330059 330751 338289]
# Visualize magnitude around the strongest correlation peak
best_peak = peaks[np.argmax(properties["peak_heights"])]
window = 130 # enough to see preamble + data region
plt.figure(figsize=(12, 3))
region = mag[best_peak - 20 : best_peak + window]
plt.plot(region)
plt.axvline(x=20, color='r', linestyle='--', label='Corr peak (preamble center)')
plt.axvline(x=20 + len(preamble)//2, color='g', linestyle='--', label='Data start')
plt.title(f"Magnitude around strongest preamble (sample {best_peak})")
plt.legend()
plt.show()

def demod_adsb_ppm(mag_signal, peak_idx, fs, preamble_len):
"""Demodulate ADS-B PPM bits starting from a correlation peak.
With mode="same" correlation, peak_idx marks the center of the preamble
template alignment. Data starts preamble_len//2 samples after the peak.
ADS-B PPM encoding (per 1 µs bit period = 2 samples at 2 MHz):
Bit 1: [HIGH, LOW] (pulse in first 0.5 µs)
Bit 0: [LOW, HIGH] (pulse in second 0.5 µs)
"""
sps = int(fs / 1e6) # 2 samples per bit at 2 MHz
data_start = int(peak_idx + preamble_len // 2) # data begins right after preamble
bits = []
for bit_idx in range(112): # max 112 bits (long message)
bit_offset = data_start + bit_idx * sps
early = int(bit_offset) # first sample in bit period
late = int(bit_offset + 1) # second sample in bit period
if late >= len(mag_signal):
break
# PPM decision: first half > second half → bit 1
bits.append(1 if mag_signal[early] > mag_signal[late] else 0)
return bits
packets = []
packet_peaks = [] # track which peak each packet came from
for peak_idx in peaks:
bits = demod_adsb_ppm(mag, peak_idx, fs, len(preamble))
if len(bits) == 112:
packets.append(bits)
packet_peaks.append(peak_idx)
print(f"Demodulated {len(packets)} candidate packets of 112 bits")
Demodulated 124 candidate packets of 112 bits
def bits_to_hex(bits):
"""Convert list of bits to hex string."""
hex_str = ""
for i in range(0, len(bits), 4):
nibble = bits[i]*8 + bits[i+1]*4 + bits[i+2]*2 + bits[i+3]
hex_str += f"{nibble:X}"
return hex_str
def bits_to_int(bits):
"""Convert list of bits to integer."""
val = 0
for b in bits:
val = (val << 1) | b
return val
def adsb_crc(bits):
"""Compute ADS-B 24-bit CRC (Mode S CRC polynomial: 0xFFF409)."""
GENERATOR = 0xFFF409
n_bytes = len(bits) // 8
msg = bits_to_int(bits)
# The last 24 bits are the CRC field; XOR with computed CRC should give 0
for i in range(n_bytes * 8 - 24):
if msg & (1 << (n_bytes * 8 - 1 - i)):
msg ^= GENERATOR << (n_bytes * 8 - 1 - i - 24)
# Remainder is the bottom 24 bits
return msg & 0xFFFFFF
# ADS-B character lookup for callsign decoding
ADSB_CHARSET = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ##### ###############0123456789######"
def decode_adsb(bits):
"""Decode an ADS-B 112-bit message and return a dict of fields."""
msg = {}
msg["hex"] = bits_to_hex(bits)
# Downlink Format (bits 1-5)
df = bits_to_int(bits[0:5])
msg["DF"] = df
# ICAO address (bits 9-32)
icao = bits_to_hex(bits[8:32])
msg["ICAO"] = icao
# CRC check (last 24 bits)
crc_remainder = adsb_crc(bits)
msg["CRC_ok"] = (crc_remainder == 0)
if df == 17: # ADS-B extended squitter
# Type Code (bits 33-37)
tc = bits_to_int(bits[32:37])
msg["TC"] = tc
if 1 <= tc <= 4:
# Aircraft identification
callsign = ""
for i in range(8):
idx = bits_to_int(bits[40 + i*6 : 40 + (i+1)*6])
callsign += ADSB_CHARSET[idx]
msg["callsign"] = callsign.strip()
elif 9 <= tc <= 18:
# Airborne position (CPR encoded)
alt_bits = bits[40:52]
# Altitude: bit 48 is the Q-bit (25ft vs 100ft resolution)
q_bit = alt_bits[7]
if q_bit:
# Remove Q-bit, reassemble, compute altitude in feet
alt_code = bits_to_int(alt_bits[:7] + alt_bits[8:])
altitude = alt_code * 25 - 1000
msg["altitude_ft"] = altitude
msg["CPR_odd"] = bits[53]
msg["lat_CPR"] = bits_to_int(bits[54:71])
msg["lon_CPR"] = bits_to_int(bits[71:88])
elif tc == 19:
# Airborne velocity
subtype = bits_to_int(bits[37:40])
msg["velocity_subtype"] = subtype
if subtype in (1, 2):
# Ground speed (East/West + North/South components)
ew_dir = bits[45]
ew_vel = bits_to_int(bits[46:56]) - 1
ns_dir = bits[56]
ns_vel = bits_to_int(bits[57:67]) - 1
vx = -ew_vel if ew_dir else ew_vel
vy = -ns_vel if ns_dir else ns_vel
speed = np.sqrt(vx**2 + vy**2)
heading = np.degrees(np.arctan2(vx, vy)) % 360
msg["speed_kts"] = round(speed, 1)
msg["heading_deg"] = round(heading, 1)
return msg
# Decode and display only CRC-passing packets
valid_packets = []
n_crc_fail = 0
for i, bits in enumerate(packets):
msg = decode_adsb(bits)
if msg["CRC_ok"]:
valid_packets.append((bits, msg, packet_peaks[i]))
print(f"\nPacket (peak @ sample {packet_peaks[i]}): {msg['hex']}")
print(f" DF={msg['DF']}, ICAO={msg['ICAO']}, CRC=PASS")
if msg["DF"] == 17:
tc = msg.get("TC", "?")
print(f" TC={tc}", end="")
if "callsign" in msg:
print(f", Callsign: {msg['callsign']}", end="")
if "altitude_ft" in msg:
print(f", Altitude: {msg['altitude_ft']} ft", end="")
if "speed_kts" in msg:
print(f", Speed: {msg['speed_kts']} kts, Heading: {msg['heading_deg']}°", end="")
print()
else:
n_crc_fail += 1
print(f"\n--- Summary: {len(valid_packets)} CRC PASS, {n_crc_fail} CRC FAIL "
f"out of {len(packets)} candidates ---")
Packet (peak @ sample 43482): 8F4D20235877A0BBBF997CDB827B DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22850 ft Packet (peak @ sample 44693): 8F4D2023587790BBA5998227C948 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22825 ft Packet (peak @ sample 44965): 8F4D2023991093AD287C148ACCDC DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 388.5 kts, Heading: 157.9° Packet (peak @ sample 48641): 8F4D2023991093AD087C133060D1 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 387.6 kts, Heading: 157.9° Packet (peak @ sample 53061): 8F4D202358777451AB85FC938B46 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22775 ft Packet (peak @ sample 53878): 8F4D2023991093AD087C14CFB0F5 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 387.6 kts, Heading: 157.9° Packet (peak @ sample 56835): 8F4D20235877645165860B69E2BB DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22750 ft Packet (peak @ sample 57106): 8F4D2023991093AD087C133060D1 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 387.6 kts, Heading: 157.9° Packet (peak @ sample 64154): 8F4D2023991093AD087C14CFB0F5 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 387.6 kts, Heading: 157.9° Packet (peak @ sample 64860): 8F4D202358773450D586263C41FF DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22675 ft Packet (peak @ sample 65131): 8F4D2023991093ACE87C133E1D54 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 386.6 kts, Heading: 157.8° Packet (peak @ sample 69661): 8F4D2023587720BA1799D04DB987 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22650 ft Packet (peak @ sample 76009): 8F4D2023587710B9D199DDD3F278 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22625 ft Packet (peak @ sample 86799): 8F4D2023991093ACE87C14C1CD70 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 386.6 kts, Heading: 157.8° Packet (peak @ sample 94405): 8F4D20235875D44F77866E8B8692 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22525 ft Packet (peak @ sample 94678): 8F4D2023991093ACC8801497EF66 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 385.7 kts, Heading: 157.8° Packet (peak @ sample 101056): 8F4D20235875B44F29867BC2A7F9 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22475 ft Packet (peak @ sample 101328): 8F4D2023991093ACC8801497EF66 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 385.7 kts, Heading: 157.8° Packet (peak @ sample 105870): 8F4D2023991093ACC8801497EF66 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 385.7 kts, Heading: 157.8° Packet (peak @ sample 106576): 8F4D2023991093ACC87C1484B159 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 385.7 kts, Heading: 157.8° Packet (peak @ sample 109319): 8F4D2023587590B83D9A2FFCF986 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22425 ft Packet (peak @ sample 109591): 8F4D2023991093ACC87C1484B159 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 385.7 kts, Heading: 157.8° Packet (peak @ sample 112245): 8F4D2023991093ACC8801497EF66 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 385.7 kts, Heading: 157.8° Packet (peak @ sample 120066): 8D4D2023587570B7AD9A4DD39061 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22375 ft Packet (peak @ sample 120337): 8D4D2023991093ACA87C14FBD7D2 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 384.8 kts, Heading: 157.7° Packet (peak @ sample 120683): 8D4D20232004D0F4CB1820B0EFD4 DF=17, ICAO=4D2023, CRC=PASS TC=4, Callsign: AMC421 Packet (peak @ sample 128109): 8D4D2023991092ACA87C14F8DD1C DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 384.4 kts, Heading: 157.8° Packet (peak @ sample 132225): 8D4D2023991092ACA87C14F8DD1C DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 384.4 kts, Heading: 157.8° Packet (peak @ sample 135213): 8D4D2023991092ACA87C15072915 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 384.4 kts, Heading: 157.8° Packet (peak @ sample 139205): 8D4D2023587520B69B9A81BA7E17 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 22250 ft Packet (peak @ sample 142319): 8D4D2023991092ACA87C15072915 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 384.4 kts, Heading: 157.8° Packet (peak @ sample 144501): 8D4D2023991092ACA88014EB8323 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 384.4 kts, Heading: 157.8° Packet (peak @ sample 177922): 8D4D2023991090AC888014A8EA96 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 382.7 kts, Heading: 158.1° Packet (peak @ sample 241357): 8D4D2023586DE0ABB39CA8931613 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 20950 ft Packet (peak @ sample 241629): 8D4D202399108FAC087C14707EFE DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 378.6 kts, Heading: 158.0° Packet (peak @ sample 244078): 8D4D2023586DC44225890EC0E540 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 20900 ft Packet (peak @ sample 244351): 8D4D202399108FABE87C14860C91 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 377.7 kts, Heading: 157.9° Packet (peak @ sample 246420): 8D4D202399108FABE87C14860C91 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 377.7 kts, Heading: 157.9° Packet (peak @ sample 250211): 8D4D202399108FABE87C14860C91 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 377.7 kts, Heading: 157.9° Packet (peak @ sample 280077): 8D4D2023586D143FB3898AB06FA9 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 20625 ft Packet (peak @ sample 280349): 8D4D202399108EABC87014882076 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 376.4 kts, Heading: 158.0° Packet (peak @ sample 289094): 8D4D2023586D00A8AF9D42B9FA54 DF=17, ICAO=4D2023, CRC=PASS TC=11, Altitude: 20600 ft Packet (peak @ sample 289367): 8D4D202399108EABA8701447A40D DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 375.5 kts, Heading: 157.9° Packet (peak @ sample 338289): 8D4D202399108DAB487014673B89 DF=17, ICAO=4D2023, CRC=PASS TC=19, Speed: 372.3 kts, Heading: 157.9° --- Summary: 44 CRC PASS, 80 CRC FAIL out of 124 candidates ---
Phase Correction for Failed CRC Packets
At exactly 2× oversampling (2 MHz sample rate for 1 Mbps PPM), any timing offset between the transmitter and receiver causes inter-symbol interference (ISI): energy from one bit period leaks into its neighbor. The classic dump1090 decoder handles this with a phase correction retry — when a preamble is detected but CRC fails, it estimates the sampling phase error from the preamble itself and compensates each data sample before re-demodulating.
Estimating Phase Offset from the Preamble
The preamble has known pulse positions at samples (“on-time” peaks). If sampling is late, energy leaks backward into the samples immediately before the peaks. If sampling is early, energy leaks forward into the samples after the peaks. We measure this by summing energy at diagnostic positions:
where is the sample just before the preamble start, is just before the third pulse, is just after the second pulse, and is just after the fourth pulse. The factor of 2 compensates for using only 2 diagnostic samples vs. the 4 on-time peaks.
Computing Scale Factors
If , sampling is late (energy bleeds backward). The fractional leakage ratio is:
We define two fixed-point scale factors (with unity = 16384):
The symmetric case applies when (sampling is early), using the late energy ratio instead.
Applying the Correction
For each data sample , the correction depends on the adjacent bit’s value, since ISI only occurs when a neighboring bit has energy:
- If the neighboring bit (on the side energy is leaking from) was a 1-bit (has a pulse), the current sample has excess leaked energy → scale down by
- If the neighboring bit was a 0-bit (no pulse on that side), the current sample lost energy to the neighbor → scale up by
where depends on the direction of leakage and the adjacent bit. After rescaling all data samples, bits are re-demodulated and CRC is re-checked.
def apply_phase_correction(mag_signal, preamble_start, bits, sps):
"""Apply dump1090-style phase correction to data samples.
Estimates sampling phase offset from preamble energy leakage,
then rescales each data sample to compensate for ISI.
Args:
mag_signal: full magnitude signal array
preamble_start: index of preamble start in mag_signal
bits: initially demodulated bits (used to determine adjacent bit values)
sps: samples per symbol (2 at 2 MHz)
Returns:
corrected copy of the data region magnitude samples
"""
m = mag_signal
j = preamble_start
# Energy at known on-time preamble peak positions
on_time = m[j+0] + m[j+2] + m[j+7] + m[j+9]
# Energy at early-leak diagnostic positions (just before peaks 3 and 4)
# m[j-1] is just before preamble, m[j+6] is just before peak at j+7
early_idx = max(j - 1, 0)
early = 2.0 * (m[early_idx] + m[j+6])
# Energy at late-leak diagnostic positions (just after peaks 2 and 4)
# m[j+3] is just after peak at j+2, m[j+10] is just after peak at j+9
late = 2.0 * (m[j+3] + m[j+10])
UNITY = 16384.0 # fixed-point unity scale
if early > late:
# Sampling is late: energy leaks backward
alpha = early / (early + on_time) if (early + on_time) > 0 else 0
scale_up = UNITY + UNITY * alpha
scale_down = UNITY - UNITY * alpha
leak_direction = "backward" # energy came from the NEXT sample
else:
# Sampling is early: energy leaks forward
alpha = late / (late + on_time) if (late + on_time) > 0 else 0
scale_up = UNITY + UNITY * alpha
scale_down = UNITY - UNITY * alpha
leak_direction = "forward" # energy came from the PREVIOUS sample
# Copy data region for correction
data_start = j + 16 # 16 preamble samples
n_data_samples = len(bits) * sps
corrected = m[data_start : data_start + n_data_samples].copy().astype(np.float64)
for bit_idx in range(len(bits)):
for half in range(sps): # each bit has 'sps' samples (2 at 2 MHz)
sample_idx = bit_idx * sps + half
if sample_idx >= len(corrected):
break
if leak_direction == "backward":
# Energy leaks backward: the NEXT bit's pulse affects current sample
if half == 1:
# Second sample of bit period: leaked energy from next bit's first sample
if bit_idx < len(bits) - 1 and bits[bit_idx + 1] == 1:
corrected[sample_idx] = corrected[sample_idx] * scale_down / UNITY
else:
corrected[sample_idx] = corrected[sample_idx] * scale_up / UNITY
else:
# First sample of bit period: leaked energy from previous bit's second sample
if bit_idx > 0 and bits[bit_idx - 1] == 0:
# Previous bit was 0 → its second half had a pulse → leaks backward
corrected[sample_idx] = corrected[sample_idx] * scale_down / UNITY
else:
corrected[sample_idx] = corrected[sample_idx] * scale_up / UNITY
else:
# Energy leaks forward: the PREVIOUS bit's pulse affects current sample
if half == 0:
# First sample: leaked energy from previous bit's second sample
if bit_idx > 0 and bits[bit_idx - 1] == 0:
# Previous bit was 0 → second half had pulse → leaks forward
corrected[sample_idx] = corrected[sample_idx] * scale_down / UNITY
else:
corrected[sample_idx] = corrected[sample_idx] * scale_up / UNITY
else:
# Second sample: leaked energy from current bit's first sample (always known)
if bits[bit_idx] == 1:
# Current bit is 1 → first half had pulse → leaks into second half
corrected[sample_idx] = corrected[sample_idx] * scale_down / UNITY
else:
corrected[sample_idx] = corrected[sample_idx] * scale_up / UNITY
return corrected
def demod_from_corrected(corrected_data, sps):
"""Re-demodulate PPM bits from phase-corrected magnitude data."""
bits = []
for bit_idx in range(len(corrected_data) // sps):
early = corrected_data[bit_idx * sps]
late = corrected_data[bit_idx * sps + 1]
bits.append(1 if early > late else 0)
return bits
# Re-process CRC-failing packets: attempt phase correction
sps_int = int(sps)
valid_peak_set = {p for _, _, p in valid_packets}
corrected_packets = []
for i, (bits, peak_idx) in enumerate(zip(packets, packet_peaks)):
if peak_idx in valid_peak_set:
continue # already passed CRC
crc_remainder = adsb_crc(bits)
if crc_remainder == 0:
continue
# Estimate preamble start from correlation peak (peak is at template center)
preamble_start = int(peak_idx - len(preamble) // 2)
if preamble_start < 1 or preamble_start + 16 + 112 * sps_int >= len(mag):
continue
# Apply phase correction and re-demodulate
corrected_data = apply_phase_correction(mag, preamble_start, bits, sps_int)
new_bits = demod_from_corrected(corrected_data, sps_int)
new_crc = adsb_crc(new_bits)
if new_crc == 0:
corrected_packets.append(new_bits)
msg = decode_adsb(new_bits)
old_hex = bits_to_hex(bits)
print(f"Phase correction recovered packet!")
print(f" Before: {old_hex} (CRC FAIL)")
print(f" After: {msg['hex']} (CRC PASS)")
print(f" DF={msg['DF']}, ICAO={msg['ICAO']}", end="")
if msg["DF"] == 17:
tc = msg.get("TC", "?")
print(f", TC={tc}", end="")
if "callsign" in msg:
print(f", Callsign: {msg['callsign']}", end="")
if "altitude_ft" in msg:
print(f", Altitude: {msg['altitude_ft']} ft", end="")
if "speed_kts" in msg:
print(f", Speed: {msg['speed_kts']} kts, Heading: {msg['heading_deg']}°", end="")
print("\n")
if not corrected_packets:
print("No additional packets recovered via phase correction.")
print(f"\nFinal summary: {len(valid_packets)} initial CRC PASS + "
f"{len(corrected_packets)} recovered = "
f"{len(valid_packets) + len(corrected_packets)} total valid packets")
No additional packets recovered via phase correction. Final summary: 44 initial CRC PASS + 0 recovered = 44 total valid packets
References
- adsb.lol: real-time, worldwide flight tracking of ADS-B data.
- readsb: one of the most up-to-date ADS-B decoders.
- dump1090-fa:
dump1090version maintained by FlightAware. - dump1090_rs: Multi-SDR supported Rust translation of the popular dump1090 project for ADS-B demodulation
- antirez/dump1090: one of the original ADS-B decoders with support for RTL-SDRs.
- gr-adsb: A GNU Radio out-of-tree (OOT) module to demodulate and decode Automatic Dependent Surveillance Broadcast (ADS-B) messages.
- HW/SW Co-Design Implementation of ADS-B Receiver Using Analog Devices AD9361/AD9364