Fast decoder for Mode-S and ADS-B messages in Python. Ground-up v3 rewrite of pyModeS with a single unified decode() function.
pip install "pyModeS>=3"Python 3.11+ required.
The decode() function returns a Decoded dict with every decodable
field populated in one pass.
from pyModeS import decode
result = decode("8D406B902015A678D4D220AA4BDA")
print(result)
# {
# 'df': 17,
# 'icao': '406B90',
# 'crc_valid': True,
# 'typecode': 4,
# 'bds': '0,8',
# 'callsign': 'EZY85MH',
# 'category': 0,
# 'wake_vortex': 'No category information',
# }Pass a list of hex strings and parallel timestamps. Any mix of downlink formats, typecodes, and Comm-B registers is fine — the dispatcher routes each message to the right decoder and uses the timestamps to resolve CPR pairs and disambiguate ambiguous Comm-B registers.
from pyModeS import decode
results = decode(
[
"8D406B902015A678D4D220AA4BDA", # DF17 BDS 0,8 identification
"8D485020994409940838175B284F", # DF17 BDS 0,9 airborne velocity
"8D40058B58C901375147EFD09357", # DF17 BDS 0,5 airborne pos (even)
"8D40058B58C904A87F402D3B8C59", # DF17 BDS 0,5 airborne pos (odd)
"A000178D10010080F50000D5893C", # DF20 BDS 1,0 data link capability
"A8000D9FA55A032DBFFC000D8123", # DF21 BDS 6,0 heading & speed
],
timestamps=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
)
assert results[0]["callsign"] == "EZY85MH"
assert results[1]["groundspeed"] == 159
assert results[3]["latitude"] is not None # CPR pair resolvedSurface CPR needs a reference within ~45 NM. Pass an ICAO airport
code (looked up in the shipped airport database) or an explicit
(lat, lon) tuple.
from pyModeS import decode
# Real DF18 surface movement on LFBO (Toulouse-Blagnac).
r = decode("903a23ff426a4e65f7487a775d17", surface_ref="LFBO")
print(r["latitude"], r["longitude"]) # 43.6264..., 1.3747...PipeDecoder is stateful — it holds per-ICAO state across calls,
matches CPR pairs automatically, evicts stale aircraft after a
TTL, and flags DF20/21 messages as icao_verified when their
CRC-derived ICAO was already seen in a clean DF17/18 plaintext.
from pyModeS import PipeDecoder
pipe = PipeDecoder(surface_ref="EHAM")
for msg, timestamp in stream:
decoded = pipe.decode(msg, timestamp=timestamp)
if "latitude" in decoded:
print(decoded["icao"], decoded["latitude"], decoded["longitude"])See docs/quickstart.md for the full tour
(full-dict mode, error handling, attribute access).
For ad-hoc message inspection that doesn't need a full decode,
pyModeS.util exposes thin wrappers around the bit/hex/CRC
primitives: hex2bin, bin2int, hex2int, bin2hex, crc,
df, icao, typecode, altcode, idcode, cprNL.
from pyModeS.util import hex2bin, crc, icao, typecode
msg = "8D406B902015A678D4D220AA4BDA"
hex2bin(msg)[:16] # '1000110101000000'
crc(msg) # 0 — valid DF17
icao(msg) # '406B90'
typecode(msg) # 4 — ADS-B identificationpyModeS ships with a modes command-line tool for ad-hoc decoding
and live streaming.
# Decode one hex message (pretty-printed JSON)
modes decode 8D406B902015A678D4D220AA4BDA
# Decode several messages inline — comma-separated, emits JSON lines
modes decode 8D40058B58C901375147EFD09357,8D40058B58C904A87F402D3B8C59,8D406B902015A678D4D220AA4BDA
# With airborne CPR reference (single message only)
modes decode 8D40058B58C901375147EFD09357 --reference 49.0 6.0
# Compact JSON piped to jq
modes decode 8D406B902015A678D4D220AA4BDA --compact | jq .
# Decode a file of hex messages (one per line OR timestamp,hex CSV)
modes decode --file captures/flight.log
# Stdin + surface CPR
cat taxi.log | modes decode --file - --surface-ref LFBO# Stream decoded JSON lines from a dump1090-style beast feed
modes live --network localhost:30005
# Tee output to a file
modes live --network host:30005 --dump-to flight.jsonl
# Stream from the TU Delft public feed (live aircraft over Europe)
modes live --network airsquitter.lr.tudelft.nl:10006
# Interactive live aircraft table (requires pyModeS[tui] extra)
pip install "pyModeS[tui]"
modes live --network host:30005 --tuiMode-S Beast binary format is supported (dump1090 port 30005 and
equivalents). See docs/quickstart.md for
the full command reference.
- Unified
decode()returns every decodable field in one dict - Batch mode preserves list length (errors become error-dicts, not exceptions)
PipeDecoderfor streams: per-ICAO state, CPR pair accumulation, TTL eviction, DF20/21 ICAO verification via trusted-set promotionfull_dict=Truepopulates every key in the canonical 123-field schema for pandas / parquet workflowsknown=aircraft state disambiguates Comm-B BDS 5,0/6,0 ambiguity- Airport ICAO database for surface CPR resolution (
surface_ref="EHAM") - Type-checked under mypy strict across all source files
- Golden-file oracle regression test against
pyModeS 2.21.1
Measured on jet1090's
long_flight.csv
(172,432 Beast-format messages, 7 runs × 1 loop, mean timings,
single-core only):
| Decoder | Wall time | Throughput | vs pyModeS v3 |
|---|---|---|---|
| pyModeS v3 (pure Python) | 2.06s ± 0.01 | 83,549 msg/s | 1.00× |
| pyModeS 2.21.1 (Python with compiled C) | 5.03s ± 0.01 | 34,303 msg/s | 0.41× |
| rs1090 (Rust) | 5.60s ± 0.01 | 30,798 msg/s | 0.37× |
| pyModeS 2.21.1 (Python) | 9.09s ± 0.02 | 18,959 msg/s | 0.23× |
pyModeS v3 is 2.44× faster than pyModeS 2.21.1's compiled C
extension, 4.41× faster than pyModeS 2.21.1 pure-Python, and
2.71× faster than rs1090's single-core Rust — all while remaining
pure Python with no C/Cython build.
Reproduce with scripts/benchmark_decode.py.
- DF4 / DF20: altitude code (surveillance altitude reply)
- DF5 / DF21: identity code (squawk)
- DF11: all-call reply (partial — II/SI decoding deferred)
- DF17 / DF18 ADS-B:
- TC 1-4 (BDS 0,8): identification + category
- TC 5-8 (BDS 0,6): surface position
- TC 9-18 (BDS 0,5): airborne position (barometric altitude)
- TC 19 (BDS 0,9): airborne velocity (all 4 subtypes)
- TC 20-22 (BDS 0,5): airborne position (GNSS altitude)
- TC 28 (BDS 6,1): aircraft status
- TC 29 (BDS 6,2): target state and status
- TC 31 (BDS 6,5): operational status
- DF20 / DF21 Comm-B:
- BDS 1,0: data link capability
- BDS 1,7: common-usage GICB capability
- BDS 2,0: aircraft identification
- BDS 3,0: ACAS active resolution advisory
- BDS 4,0: selected vertical intention
- BDS 4,4: meteorological routine air report
- BDS 4,5: meteorological hazard report
- BDS 5,0: track and turn report
- BDS 6,0: heading and speed report
pyModeS 3 is not backwards-compatible with pyModeS 2.x. The
function-per-field API (pms.adsb.callsign(msg), ...) is replaced
by a single decode() returning a dict. See the migration
guide for the full equivalence table.
If you aren't ready to migrate:
pip install "pyModeS<3"Both v2 and v3 coexist on PyPI because they use different import
names (pyModeS vs pyModeS).
The full docs live under docs/ and are published via MkDocs + Material.
To rebuild locally:
# One-shot build (strict mode fails on warnings). Output → site/
uv run --with mkdocs-material --with mkdocs-include-markdown-plugin --with "mkdocstrings[python]" mkdocs build --clean --strict
# Live-reload dev server at http://127.0.0.1:8000
uv run --with mkdocs-material --with mkdocs-include-markdown-plugin --with "mkdocstrings[python]" mkdocs serve- Source: https://github.com/junzis/pyModeS
- Changelog: CHANGELOG.md
- Issues: https://github.com/junzis/pyModeS/issues
- License: GPL-3.0 (see
LICENSE)
pyModeS is a project created by Junzi Sun, who works at TU Delft, Aerospace Engineering Faculty. It is supported by many contributors from different institutions.
If you use pyModeS in academic work, please cite:
@article{sun2019pyModeS,
author={J. {Sun} and H. {V\^u} and J. {Ellerbroek} and J. M. {Hoekstra}},
journal={IEEE Transactions on Intelligent Transportation Systems},
title={pyModeS: Decoding Mode-S Surveillance Data for Open Air Transportation Research},
year={2019},
doi={10.1109/TITS.2019.2914770},
ISSN={1524-9050},
}