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.
Files changed (61) hide show
  1. {pymodes-3.0.0 → pymodes-3.1.0}/CHANGELOG.md +43 -0
  2. {pymodes-3.0.0 → pymodes-3.1.0}/PKG-INFO +1 -1
  3. {pymodes-3.0.0 → pymodes-3.1.0}/pyproject.toml +1 -1
  4. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_altcode.py +16 -8
  5. pymodes-3.1.0/src/pyModeS/_pipe.py +758 -0
  6. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/core.py +9 -1
  7. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/position/_cpr.py +8 -0
  8. pymodes-3.0.0/src/pyModeS/_pipe.py +0 -364
  9. {pymodes-3.0.0 → pymodes-3.1.0}/.gitignore +0 -0
  10. {pymodes-3.0.0 → pymodes-3.1.0}/LICENSE +0 -0
  11. {pymodes-3.0.0 → pymodes-3.1.0}/README.md +0 -0
  12. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/__init__.py +0 -0
  13. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_aero.py +0 -0
  14. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_bits.py +0 -0
  15. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_callsign.py +0 -0
  16. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_idcode.py +0 -0
  17. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_schema.py +0 -0
  18. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_uncertainty.py +0 -0
  19. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/_v2_removed.py +0 -0
  20. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/__init__.py +0 -0
  21. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/__main__.py +0 -0
  22. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_args.py +0 -0
  23. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_sink.py +0 -0
  24. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_source.py +0 -0
  25. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/_tui.py +0 -0
  26. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/decode.py +0 -0
  27. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/cli/live.py +0 -0
  28. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/data/__init__.py +0 -0
  29. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/data/airports.py +0 -0
  30. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/__init__.py +0 -0
  31. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/_base.py +0 -0
  32. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/acas.py +0 -0
  33. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/adsb.py +0 -0
  34. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/allcall.py +0 -0
  35. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/__init__.py +0 -0
  36. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/_helpers.py +0 -0
  37. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/_infer.py +0 -0
  38. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds05.py +0 -0
  39. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds06.py +0 -0
  40. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds08.py +0 -0
  41. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds09.py +0 -0
  42. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds10.py +0 -0
  43. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds17.py +0 -0
  44. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds20.py +0 -0
  45. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds30.py +0 -0
  46. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds40.py +0 -0
  47. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds44.py +0 -0
  48. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds45.py +0 -0
  49. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds50.py +0 -0
  50. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds60.py +0 -0
  51. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds61.py +0 -0
  52. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds62.py +0 -0
  53. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/bds/bds65.py +0 -0
  54. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/commb.py +0 -0
  55. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/decoder/surv.py +0 -0
  56. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/errors.py +0 -0
  57. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/message.py +0 -0
  58. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/position/__init__.py +0 -0
  59. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/position/_airports.py +0 -0
  60. {pymodes-3.0.0 → pymodes-3.1.0}/src/pyModeS/py.typed +0 -0
  61. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyModeS
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Python Mode-S and ADS-B Decoder
5
5
  Project-URL: homepage, https://mode-s.org
6
6
  Project-URL: repository, https://github.com/junzis/pyModeS
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyModeS"
3
- version = "3.0.0"
3
+ version = "3.1.0"
4
4
  description = "Python Mode-S and ADS-B Decoder"
5
5
  authors = [{ name = "Junzi Sun", email = "git@junzis.com" }]
6
6
  license = { text = "GNU GPL v3" }
@@ -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 gray code. Re-orders the 11 non-M
9
- non-Q bits into a gray-coded (500-ft, 100-ft) pair and converts to
10
- feet via `alt = n500 * 500 + n100 * 100 - 1300`.
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 gray code.
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
- # Re-order into the Gillham gray string:
57
- # graystr = D2 D4 A1 A2 A4 B1 B2 B4 C1 C2 C4 (11 bits)
58
- # Split: gc500 = first 8, gc100 = last 3.
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 gray code to a plain binary integer."""
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