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.
Files changed (165) hide show
  1. {pymodes-2.22.0 → pymodes-3.1.0}/.gitignore +8 -1
  2. pymodes-3.1.0/CHANGELOG.md +182 -0
  3. pymodes-3.1.0/PKG-INFO +309 -0
  4. pymodes-3.1.0/README.md +282 -0
  5. pymodes-3.1.0/pyproject.toml +124 -0
  6. pymodes-3.1.0/src/pyModeS/__init__.py +74 -0
  7. pymodes-3.1.0/src/pyModeS/_aero.py +126 -0
  8. pymodes-3.1.0/src/pyModeS/_altcode.py +106 -0
  9. pymodes-3.1.0/src/pyModeS/_bits.py +149 -0
  10. pymodes-3.1.0/src/pyModeS/_callsign.py +62 -0
  11. pymodes-3.1.0/src/pyModeS/_idcode.py +42 -0
  12. pymodes-3.1.0/src/pyModeS/_pipe.py +758 -0
  13. pymodes-3.1.0/src/pyModeS/_schema.py +171 -0
  14. pymodes-2.22.0/src/pyModeS/decoder/uncertainty.py → pymodes-3.1.0/src/pyModeS/_uncertainty.py +23 -22
  15. pymodes-3.1.0/src/pyModeS/_v2_removed.py +251 -0
  16. pymodes-3.1.0/src/pyModeS/cli/__init__.py +51 -0
  17. pymodes-3.1.0/src/pyModeS/cli/__main__.py +8 -0
  18. pymodes-3.1.0/src/pyModeS/cli/_args.py +190 -0
  19. pymodes-3.1.0/src/pyModeS/cli/_sink.py +97 -0
  20. pymodes-3.1.0/src/pyModeS/cli/_source.py +387 -0
  21. pymodes-3.1.0/src/pyModeS/cli/_tui.py +739 -0
  22. pymodes-3.1.0/src/pyModeS/cli/decode.py +216 -0
  23. pymodes-3.1.0/src/pyModeS/cli/live.py +217 -0
  24. pymodes-3.1.0/src/pyModeS/core.py +203 -0
  25. pymodes-3.1.0/src/pyModeS/data/__init__.py +1 -0
  26. pymodes-3.1.0/src/pyModeS/data/airports.py +4915 -0
  27. pymodes-3.1.0/src/pyModeS/decoder/__init__.py +50 -0
  28. pymodes-3.1.0/src/pyModeS/decoder/_base.py +68 -0
  29. pymodes-3.1.0/src/pyModeS/decoder/acas.py +60 -0
  30. pymodes-3.1.0/src/pyModeS/decoder/adsb.py +80 -0
  31. pymodes-3.1.0/src/pyModeS/decoder/allcall.py +42 -0
  32. pymodes-3.1.0/src/pyModeS/decoder/bds/__init__.py +12 -0
  33. pymodes-3.1.0/src/pyModeS/decoder/bds/_helpers.py +86 -0
  34. pymodes-3.1.0/src/pyModeS/decoder/bds/_infer.py +217 -0
  35. pymodes-3.1.0/src/pyModeS/decoder/bds/bds05.py +70 -0
  36. pymodes-3.1.0/src/pyModeS/decoder/bds/bds06.py +71 -0
  37. pymodes-3.1.0/src/pyModeS/decoder/bds/bds08.py +67 -0
  38. pymodes-3.1.0/src/pyModeS/decoder/bds/bds09.py +148 -0
  39. pymodes-3.1.0/src/pyModeS/decoder/bds/bds10.py +80 -0
  40. pymodes-3.1.0/src/pyModeS/decoder/bds/bds17.py +72 -0
  41. pymodes-3.1.0/src/pyModeS/decoder/bds/bds20.py +44 -0
  42. pymodes-3.1.0/src/pyModeS/decoder/bds/bds30.py +113 -0
  43. pymodes-3.1.0/src/pyModeS/decoder/bds/bds40.py +96 -0
  44. pymodes-3.1.0/src/pyModeS/decoder/bds/bds44.py +98 -0
  45. pymodes-3.1.0/src/pyModeS/decoder/bds/bds45.py +104 -0
  46. pymodes-3.1.0/src/pyModeS/decoder/bds/bds50.py +123 -0
  47. pymodes-3.1.0/src/pyModeS/decoder/bds/bds60.py +111 -0
  48. pymodes-3.1.0/src/pyModeS/decoder/bds/bds61.py +55 -0
  49. pymodes-3.1.0/src/pyModeS/decoder/bds/bds62.py +101 -0
  50. pymodes-3.1.0/src/pyModeS/decoder/bds/bds65.py +85 -0
  51. pymodes-3.1.0/src/pyModeS/decoder/commb.py +88 -0
  52. pymodes-3.1.0/src/pyModeS/decoder/surv.py +58 -0
  53. pymodes-3.1.0/src/pyModeS/errors.py +36 -0
  54. pymodes-3.1.0/src/pyModeS/message.py +360 -0
  55. pymodes-3.1.0/src/pyModeS/position/__init__.py +19 -0
  56. pymodes-3.1.0/src/pyModeS/position/_airports.py +33 -0
  57. pymodes-3.1.0/src/pyModeS/position/_cpr.py +325 -0
  58. pymodes-3.1.0/src/pyModeS/util.py +191 -0
  59. pymodes-2.22.0/.coveragerc +0 -12
  60. pymodes-2.22.0/.github/workflows/build-publish.yml +0 -85
  61. pymodes-2.22.0/PKG-INFO +0 -389
  62. pymodes-2.22.0/README.rst +0 -366
  63. pymodes-2.22.0/doc/Makefile +0 -21
  64. pymodes-2.22.0/doc/README.rst +0 -7
  65. pymodes-2.22.0/doc/modeslive-screenshot.png +0 -0
  66. pymodes-2.22.0/doc/source/_templates/layout.html +0 -17
  67. pymodes-2.22.0/doc/source/conf.py +0 -190
  68. pymodes-2.22.0/doc/source/index.rst +0 -68
  69. pymodes-2.22.0/doc/source/modules.rst +0 -7
  70. pymodes-2.22.0/doc/source/pyModeS.c_common.rst +0 -7
  71. pymodes-2.22.0/doc/source/pyModeS.common.rst +0 -7
  72. pymodes-2.22.0/doc/source/pyModeS.decoder.acas.rst +0 -7
  73. pymodes-2.22.0/doc/source/pyModeS.decoder.adsb.rst +0 -7
  74. pymodes-2.22.0/doc/source/pyModeS.decoder.allcall.rst +0 -7
  75. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds05.rst +0 -7
  76. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds06.rst +0 -7
  77. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds08.rst +0 -7
  78. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds09.rst +0 -7
  79. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds10.rst +0 -7
  80. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds17.rst +0 -7
  81. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds20.rst +0 -7
  82. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds30.rst +0 -7
  83. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds40.rst +0 -7
  84. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds44.rst +0 -7
  85. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds45.rst +0 -7
  86. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds50.rst +0 -7
  87. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds53.rst +0 -7
  88. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.bds60.rst +0 -7
  89. pymodes-2.22.0/doc/source/pyModeS.decoder.bds.rst +0 -28
  90. pymodes-2.22.0/doc/source/pyModeS.decoder.commb.rst +0 -7
  91. pymodes-2.22.0/doc/source/pyModeS.decoder.rst +0 -29
  92. pymodes-2.22.0/doc/source/pyModeS.decoder.surv.rst +0 -7
  93. pymodes-2.22.0/doc/source/pyModeS.decoder.uncertainty.rst +0 -7
  94. pymodes-2.22.0/doc/source/pyModeS.decoder.uplink.rst +0 -7
  95. pymodes-2.22.0/doc/source/pyModeS.rst +0 -24
  96. pymodes-2.22.0/doc/warnings +0 -0
  97. pymodes-2.22.0/hatch_build.py +0 -81
  98. pymodes-2.22.0/pyproject.toml +0 -62
  99. pymodes-2.22.0/src/pyModeS/.gitignore +0 -3
  100. pymodes-2.22.0/src/pyModeS/__init__.py +0 -29
  101. pymodes-2.22.0/src/pyModeS/c_common.pxd +0 -28
  102. pymodes-2.22.0/src/pyModeS/c_common.pyi +0 -22
  103. pymodes-2.22.0/src/pyModeS/c_common.pyx +0 -402
  104. pymodes-2.22.0/src/pyModeS/common.pyi +0 -22
  105. pymodes-2.22.0/src/pyModeS/decoder/__init__.py +0 -234
  106. pymodes-2.22.0/src/pyModeS/decoder/acas.py +0 -5
  107. pymodes-2.22.0/src/pyModeS/decoder/adsb.py +0 -670
  108. pymodes-2.22.0/src/pyModeS/decoder/allcall.py +0 -94
  109. pymodes-2.22.0/src/pyModeS/decoder/bds/__init__.py +0 -193
  110. pymodes-2.22.0/src/pyModeS/decoder/bds/bds05.py +0 -154
  111. pymodes-2.22.0/src/pyModeS/decoder/bds/bds06.py +0 -210
  112. pymodes-2.22.0/src/pyModeS/decoder/bds/bds08.py +0 -60
  113. pymodes-2.22.0/src/pyModeS/decoder/bds/bds09.py +0 -150
  114. pymodes-2.22.0/src/pyModeS/decoder/bds/bds10.py +0 -53
  115. pymodes-2.22.0/src/pyModeS/decoder/bds/bds17.py +0 -83
  116. pymodes-2.22.0/src/pyModeS/decoder/bds/bds20.py +0 -60
  117. pymodes-2.22.0/src/pyModeS/decoder/bds/bds30.py +0 -35
  118. pymodes-2.22.0/src/pyModeS/decoder/bds/bds40.py +0 -124
  119. pymodes-2.22.0/src/pyModeS/decoder/bds/bds44.py +0 -164
  120. pymodes-2.22.0/src/pyModeS/decoder/bds/bds45.py +0 -208
  121. pymodes-2.22.0/src/pyModeS/decoder/bds/bds50.py +0 -173
  122. pymodes-2.22.0/src/pyModeS/decoder/bds/bds53.py +0 -169
  123. pymodes-2.22.0/src/pyModeS/decoder/bds/bds60.py +0 -181
  124. pymodes-2.22.0/src/pyModeS/decoder/bds/bds61.py +0 -83
  125. pymodes-2.22.0/src/pyModeS/decoder/bds/bds62.py +0 -551
  126. pymodes-2.22.0/src/pyModeS/decoder/commb.py +0 -88
  127. pymodes-2.22.0/src/pyModeS/decoder/ehs.py +0 -66
  128. pymodes-2.22.0/src/pyModeS/decoder/els.py +0 -35
  129. pymodes-2.22.0/src/pyModeS/decoder/flarm/__init__.py +0 -27
  130. pymodes-2.22.0/src/pyModeS/decoder/flarm/core.c +0 -124
  131. pymodes-2.22.0/src/pyModeS/decoder/flarm/core.h +0 -13
  132. pymodes-2.22.0/src/pyModeS/decoder/flarm/core.pxd +0 -4
  133. pymodes-2.22.0/src/pyModeS/decoder/flarm/decode.pyi +0 -13
  134. pymodes-2.22.0/src/pyModeS/decoder/flarm/decode.pyx +0 -147
  135. pymodes-2.22.0/src/pyModeS/decoder/surv.py +0 -139
  136. pymodes-2.22.0/src/pyModeS/decoder/uplink.py +0 -227
  137. pymodes-2.22.0/src/pyModeS/extra/__init__.py +0 -0
  138. pymodes-2.22.0/src/pyModeS/extra/aero.py +0 -185
  139. pymodes-2.22.0/src/pyModeS/extra/rtlreader.py +0 -207
  140. pymodes-2.22.0/src/pyModeS/extra/tcpclient.py +0 -398
  141. pymodes-2.22.0/src/pyModeS/py_common.py +0 -490
  142. pymodes-2.22.0/src/pyModeS/streamer/__init__.py +0 -0
  143. pymodes-2.22.0/src/pyModeS/streamer/decode.py +0 -320
  144. pymodes-2.22.0/src/pyModeS/streamer/modeslive.py +0 -164
  145. pymodes-2.22.0/src/pyModeS/streamer/screen.py +0 -234
  146. pymodes-2.22.0/src/pyModeS/streamer/source.py +0 -91
  147. pymodes-2.22.0/tests/benchmark.py +0 -132
  148. pymodes-2.22.0/tests/data/sample_data_adsb.csv +0 -2000
  149. pymodes-2.22.0/tests/data/sample_data_commb_df20.csv +0 -5000
  150. pymodes-2.22.0/tests/data/sample_data_commb_df21.csv +0 -5000
  151. pymodes-2.22.0/tests/sample_run_adsb.py +0 -41
  152. pymodes-2.22.0/tests/sample_run_commb.py +0 -91
  153. pymodes-2.22.0/tests/test_adsb.py +0 -266
  154. pymodes-2.22.0/tests/test_allcall.py +0 -13
  155. pymodes-2.22.0/tests/test_bds_inference.py +0 -67
  156. pymodes-2.22.0/tests/test_c_common.py +0 -106
  157. pymodes-2.22.0/tests/test_commb.py +0 -47
  158. pymodes-2.22.0/tests/test_common_contract.py +0 -84
  159. pymodes-2.22.0/tests/test_full.py +0 -17
  160. pymodes-2.22.0/tests/test_py_common.py +0 -148
  161. pymodes-2.22.0/tests/test_surv.py +0 -22
  162. pymodes-2.22.0/tests/test_tell.py +0 -20
  163. pymodes-2.22.0/uv.lock +0 -752
  164. {pymodes-2.22.0 → pymodes-3.1.0}/LICENSE +0 -0
  165. {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
- # C extensions
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
+ [![license](https://img.shields.io/badge/license-GPL--3.0-blue)](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
+ ```