pyModeS 2.22.0__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymodes-2.22.0 → pymodes-3.1.0}/.gitignore +8 -1
- pymodes-3.1.0/CHANGELOG.md +182 -0
- pymodes-3.1.0/PKG-INFO +309 -0
- pymodes-3.1.0/README.md +282 -0
- pymodes-3.1.0/pyproject.toml +124 -0
- pymodes-3.1.0/src/pyModeS/__init__.py +74 -0
- pymodes-3.1.0/src/pyModeS/_aero.py +126 -0
- pymodes-3.1.0/src/pyModeS/_altcode.py +106 -0
- pymodes-3.1.0/src/pyModeS/_bits.py +149 -0
- pymodes-3.1.0/src/pyModeS/_callsign.py +62 -0
- pymodes-3.1.0/src/pyModeS/_idcode.py +42 -0
- pymodes-3.1.0/src/pyModeS/_pipe.py +758 -0
- pymodes-3.1.0/src/pyModeS/_schema.py +171 -0
- pymodes-2.22.0/src/pyModeS/decoder/uncertainty.py → pymodes-3.1.0/src/pyModeS/_uncertainty.py +23 -22
- pymodes-3.1.0/src/pyModeS/_v2_removed.py +251 -0
- pymodes-3.1.0/src/pyModeS/cli/__init__.py +51 -0
- pymodes-3.1.0/src/pyModeS/cli/__main__.py +8 -0
- pymodes-3.1.0/src/pyModeS/cli/_args.py +190 -0
- pymodes-3.1.0/src/pyModeS/cli/_sink.py +97 -0
- pymodes-3.1.0/src/pyModeS/cli/_source.py +387 -0
- pymodes-3.1.0/src/pyModeS/cli/_tui.py +739 -0
- pymodes-3.1.0/src/pyModeS/cli/decode.py +216 -0
- pymodes-3.1.0/src/pyModeS/cli/live.py +217 -0
- pymodes-3.1.0/src/pyModeS/core.py +203 -0
- pymodes-3.1.0/src/pyModeS/data/__init__.py +1 -0
- pymodes-3.1.0/src/pyModeS/data/airports.py +4915 -0
- pymodes-3.1.0/src/pyModeS/decoder/__init__.py +50 -0
- pymodes-3.1.0/src/pyModeS/decoder/_base.py +68 -0
- pymodes-3.1.0/src/pyModeS/decoder/acas.py +60 -0
- pymodes-3.1.0/src/pyModeS/decoder/adsb.py +80 -0
- pymodes-3.1.0/src/pyModeS/decoder/allcall.py +42 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/__init__.py +12 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/_helpers.py +86 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/_infer.py +217 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds05.py +70 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds06.py +71 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds08.py +67 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds09.py +148 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds10.py +80 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds17.py +72 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds20.py +44 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds30.py +113 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds40.py +96 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds44.py +98 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds45.py +104 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds50.py +123 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds60.py +111 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds61.py +55 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds62.py +101 -0
- pymodes-3.1.0/src/pyModeS/decoder/bds/bds65.py +85 -0
- pymodes-3.1.0/src/pyModeS/decoder/commb.py +88 -0
- pymodes-3.1.0/src/pyModeS/decoder/surv.py +58 -0
- pymodes-3.1.0/src/pyModeS/errors.py +36 -0
- pymodes-3.1.0/src/pyModeS/message.py +360 -0
- pymodes-3.1.0/src/pyModeS/position/__init__.py +19 -0
- pymodes-3.1.0/src/pyModeS/position/_airports.py +33 -0
- pymodes-3.1.0/src/pyModeS/position/_cpr.py +325 -0
- pymodes-3.1.0/src/pyModeS/util.py +191 -0
- pymodes-2.22.0/.coveragerc +0 -12
- pymodes-2.22.0/.github/workflows/build-publish.yml +0 -85
- pymodes-2.22.0/PKG-INFO +0 -389
- pymodes-2.22.0/README.rst +0 -366
- pymodes-2.22.0/doc/Makefile +0 -21
- pymodes-2.22.0/doc/README.rst +0 -7
- pymodes-2.22.0/doc/modeslive-screenshot.png +0 -0
- pymodes-2.22.0/doc/source/_templates/layout.html +0 -17
- pymodes-2.22.0/doc/source/conf.py +0 -190
- pymodes-2.22.0/doc/source/index.rst +0 -68
- pymodes-2.22.0/doc/source/modules.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.c_common.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.common.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.acas.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.adsb.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.allcall.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds05.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds06.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds08.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds09.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds10.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds17.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds20.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds30.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds40.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds44.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds45.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds50.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds53.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds60.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.bds.rst +0 -28
- pymodes-2.22.0/doc/source/pyModeS.decoder.commb.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.rst +0 -29
- pymodes-2.22.0/doc/source/pyModeS.decoder.surv.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.uncertainty.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.decoder.uplink.rst +0 -7
- pymodes-2.22.0/doc/source/pyModeS.rst +0 -24
- pymodes-2.22.0/doc/warnings +0 -0
- pymodes-2.22.0/hatch_build.py +0 -81
- pymodes-2.22.0/pyproject.toml +0 -62
- pymodes-2.22.0/src/pyModeS/.gitignore +0 -3
- pymodes-2.22.0/src/pyModeS/__init__.py +0 -29
- pymodes-2.22.0/src/pyModeS/c_common.pxd +0 -28
- pymodes-2.22.0/src/pyModeS/c_common.pyi +0 -22
- pymodes-2.22.0/src/pyModeS/c_common.pyx +0 -402
- pymodes-2.22.0/src/pyModeS/common.pyi +0 -22
- pymodes-2.22.0/src/pyModeS/decoder/__init__.py +0 -234
- pymodes-2.22.0/src/pyModeS/decoder/acas.py +0 -5
- pymodes-2.22.0/src/pyModeS/decoder/adsb.py +0 -670
- pymodes-2.22.0/src/pyModeS/decoder/allcall.py +0 -94
- pymodes-2.22.0/src/pyModeS/decoder/bds/__init__.py +0 -193
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds05.py +0 -154
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds06.py +0 -210
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds08.py +0 -60
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds09.py +0 -150
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds10.py +0 -53
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds17.py +0 -83
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds20.py +0 -60
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds30.py +0 -35
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds40.py +0 -124
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds44.py +0 -164
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds45.py +0 -208
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds50.py +0 -173
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds53.py +0 -169
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds60.py +0 -181
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds61.py +0 -83
- pymodes-2.22.0/src/pyModeS/decoder/bds/bds62.py +0 -551
- pymodes-2.22.0/src/pyModeS/decoder/commb.py +0 -88
- pymodes-2.22.0/src/pyModeS/decoder/ehs.py +0 -66
- pymodes-2.22.0/src/pyModeS/decoder/els.py +0 -35
- pymodes-2.22.0/src/pyModeS/decoder/flarm/__init__.py +0 -27
- pymodes-2.22.0/src/pyModeS/decoder/flarm/core.c +0 -124
- pymodes-2.22.0/src/pyModeS/decoder/flarm/core.h +0 -13
- pymodes-2.22.0/src/pyModeS/decoder/flarm/core.pxd +0 -4
- pymodes-2.22.0/src/pyModeS/decoder/flarm/decode.pyi +0 -13
- pymodes-2.22.0/src/pyModeS/decoder/flarm/decode.pyx +0 -147
- pymodes-2.22.0/src/pyModeS/decoder/surv.py +0 -139
- pymodes-2.22.0/src/pyModeS/decoder/uplink.py +0 -227
- pymodes-2.22.0/src/pyModeS/extra/__init__.py +0 -0
- pymodes-2.22.0/src/pyModeS/extra/aero.py +0 -185
- pymodes-2.22.0/src/pyModeS/extra/rtlreader.py +0 -207
- pymodes-2.22.0/src/pyModeS/extra/tcpclient.py +0 -398
- pymodes-2.22.0/src/pyModeS/py_common.py +0 -490
- pymodes-2.22.0/src/pyModeS/streamer/__init__.py +0 -0
- pymodes-2.22.0/src/pyModeS/streamer/decode.py +0 -320
- pymodes-2.22.0/src/pyModeS/streamer/modeslive.py +0 -164
- pymodes-2.22.0/src/pyModeS/streamer/screen.py +0 -234
- pymodes-2.22.0/src/pyModeS/streamer/source.py +0 -91
- pymodes-2.22.0/tests/benchmark.py +0 -132
- pymodes-2.22.0/tests/data/sample_data_adsb.csv +0 -2000
- pymodes-2.22.0/tests/data/sample_data_commb_df20.csv +0 -5000
- pymodes-2.22.0/tests/data/sample_data_commb_df21.csv +0 -5000
- pymodes-2.22.0/tests/sample_run_adsb.py +0 -41
- pymodes-2.22.0/tests/sample_run_commb.py +0 -91
- pymodes-2.22.0/tests/test_adsb.py +0 -266
- pymodes-2.22.0/tests/test_allcall.py +0 -13
- pymodes-2.22.0/tests/test_bds_inference.py +0 -67
- pymodes-2.22.0/tests/test_c_common.py +0 -106
- pymodes-2.22.0/tests/test_commb.py +0 -47
- pymodes-2.22.0/tests/test_common_contract.py +0 -84
- pymodes-2.22.0/tests/test_full.py +0 -17
- pymodes-2.22.0/tests/test_py_common.py +0 -148
- pymodes-2.22.0/tests/test_surv.py +0 -22
- pymodes-2.22.0/tests/test_tell.py +0 -20
- pymodes-2.22.0/uv.lock +0 -752
- {pymodes-2.22.0 → pymodes-3.1.0}/LICENSE +0 -0
- {pymodes-2.22.0 → pymodes-3.1.0}/src/pyModeS/py.typed +0 -0
|
@@ -6,8 +6,12 @@ tmp/
|
|
|
6
6
|
__pycache__/
|
|
7
7
|
*.py[cod]
|
|
8
8
|
.pytest_cache/
|
|
9
|
+
.ipynb_checkpoints/
|
|
10
|
+
.mypy_cache/
|
|
9
11
|
|
|
10
|
-
#
|
|
12
|
+
# Compiled extensions (none shipped in v3 — pure Python — but
|
|
13
|
+
# left in place to keep `.so` build artifacts out of accidental
|
|
14
|
+
# commits if a user experiments locally).
|
|
11
15
|
*.so
|
|
12
16
|
|
|
13
17
|
# Distribution / packaging
|
|
@@ -55,6 +59,9 @@ coverage.xml
|
|
|
55
59
|
# Sphinx documentation
|
|
56
60
|
docs/_build/
|
|
57
61
|
|
|
62
|
+
# MkDocs build output
|
|
63
|
+
site/
|
|
64
|
+
|
|
58
65
|
# Local planning / brainstorming artifacts (not for publication)
|
|
59
66
|
docs/superpowers/
|
|
60
67
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to pyModeS are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [3.1.0] — 2026-04-14
|
|
9
|
+
|
|
10
|
+
Streaming-robustness release. Adds defences against phantom positions
|
|
11
|
+
at stream start and against DF20 CRC-collisions that attribute a
|
|
12
|
+
Comm-B reply to the wrong aircraft.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `PipeDecoder` bootstrap cluster analysis for the first position per
|
|
17
|
+
ICAO: lat/lon emission is held until `_BOOTSTRAP_K` (=5) candidate
|
|
18
|
+
positions agree under a motion-consistency check. Prevents an
|
|
19
|
+
initial phantom frame from anchoring the rolling position history.
|
|
20
|
+
On each decode the held result dicts are retro-filled once the
|
|
21
|
+
cluster locks. Scattered buffers reset and accumulate a fresh K.
|
|
22
|
+
- `PipeDecoder` altitude cross-check on DF20 Comm-B replies: if the
|
|
23
|
+
message's 13-bit AC-code disagrees with the ICAO's most recent
|
|
24
|
+
ADS-B-derived altitude by more than `_altitude_tolerance(dt)` (a
|
|
25
|
+
linearly growing, floored-and-capped window), the BDS payload
|
|
26
|
+
fields are stripped from the result and `altitude_mismatch=True`
|
|
27
|
+
is set. The AC-code altitude itself is preserved so callers see
|
|
28
|
+
why the frame was flagged.
|
|
29
|
+
- `PipeDecoder.flush()` method that finalizes any still-bootstrapping
|
|
30
|
+
ICAOs, running cluster analysis on whatever candidates are
|
|
31
|
+
buffered (even if fewer than K) and retro-filling the held result
|
|
32
|
+
dicts in place. Automatically called by the batch `decode(list,
|
|
33
|
+
timestamps=list)` code path so batch callers get positions on
|
|
34
|
+
every resolvable frame without having to reach into `PipeDecoder`.
|
|
35
|
+
- Four new per-stream stat counters exposed via `PipeDecoder.stats`:
|
|
36
|
+
`altitude_mismatch`, `position_rejected`, `bootstrap_held`,
|
|
37
|
+
`bootstrap_reset`.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- `airborne_position_pair` now rejects CPR pairs that resolve to
|
|
42
|
+
physically impossible latitudes (|lat| > 90). Root cause: real
|
|
43
|
+
DF17 pairs that straddle a `cprNL` zone can slip past the
|
|
44
|
+
pre-pair `NL(lat_even) == NL(lat_odd)` check and produce
|
|
45
|
+
latitudes in [90, 270) — e.g. an aircraft climbing out of
|
|
46
|
+
Amsterdam resolving to lat ≈ 113° over the Bering Sea.
|
|
47
|
+
Observed on ICAO 485A33 (2025-04-13 19:34:57 UTC, OpenSky feed)
|
|
48
|
+
— now rejected at the pair resolver rather than poisoning the
|
|
49
|
+
receiver's position history.
|
|
50
|
+
|
|
51
|
+
## [3.0.0] — 2026-04-13
|
|
52
|
+
|
|
53
|
+
First release of the v3 ground-up rewrite. **Not backwards-compatible
|
|
54
|
+
with pyModeS 2.x.** See [docs/migration.md](./docs/migration.md) for
|
|
55
|
+
the migration guide.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- Unified `pyModeS.decode(msg)` returning a `Decoded` dict with every
|
|
60
|
+
decodable field populated in one call
|
|
61
|
+
- Batch mode: `pyModeS.decode(list_of_msgs, timestamps=...)` preserves
|
|
62
|
+
list length (errors become error-dicts, no exceptions)
|
|
63
|
+
- `PipeDecoder` stateful streaming decoder — per-ICAO state, CPR
|
|
64
|
+
pair matching, TTL eviction, DF20/21 `icao_verified` promotion
|
|
65
|
+
via a trusted-ICAO set populated from clean DF17/18 plain-text
|
|
66
|
+
addresses
|
|
67
|
+
- `full_dict=True` mode populates every key in the canonical
|
|
68
|
+
`_FULL_SCHEMA` (123 fields) for pandas / parquet workflows
|
|
69
|
+
- Phase 3 BDS 5,0/6,0 disambiguation via a `known=` aircraft-state
|
|
70
|
+
kwarg — scores candidate registers against recent observations
|
|
71
|
+
of groundspeed / track / heading / mach
|
|
72
|
+
- Airport database (ICAO 4-letter codes only) for surface CPR
|
|
73
|
+
resolution: `surface_ref="EHAM"` or `surface_ref=(lat, lon)`
|
|
74
|
+
- Golden-file oracle test asserting v3 decode output matches
|
|
75
|
+
`pyModeS 2.21.1` on the deduplicated `tests/data/` corpus
|
|
76
|
+
- MkDocs + Material documentation site (docs added in a later
|
|
77
|
+
commit in this release)
|
|
78
|
+
- `modes` command-line tool with two subcommands:
|
|
79
|
+
- `modes decode MESSAGE` for one-shot decoding (pretty or compact JSON)
|
|
80
|
+
- `modes decode HEX1,HEX2,HEX3` inline-batch mode with shared
|
|
81
|
+
`PipeDecoder` state so CPR pairs resolve across the batch
|
|
82
|
+
- `modes decode --file PATH` for batch decoding a file of hex messages
|
|
83
|
+
- `modes live --network HOST:PORT` for streaming TCP decode from a
|
|
84
|
+
Mode-S Beast binary feed
|
|
85
|
+
- `modes live --dump-to FILE` to tee JSON lines to a file. Every
|
|
86
|
+
record carries both `raw_msg` (source hex) and `timestamp` so
|
|
87
|
+
captures are self-contained for offline re-decode
|
|
88
|
+
- `modes live --tui` for an interactive textual-based aircraft
|
|
89
|
+
table: keyboard navigation (j/k/g/G), live incremental search
|
|
90
|
+
(/) that filters as you type, sort cycling (s/r), responsive
|
|
91
|
+
column set (7/10/18 cols by terminal width). Requires
|
|
92
|
+
`pyModeS[tui]` extra
|
|
93
|
+
- Graceful SIGINT/SIGTERM shutdown with final stats line
|
|
94
|
+
- `pyModeS[tui]` optional dependency: pulls in `textual>=0.50` for
|
|
95
|
+
the `modes live --tui` interactive display
|
|
96
|
+
- `pyModeS.util` public helpers for low-level message inspection
|
|
97
|
+
without going through `decode()`: `hex2bin`, `bin2int`, `hex2int`,
|
|
98
|
+
`bin2hex`, `crc`, `df`, `icao`, `typecode`, `altcode`, `idcode`,
|
|
99
|
+
`cprNL`. Thin wrappers over `_bits.py`, `_altcode.py`, `_idcode.py`
|
|
100
|
+
and `position/_cpr.py` with no logic duplication.
|
|
101
|
+
- `scripts/smoke_test.sh` installs the freshly built wheel
|
|
102
|
+
into a clean Python 3.12 venv and verifies the public API end to
|
|
103
|
+
end (single-message decode, PipeDecoder baseline, batch CPR pair
|
|
104
|
+
resolution, error-dict on malformed input).
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- BDS 5,0 / 6,0 Phase 3 disambiguation now works cold-start
|
|
109
|
+
(before any ADS-B frames have been cached). Root cause: the
|
|
110
|
+
BDS 5,0 validator accepted roll angles up to ±50°, the full
|
|
111
|
+
wire-format range, which let garbage payloads pass as track-
|
|
112
|
+
and-turn reports with physically implausible banks (observed
|
|
113
|
+
-44.5° on the TU Delft live feed). Tightened the gate to ±35°
|
|
114
|
+
to match the commercial-airliner envelope (30° bank ≈ 2 G in
|
|
115
|
+
a level turn).
|
|
116
|
+
- BDS 6,0 scorer now has reference fields to compare against in
|
|
117
|
+
streaming mode. PipeDecoder derives approximate IAS and mach
|
|
118
|
+
from cached groundspeed + altitude using a minimal ISA
|
|
119
|
+
calculator in `pyModeS._aero` (zero-wind assumption, low-Mach
|
|
120
|
+
approximation). Previously the scorer's `ias`/`mach` slots
|
|
121
|
+
were always empty because ADS-B airborne-velocity BDS 0,9
|
|
122
|
+
subtypes 1/2 only emit groundspeed; the scorer then returned
|
|
123
|
+
+inf for every BDS 6,0 candidate and BDS 5,0 won every tie by
|
|
124
|
+
default.
|
|
125
|
+
- `modes live --tui` no longer freezes after the "detected beast
|
|
126
|
+
format, resyncing" line. Two root causes: the sink context
|
|
127
|
+
manager wrap was lost during a prior refactor, and the
|
|
128
|
+
on-detect stderr print corrupted rich's alt-screen buffer.
|
|
129
|
+
Restored the context manager and silenced stderr writes when
|
|
130
|
+
`--tui` is active.
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
|
|
134
|
+
- Python 3.11+ minimum (was 3.9+)
|
|
135
|
+
- Internal message representation: Python int + bit-shift primitives
|
|
136
|
+
(replaces numpy arrays and the Cython `c_common` extension)
|
|
137
|
+
- Every decoder API → single `decode()` returning a dict
|
|
138
|
+
- CPR resolution API: `reference=(lat, lon)` for airborne (180 NM
|
|
139
|
+
tolerance), `surface_ref="EHAM"` or `surface_ref=(lat, lon)` for
|
|
140
|
+
surface (45 NM tolerance)
|
|
141
|
+
- Header fields (df, icao, crc, crc_valid) are computed eagerly in
|
|
142
|
+
`Message.__init__` as plain attributes instead of lazy
|
|
143
|
+
`cached_property` descriptors (measurably faster on streaming
|
|
144
|
+
decode loads, and the decode path always needs all four)
|
|
145
|
+
- `modes live` stream timestamps are now derived from the Beast
|
|
146
|
+
48-bit MLAT counter, with tick rate auto-calibrated per
|
|
147
|
+
connection from the delta between consecutive `recv()` bursts.
|
|
148
|
+
Works on both dump1090 12 MHz counters and radarcape / GNS
|
|
149
|
+
AirSquitter 1 GHz nanosecond counters without configuration.
|
|
150
|
+
Gives sub-microsecond per-frame precision across a TCP burst,
|
|
151
|
+
replacing the previous coarse one-`time.time()`-per-batch
|
|
152
|
+
behaviour.
|
|
153
|
+
- `modes live --tui` rewritten from `rich.live.Live` to a full
|
|
154
|
+
`textual.App` with a `DataTable` widget, interactive row
|
|
155
|
+
cursor, and per-tick diff-update of cells (only cells whose
|
|
156
|
+
values actually change are redrawn). The result: no more
|
|
157
|
+
whole-table flashing on each message, cursor and scroll
|
|
158
|
+
position survive refreshes, and the default sort is now by
|
|
159
|
+
ICAO address (stable) instead of last-seen (reshuffles on
|
|
160
|
+
every message). Dropped the `rich>=13.0` dependency in favour
|
|
161
|
+
of `textual>=0.50`.
|
|
162
|
+
|
|
163
|
+
### Removed
|
|
164
|
+
|
|
165
|
+
- Cython extension (`c_common`) and its build dependency
|
|
166
|
+
- Legacy function-per-field API (`pms.adsb.callsign`, `pms.commb.bds`,
|
|
167
|
+
`pms.common.hex2bin`, etc.). The former submodule paths
|
|
168
|
+
(`pyModeS.adsb`, `pyModeS.commb`, `pyModeS.ehs`, `pyModeS.els`,
|
|
169
|
+
`pyModeS.common`, `pyModeS.bds`, `pyModeS.streamer`,
|
|
170
|
+
`pyModeS.extra`) are intercepted by a meta-path finder in
|
|
171
|
+
`pyModeS._v2_removed` which raises `V2APIRemovedError` (an
|
|
172
|
+
`ImportError` subclass) with a migration hint pointing at
|
|
173
|
+
`pyModeS.decode()` and — for `pyModeS.common` specifically —
|
|
174
|
+
at the restored helpers in `pyModeS.util`. Both `from pyModeS.X
|
|
175
|
+
import Y` and bare attribute access (`pms.X`) hit the same
|
|
176
|
+
error message.
|
|
177
|
+
- numpy hard dependency.
|
|
178
|
+
- `modeslive` CLI entry point: replaced by `modes live` / `modes
|
|
179
|
+
decode`. The v3 package still registers `modeslive` as a console
|
|
180
|
+
script — running it prints a migration hint to stderr and exits
|
|
181
|
+
with code 2, rather than vanishing into a bare `command not
|
|
182
|
+
found` after a `pip install -U pyModeS`.
|
pymodes-3.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyModeS
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: Python Mode-S and ADS-B Decoder
|
|
5
|
+
Project-URL: homepage, https://mode-s.org
|
|
6
|
+
Project-URL: repository, https://github.com/junzis/pyModeS
|
|
7
|
+
Project-URL: issues, https://github.com/junzis/pyModeS/issues
|
|
8
|
+
Author-email: Junzi Sun <git@junzis.com>
|
|
9
|
+
License: GNU GPL v3
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Provides-Extra: tui
|
|
25
|
+
Requires-Dist: textual>=0.50; extra == 'tui'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# pyModeS
|
|
29
|
+
|
|
30
|
+
Fast decoder for Mode-S and ADS-B messages in Python. Ground-up v3 rewrite of `pyModeS` with a single unified `decode()` function.
|
|
31
|
+
|
|
32
|
+
[](https://github.com/junzis/pyModeS/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install "pyModeS>=3"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Python 3.11+ required.
|
|
41
|
+
|
|
42
|
+
## Usage examples
|
|
43
|
+
|
|
44
|
+
### Single-message decode
|
|
45
|
+
|
|
46
|
+
The `decode()` function returns a `Decoded` dict with every decodable
|
|
47
|
+
field populated in one pass.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pyModeS import decode
|
|
51
|
+
|
|
52
|
+
result = decode("8D406B902015A678D4D220AA4BDA")
|
|
53
|
+
print(result)
|
|
54
|
+
# {
|
|
55
|
+
# 'df': 17,
|
|
56
|
+
# 'icao': '406B90',
|
|
57
|
+
# 'crc_valid': True,
|
|
58
|
+
# 'typecode': 4,
|
|
59
|
+
# 'bds': '0,8',
|
|
60
|
+
# 'callsign': 'EZY85MH',
|
|
61
|
+
# 'category': 0,
|
|
62
|
+
# 'wake_vortex': 'No category information',
|
|
63
|
+
# }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Batch decode (mixed message types)
|
|
67
|
+
|
|
68
|
+
Pass a list of hex strings and parallel timestamps. Any mix of
|
|
69
|
+
downlink formats, typecodes, and Comm-B registers is fine —
|
|
70
|
+
the dispatcher routes each message to the right decoder and
|
|
71
|
+
uses the timestamps to resolve CPR pairs and disambiguate
|
|
72
|
+
ambiguous Comm-B registers.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from pyModeS import decode
|
|
76
|
+
|
|
77
|
+
results = decode(
|
|
78
|
+
[
|
|
79
|
+
"8D406B902015A678D4D220AA4BDA", # DF17 BDS 0,8 identification
|
|
80
|
+
"8D485020994409940838175B284F", # DF17 BDS 0,9 airborne velocity
|
|
81
|
+
"8D40058B58C901375147EFD09357", # DF17 BDS 0,5 airborne pos (even)
|
|
82
|
+
"8D40058B58C904A87F402D3B8C59", # DF17 BDS 0,5 airborne pos (odd)
|
|
83
|
+
"A000178D10010080F50000D5893C", # DF20 BDS 1,0 data link capability
|
|
84
|
+
"A8000D9FA55A032DBFFC000D8123", # DF21 BDS 6,0 heading & speed
|
|
85
|
+
],
|
|
86
|
+
timestamps=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
|
87
|
+
)
|
|
88
|
+
assert results[0]["callsign"] == "EZY85MH"
|
|
89
|
+
assert results[1]["groundspeed"] == 159
|
|
90
|
+
assert results[3]["latitude"] is not None # CPR pair resolved
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Surface position with airport reference
|
|
94
|
+
|
|
95
|
+
Surface CPR needs a reference within ~45 NM. Pass an ICAO airport
|
|
96
|
+
code (looked up in the shipped airport database) or an explicit
|
|
97
|
+
`(lat, lon)` tuple.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from pyModeS import decode
|
|
101
|
+
|
|
102
|
+
# Real DF18 surface movement on LFBO (Toulouse-Blagnac).
|
|
103
|
+
r = decode("903a23ff426a4e65f7487a775d17", surface_ref="LFBO")
|
|
104
|
+
print(r["latitude"], r["longitude"]) # 43.6264..., 1.3747...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Streaming decoder
|
|
108
|
+
|
|
109
|
+
`PipeDecoder` is stateful — it holds per-ICAO state across calls,
|
|
110
|
+
matches CPR pairs automatically, evicts stale aircraft after a
|
|
111
|
+
TTL, and flags DF20/21 messages as `icao_verified` when their
|
|
112
|
+
CRC-derived ICAO was already seen in a clean DF17/18 plaintext.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from pyModeS import PipeDecoder
|
|
116
|
+
|
|
117
|
+
pipe = PipeDecoder(surface_ref="EHAM")
|
|
118
|
+
for msg, timestamp in stream:
|
|
119
|
+
decoded = pipe.decode(msg, timestamp=timestamp)
|
|
120
|
+
if "latitude" in decoded:
|
|
121
|
+
print(decoded["icao"], decoded["latitude"], decoded["longitude"])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
See [`docs/quickstart.md`](./docs/quickstart.md) for the full tour
|
|
125
|
+
(full-dict mode, error handling, attribute access).
|
|
126
|
+
|
|
127
|
+
### Low-level helpers
|
|
128
|
+
|
|
129
|
+
For ad-hoc message inspection that doesn't need a full decode,
|
|
130
|
+
`pyModeS.util` exposes thin wrappers around the bit/hex/CRC
|
|
131
|
+
primitives: `hex2bin`, `bin2int`, `hex2int`, `bin2hex`, `crc`,
|
|
132
|
+
`df`, `icao`, `typecode`, `altcode`, `idcode`, `cprNL`.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pyModeS.util import hex2bin, crc, icao, typecode
|
|
136
|
+
|
|
137
|
+
msg = "8D406B902015A678D4D220AA4BDA"
|
|
138
|
+
hex2bin(msg)[:16] # '1000110101000000'
|
|
139
|
+
crc(msg) # 0 — valid DF17
|
|
140
|
+
icao(msg) # '406B90'
|
|
141
|
+
typecode(msg) # 4 — ADS-B identification
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CLI
|
|
145
|
+
|
|
146
|
+
pyModeS ships with a `modes` command-line tool for ad-hoc decoding
|
|
147
|
+
and live streaming.
|
|
148
|
+
|
|
149
|
+
### `modes decode` — one-shot and file mode
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
# Decode one hex message (pretty-printed JSON)
|
|
153
|
+
modes decode 8D406B902015A678D4D220AA4BDA
|
|
154
|
+
|
|
155
|
+
# Decode several messages inline — comma-separated, emits JSON lines
|
|
156
|
+
modes decode 8D40058B58C901375147EFD09357,8D40058B58C904A87F402D3B8C59,8D406B902015A678D4D220AA4BDA
|
|
157
|
+
|
|
158
|
+
# With airborne CPR reference (single message only)
|
|
159
|
+
modes decode 8D40058B58C901375147EFD09357 --reference 49.0 6.0
|
|
160
|
+
|
|
161
|
+
# Compact JSON piped to jq
|
|
162
|
+
modes decode 8D406B902015A678D4D220AA4BDA --compact | jq .
|
|
163
|
+
|
|
164
|
+
# Decode a file of hex messages (one per line OR timestamp,hex CSV)
|
|
165
|
+
modes decode --file captures/flight.log
|
|
166
|
+
|
|
167
|
+
# Stdin + surface CPR
|
|
168
|
+
cat taxi.log | modes decode --file - --surface-ref LFBO
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `modes live` — streaming TCP source
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
# Stream decoded JSON lines from a dump1090-style beast feed
|
|
175
|
+
modes live --network localhost:30005
|
|
176
|
+
|
|
177
|
+
# Tee output to a file
|
|
178
|
+
modes live --network host:30005 --dump-to flight.jsonl
|
|
179
|
+
|
|
180
|
+
# Stream from the TU Delft public feed (live aircraft over Europe)
|
|
181
|
+
modes live --network airsquitter.lr.tudelft.nl:10006
|
|
182
|
+
|
|
183
|
+
# Interactive live aircraft table (requires pyModeS[tui] extra)
|
|
184
|
+
pip install "pyModeS[tui]"
|
|
185
|
+
modes live --network host:30005 --tui
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Mode-S Beast binary format is supported (dump1090 port 30005 and
|
|
189
|
+
equivalents). See [`docs/quickstart.md`](./docs/quickstart.md) for
|
|
190
|
+
the full command reference.
|
|
191
|
+
|
|
192
|
+
## Features
|
|
193
|
+
|
|
194
|
+
- Unified `decode()` returns every decodable field in one dict
|
|
195
|
+
- Batch mode preserves list length (errors become error-dicts, not
|
|
196
|
+
exceptions)
|
|
197
|
+
- `PipeDecoder` for streams: per-ICAO state, CPR pair accumulation,
|
|
198
|
+
TTL eviction, DF20/21 ICAO verification via trusted-set promotion
|
|
199
|
+
- `full_dict=True` populates every key in the canonical 123-field
|
|
200
|
+
schema for pandas / parquet workflows
|
|
201
|
+
- `known=` aircraft state disambiguates Comm-B BDS 5,0/6,0 ambiguity
|
|
202
|
+
- Airport ICAO database for surface CPR resolution (`surface_ref="EHAM"`)
|
|
203
|
+
- Type-checked under mypy strict across all source files
|
|
204
|
+
- Golden-file oracle regression test against `pyModeS 2.21.1`
|
|
205
|
+
|
|
206
|
+
## Performance
|
|
207
|
+
|
|
208
|
+
Measured on jet1090's
|
|
209
|
+
[`long_flight.csv`](https://github.com/xoolive/jet1090/blob/master/crates/rs1090/data/long_flight.csv)
|
|
210
|
+
(172,432 Beast-format messages, 7 runs × 1 loop, mean timings,
|
|
211
|
+
single-core only):
|
|
212
|
+
|
|
213
|
+
| Decoder | Wall time | Throughput | vs pyModeS v3 |
|
|
214
|
+
|---|---|---|---|
|
|
215
|
+
| **pyModeS v3 (pure Python)** | **2.06s ± 0.01** | **83,549 msg/s** | **1.00×** |
|
|
216
|
+
| pyModeS 2.21.1 (Python with compiled C) | 5.03s ± 0.01 | 34,303 msg/s | 0.41× |
|
|
217
|
+
| rs1090 (Rust) | 5.60s ± 0.01 | 30,798 msg/s | 0.37× |
|
|
218
|
+
| pyModeS 2.21.1 (Python) | 9.09s ± 0.02 | 18,959 msg/s | 0.23× |
|
|
219
|
+
|
|
220
|
+
pyModeS v3 is **2.44× faster** than `pyModeS 2.21.1`'s compiled C
|
|
221
|
+
extension, **4.41× faster** than `pyModeS 2.21.1` pure-Python, and
|
|
222
|
+
**2.71× faster** than rs1090's single-core Rust — all while remaining
|
|
223
|
+
pure Python with no C/Cython build.
|
|
224
|
+
|
|
225
|
+
Reproduce with `scripts/benchmark_decode.py`.
|
|
226
|
+
|
|
227
|
+
## Supported messages
|
|
228
|
+
|
|
229
|
+
- **DF4 / DF20:** altitude code (surveillance altitude reply)
|
|
230
|
+
- **DF5 / DF21:** identity code (squawk)
|
|
231
|
+
- **DF11:** all-call reply (partial — II/SI decoding deferred)
|
|
232
|
+
- **DF17 / DF18 ADS-B:**
|
|
233
|
+
- TC 1-4 (BDS 0,8): identification + category
|
|
234
|
+
- TC 5-8 (BDS 0,6): surface position
|
|
235
|
+
- TC 9-18 (BDS 0,5): airborne position (barometric altitude)
|
|
236
|
+
- TC 19 (BDS 0,9): airborne velocity (all 4 subtypes)
|
|
237
|
+
- TC 20-22 (BDS 0,5): airborne position (GNSS altitude)
|
|
238
|
+
- TC 28 (BDS 6,1): aircraft status
|
|
239
|
+
- TC 29 (BDS 6,2): target state and status
|
|
240
|
+
- TC 31 (BDS 6,5): operational status
|
|
241
|
+
- **DF20 / DF21 Comm-B:**
|
|
242
|
+
- BDS 1,0: data link capability
|
|
243
|
+
- BDS 1,7: common-usage GICB capability
|
|
244
|
+
- BDS 2,0: aircraft identification
|
|
245
|
+
- BDS 3,0: ACAS active resolution advisory
|
|
246
|
+
- BDS 4,0: selected vertical intention
|
|
247
|
+
- BDS 4,4: meteorological routine air report
|
|
248
|
+
- BDS 4,5: meteorological hazard report
|
|
249
|
+
- BDS 5,0: track and turn report
|
|
250
|
+
- BDS 6,0: heading and speed report
|
|
251
|
+
|
|
252
|
+
## Migrating from pyModeS 2.x
|
|
253
|
+
|
|
254
|
+
pyModeS 3 is **not backwards-compatible** with pyModeS 2.x. The
|
|
255
|
+
function-per-field API (`pms.adsb.callsign(msg)`, ...) is replaced
|
|
256
|
+
by a single `decode()` returning a dict. See the [migration
|
|
257
|
+
guide](https://github.com/junzis/pyModeS/blob/main/docs/migration.md) for the full equivalence table.
|
|
258
|
+
|
|
259
|
+
If you aren't ready to migrate:
|
|
260
|
+
|
|
261
|
+
```sh
|
|
262
|
+
pip install "pyModeS<3"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
v3 replaces v2 on the same PyPI slot and import name, so
|
|
266
|
+
`pip install -U pyModeS` upgrades existing v2 users to v3
|
|
267
|
+
(and triggers the v2-API removal shims — see the migration
|
|
268
|
+
guide). Pinning `<3` keeps you on the 2.x line.
|
|
269
|
+
|
|
270
|
+
## Documentation
|
|
271
|
+
|
|
272
|
+
The full docs live under `docs/` and are published via MkDocs + Material.
|
|
273
|
+
To rebuild locally:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
# One-shot build (strict mode fails on warnings). Output → site/
|
|
277
|
+
uv run --with mkdocs-material --with mkdocs-include-markdown-plugin --with "mkdocstrings[python]" mkdocs build --clean --strict
|
|
278
|
+
|
|
279
|
+
# Live-reload dev server at http://127.0.0.1:8000
|
|
280
|
+
uv run --with mkdocs-material --with mkdocs-include-markdown-plugin --with "mkdocstrings[python]" mkdocs serve
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Links
|
|
284
|
+
|
|
285
|
+
- Source: https://github.com/junzis/pyModeS
|
|
286
|
+
- Changelog: [CHANGELOG.md](https://github.com/junzis/pyModeS/blob/main/CHANGELOG.md)
|
|
287
|
+
- Issues: https://github.com/junzis/pyModeS/issues
|
|
288
|
+
- License: GPL-3.0 (see `LICENSE`)
|
|
289
|
+
|
|
290
|
+
## Attribution
|
|
291
|
+
|
|
292
|
+
pyModeS is a project created by Junzi Sun, who works at TU Delft,
|
|
293
|
+
[Aerospace Engineering Faculty](https://tudelft.nl/en/ae/). It is
|
|
294
|
+
supported by many
|
|
295
|
+
[contributors](https://github.com/junzis/pyModeS/graphs/contributors)
|
|
296
|
+
from different institutions.
|
|
297
|
+
|
|
298
|
+
If you use pyModeS in academic work, please cite:
|
|
299
|
+
|
|
300
|
+
```bibtex
|
|
301
|
+
@article{sun2019pyModeS,
|
|
302
|
+
author={J. {Sun} and H. {V\^u} and J. {Ellerbroek} and J. M. {Hoekstra}},
|
|
303
|
+
journal={IEEE Transactions on Intelligent Transportation Systems},
|
|
304
|
+
title={pyModeS: Decoding Mode-S Surveillance Data for Open Air Transportation Research},
|
|
305
|
+
year={2019},
|
|
306
|
+
doi={10.1109/TITS.2019.2914770},
|
|
307
|
+
ISSN={1524-9050},
|
|
308
|
+
}
|
|
309
|
+
```
|