import pyModeS
result = pyModeS.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',
# }The returned object is a Decoded — a subclass of dict with
attribute-style access, JSON serialization, and pandas/parquet
compatibility. Read individual fields either by key or as an
attribute:
result["icao"] # '406B90'
result.icao # '406B90' — same thing
result["callsign"] # 'EZY85MH'
result.get("altitude") # None — missing keys are safe via .get()Pass a list of messages plus timestamps to run them through a
transient PipeDecoder. The batch can mix any downlink formats,
typecodes, and Comm-B registers — the dispatcher picks the right
decoder per message:
results = pyModeS.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" # identification
assert results[1]["groundspeed"] == 159 # velocity
assert results[3]["latitude"] is not None # CPR pair resolved
assert results[4]["bds"] == "1,0" # Comm-B capability
assert results[5]["magnetic_heading"] is not None # Comm-B BDS 6,0Errors in the batch become error-dicts ({"error": ..., "raw_msg": ...})
so the output list length always matches the input length.
PipeDecoder holds per-ICAO state across calls. This lets it:
- Resolve CPR pairs from consecutive even/odd frames
- Disambiguate BDS 5,0/6,0 Comm-B using recently-observed state
- Verify DF20/21 ICAO addresses against ones learned from DF17/18
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"])
print(pipe.stats) # {'total': ..., 'decoded': ..., 'crc_fail': ..., 'pending_pairs': ...}See the PipeDecoder deep-dive for the full state model.
Surface CPR (BDS 0,6) needs a reference within ~45 NM. Pass either
an ICAO airport code (looked up in the shipped database) or an
explicit (lat, lon) tuple:
# Airport code — real DF18 surface movement from the jet1090 corpus,
# aircraft on LFBO (Toulouse-Blagnac) taxiway
r = pyModeS.decode("903a23ff426a4e65f7487a775d17", surface_ref="LFBO")
print(r["latitude"], r["longitude"]) # 43.6264..., 1.3747...
# Explicit tuple (e.g., receiver location)
r = pyModeS.decode("903a23ff426a4e65f7487a775d17", surface_ref=(43.63, 1.37))pyModeS.decode(msg) returns every decodable field in one pass,
so most users never need lower-level primitives. The
pyModeS.util module exists for the exceptions — ad-hoc message
inspection, custom CRC gating in front of a pipeline, or tools
that need a raw bit string:
from pyModeS.util import hex2bin, bin2int, hex2int, bin2hex
from pyModeS.util import crc, df, icao, typecode, altcode, idcode, cprNL
msg = "8D406B902015A678D4D220AA4BDA"
hex2bin(msg)[:16] # '1000110101000000' — 4 bits per hex char
bin2int("10001101") # 141
df(msg) # 17
icao(msg) # '406B90'
typecode(msg) # 4 — ADS-B identification
crc(msg) # 0 — valid DF17 extended squitter
# For Comm-B replies the CRC recovers the ICAO address directly
icao("A000178D10010080F50000D5893C") # '8BDCDB'
# Altitude / squawk helpers pull from the AC/ID fields in
# DF0/4/16/20 and DF5/21 respectively
altcode("A000178D10010080F50000D5893C") # None — AC is zero
idcode("A8000D9FA55A032DBFFC000D8123") # '5667'
# CPR longitude-zone count, the same table the decoder uses
cprNL(52.0) # 36
cprNL(0.0) # 59These are thin wrappers around pyModeS._bits, _altcode,
_idcode, and position._cpr — changing the primitives auto-
flows through. Importing from pyModeS.common (the v2 location)
raises V2APIRemovedError with a pointer to this module.
For pandas / parquet workflows that need uniform column shapes:
result = pyModeS.decode("8D406B902015A678D4D220AA4BDA", full_dict=True)
# Result contains all ~123 schema keys; missing values default to NoneMalformed input raises an exception in single-message mode:
from pyModeS.errors import InvalidHexError
try:
pyModeS.decode("not hex")
except InvalidHexError:
passIn batch mode, the same input becomes an error-dict instead:
results = pyModeS.decode(
["not hex", "8D406B902015A678D4D220AA4BDA"],
timestamps=[0, 1],
)
assert "error" in results[0]
assert results[1]["icao"] == "406B90"pyModeS ships with a modes command-line tool, installed as a
console script when you run pip install pyModeS.
modes decode [--compact] [--full-dict] [--surface-ref REF]
(MESSAGE [--reference LAT LON] | --file PATH)
Three input shapes:
- Single message —
modes decode HEXprints a pretty-printed JSON object (or one-line compact JSON with--compact). - Inline batch —
modes decode HEX1,HEX2,HEX3comma-separated messages, sharing a transientPipeDecoderso CPR pairs resolve automatically. - File mode —
modes decode --file PATHreads from a file (one hex per line ortimestamp,hexCSV). Use-asPATHfor stdin.
Output format is pretty-printed JSON by default in all three
shapes — one indented {...} block per message, separated by a
blank line. Pass --compact to switch to one-line-per-message
output that composes with jq, pandas, parquet writers, etc.
Flags:
--compact— emit one-line JSON instead of pretty-printed. In batch shapes this yields one JSON line per message (JSONL).--full-dict— populate every key in the canonical schema--reference LAT LON— airborne CPR reference (only valid with a single positional MESSAGE — not with--fileor comma-batch, since one reference cannot apply to multiple aircraft)--surface-ref REF— surface CPR reference (airport ICAO code likeLFBO, or alat,lonstring)--file PATH— read from a file; use-for stdin
Examples:
# Single message, pretty
modes decode 8D406B902015A678D4D220AA4BDA
# Single message + airborne reference
modes decode 8D40058B58C901375147EFD09357 --reference 49.0 6.0
# Inline batch — comma-separated, one pretty JSON block per message
modes decode 8D40058B58C901375147EFD09357,8D40058B58C904A87F402D3B8C59,8D406B902015A678D4D220AA4BDA
# Inline batch compact output (JSONL) for piping to jq
modes decode 8D40058B58C901375147EFD09357,8D40058B58C904A87F402D3B8C59,8D406B902015A678D4D220AA4BDA --compact
# Single message, compact JSON for piping
modes decode 8D406B902015A678D4D220AA4BDA --compact | jq .
# File + surface reference (all aircraft at LFBO)
modes decode --file captures/lfbo.csv --surface-ref LFBO
# File from stdin
cat flight.log | modes decode --file -modes live --network HOST:PORT [--surface-ref REF]
[--full-dict]
[--dump-to FILE]
[--tui]
[--quiet]
Opens a TCP connection to a Mode-S Beast binary feed (dump1090's default port 30005, dump1090-fa, readsb, piaware, AirSquitter) and emits decoded JSON lines to stdout as they arrive. Legacy AVR raw text format is not supported in v3.
Flags:
--network HOST:PORT— required TCP endpoint--surface-ref REF— forwarded to the internalPipeDecoderfor surface CPR resolution--full-dict— emit every schema key per line--dump-to FILE— tee JSON lines to a file in addition to stdout (incompatible with--tui)--tui— interactive live aircraft table (requirespyModeS[tui]extra; incompatible with--dump-toand--quiet)--quiet— suppress stdout (use with--dump-to)
Examples:
# Basic streaming to stdout
modes live --network localhost:30005
# Public test feed over Europe (TU Delft)
modes live --network airsquitter.lr.tudelft.nl:10006
# Tee to a file for later analysis
modes live --network host:30005 --dump-to flight.jsonl
# Interactive TUI
pip install "pyModeS[tui]"
modes live --network host:30005 --tuiSignal handling: Ctrl-C (SIGINT) and SIGTERM trigger a clean shutdown and print a final stats line to stderr.
Reconnect: the network source automatically reconnects on dropped connections with exponential backoff (0.5 s → 10 s cap).