pyModeS 3.0.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-3.0.0 → pymodes-3.1.0}/CHANGELOG.md +43 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/PKG-INFO +1 -1
- {pymodes-3.0.0 → pymodes-3.1.0}/pyproject.toml +1 -1
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_altcode.py +16 -8
- pymodes-3.1.0/src/pyModeS/_pipe.py +758 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/core.py +9 -1
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/position/_cpr.py +8 -0
- pymodes-3.0.0/src/pyModeS/_pipe.py +0 -364
- {pymodes-3.0.0 → pymodes-3.1.0}/.gitignore +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/LICENSE +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/README.md +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/__init__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_aero.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_bits.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_callsign.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_idcode.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_schema.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_uncertainty.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_v2_removed.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/__init__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/__main__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_args.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_sink.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_source.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_tui.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/decode.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/live.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/data/__init__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/data/airports.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/__init__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/_base.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/acas.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/adsb.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/allcall.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/__init__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/_helpers.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/_infer.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds05.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds06.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds08.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds09.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds10.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds17.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds20.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds30.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds40.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds44.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds45.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds50.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds60.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds61.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds62.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds65.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/commb.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/surv.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/errors.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/message.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/position/__init__.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/position/_airports.py +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/py.typed +0 -0
- {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/util.py +0 -0
|
@@ -5,6 +5,49 @@ All notable changes to pyModeS are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
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
|
+
|
|
8
51
|
## [3.0.0] — 2026-04-13
|
|
9
52
|
|
|
10
53
|
First release of the v3 ground-up rewrite. **Not backwards-compatible
|
|
@@ -5,9 +5,12 @@ It supports two encodings:
|
|
|
5
5
|
|
|
6
6
|
- Q = 1: 25-foot interval, linear. 11 bits form a value N;
|
|
7
7
|
altitude = N * 25 - 1000 (feet).
|
|
8
|
-
- Q = 0: 100-foot interval, Gillham
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
- Q = 0: 100-foot interval, Gillham code (a Gray-code variant used by
|
|
9
|
+
legacy pressure altimeters; see ICAO Annex 10 Vol IV and DO-260).
|
|
10
|
+
Re-orders the 11 non-M non-Q bits into a Gillham-coded (500-ft,
|
|
11
|
+
100-ft) pair and converts to feet via
|
|
12
|
+
`alt = n500 * 500 + n100 * 100 - 1300`. Note: Q=0 is a distinct
|
|
13
|
+
encoding standard, not an indicator of high altitude.
|
|
11
14
|
|
|
12
15
|
If the entire 13-bit field is zero, altitude is reported as unknown.
|
|
13
16
|
"""
|
|
@@ -41,7 +44,7 @@ def altcode_to_altitude(ac: int) -> int | None:
|
|
|
41
44
|
return n * 25 - 1000
|
|
42
45
|
|
|
43
46
|
if m_bit == 0 and q_bit == 0:
|
|
44
|
-
# 100-foot interval, Gillham
|
|
47
|
+
# 100-foot interval, Gillham code.
|
|
45
48
|
# Extract each named bit from the 13-bit AC field.
|
|
46
49
|
def b(pos: int) -> int:
|
|
47
50
|
return (ac >> (12 - pos)) & 0x1
|
|
@@ -53,9 +56,10 @@ def altcode_to_altitude(ac: int) -> int | None:
|
|
|
53
56
|
b2, d2 = b(9), b(10)
|
|
54
57
|
b4, d4 = b(11), b(12)
|
|
55
58
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
+
# Rearrange the non-M, non-Q bits per DO-260 into the sequence
|
|
60
|
+
# D2 D4 A1 A2 A4 B1 B2 B4 C1 C2 C4 (11 bits)
|
|
61
|
+
# The first 8 bits form a Gillham-coded 500-ft counter and the
|
|
62
|
+
# last 3 bits a 100-ft counter, per ICAO Annex 10 Vol IV.
|
|
59
63
|
gc500 = (
|
|
60
64
|
(d2 << 7)
|
|
61
65
|
| (d4 << 6)
|
|
@@ -90,7 +94,11 @@ def altcode_to_altitude(ac: int) -> int | None:
|
|
|
90
94
|
|
|
91
95
|
|
|
92
96
|
def _gray2int(n: int) -> int:
|
|
93
|
-
"""Convert an unsigned Gillham
|
|
97
|
+
"""Convert an unsigned Gillham code to a plain binary integer.
|
|
98
|
+
|
|
99
|
+
Gillham is a reflected Gray-code variant, so the standard Gray-to-binary
|
|
100
|
+
fold (XOR with successive right-shifts) applies.
|
|
101
|
+
"""
|
|
94
102
|
n ^= n >> 8
|
|
95
103
|
n ^= n >> 4
|
|
96
104
|
n ^= n >> 2
|