ka9q-python 3.16.1__tar.gz → 3.18.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 (114) hide show
  1. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/CHANGELOG.md +95 -0
  2. {ka9q_python-3.16.1/ka9q_python.egg-info → ka9q_python-3.18.0}/PKG-INFO +7 -12
  3. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/README.md +1 -1
  4. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/GETTING_STARTED.md +2 -2
  5. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/INSTALLATION.md +5 -5
  6. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/MULTI_STREAM.md +3 -3
  7. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/RECIPES.md +3 -3
  8. ka9q_python-3.18.0/docs/REQUIREMENTS.md +409 -0
  9. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/__init__.py +9 -1
  10. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/compat.py +1 -1
  11. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/control.py +145 -59
  12. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/discovery.py +95 -9
  13. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/multi_stream.py +88 -0
  14. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/rtp_recorder.py +17 -2
  15. ka9q_python-3.18.0/ka9q/slot_clock.py +258 -0
  16. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/stream.py +82 -29
  17. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/types.py +1 -1
  18. {ka9q_python-3.16.1 → ka9q_python-3.18.0/ka9q_python.egg-info}/PKG-INFO +7 -12
  19. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/SOURCES.txt +5 -1
  20. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_radio_compat +1 -1
  21. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/pyproject.toml +6 -6
  22. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/conftest.py +4 -3
  23. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_channel_verification.py +134 -13
  24. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_client_id_destination.py +16 -16
  25. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ensure_channel_encoding.py +10 -8
  26. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_filter_edges.py +17 -20
  27. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_iq_20khz_f32.py +13 -7
  28. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_listen_multicast.py +13 -5
  29. ka9q_python-3.18.0/tests/test_multistream_gap_storm.py +57 -0
  30. ka9q_python-3.18.0/tests/test_multistream_prune.py +84 -0
  31. ka9q_python-3.18.0/tests/test_slot_clock.py +145 -0
  32. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_live.py +13 -3
  33. ka9q_python-3.16.1/setup.py +0 -47
  34. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/LICENSE +0 -0
  35. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/MANIFEST.in +0 -0
  36. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/API_REFERENCE.md +0 -0
  37. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/ARCHITECTURE.md +0 -0
  38. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/CLI_GUIDE.md +0 -0
  39. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/RTP_TIMING_SUPPORT.md +0 -0
  40. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/SECURITY.md +0 -0
  41. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/TESTING_GUIDE.md +0 -0
  42. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/TUI_GUIDE.md +0 -0
  43. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/advanced_features_demo.py +0 -0
  44. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/channel_cleanup_example.py +0 -0
  45. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/codar_oceanography.py +0 -0
  46. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/diagnostics/diagnose_packets.py +0 -0
  47. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/diagnostics/repro_utc_bug.py +0 -0
  48. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/discover_example.py +0 -0
  49. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/grape_integration_example.py +0 -0
  50. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/hf_band_scanner.py +0 -0
  51. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/multi_stream_smoke.py +0 -0
  52. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/rtp_recorder_example.py +0 -0
  53. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/simple_am_radio.py +0 -0
  54. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/spectrum_example.py +0 -0
  55. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/stream_example.py +0 -0
  56. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/superdarn_recorder.py +0 -0
  57. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/test_channel_operations.py +0 -0
  58. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/test_improvements.py +0 -0
  59. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/test_timing_fields.py +0 -0
  60. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/tune.py +0 -0
  61. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/tune_example.py +0 -0
  62. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/_multicast.py +0 -0
  63. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/addressing.py +0 -0
  64. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/cli.py +0 -0
  65. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/exceptions.py +0 -0
  66. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/managed_stream.py +0 -0
  67. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/monitor.py +0 -0
  68. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/pps_calibrator.py +0 -0
  69. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/resequencer.py +0 -0
  70. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/spectrum_stream.py +0 -0
  71. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/status.py +0 -0
  72. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/status_listener.py +0 -0
  73. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/stream_quality.py +0 -0
  74. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/tui.py +0 -0
  75. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/utils.py +0 -0
  76. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
  77. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/entry_points.txt +0 -0
  78. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/requires.txt +0 -0
  79. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/top_level.txt +0 -0
  80. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/scripts/check_upstream_drift.py +0 -0
  81. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/scripts/sync_types.py +0 -0
  82. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/setup.cfg +0 -0
  83. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/__init__.py +0 -0
  84. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_addressing.py +0 -0
  85. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_create_split_encoding.py +0 -0
  86. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_decode_description.py +0 -0
  87. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_decode_functions.py +0 -0
  88. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_encode_functions.py +0 -0
  89. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_encode_socket.py +0 -0
  90. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_integration.py +0 -0
  91. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_lifetime.py +0 -0
  92. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_managed_stream_recovery.py +0 -0
  93. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_monitor.py +0 -0
  94. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_multicast_helpers.py +0 -0
  95. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_multihomed.py +0 -0
  96. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_native_discovery.py +0 -0
  97. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_parse_rtp_samples_iq.py +0 -0
  98. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_performance_fixes.py +0 -0
  99. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_protocol_compat.py +0 -0
  100. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_remove_channel.py +0 -0
  101. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_rtp_recorder.py +0 -0
  102. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_security_features.py +0 -0
  103. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_spectrum.py +0 -0
  104. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ssrc_dest_unit.py +0 -0
  105. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ssrc_encoding_unit.py +0 -0
  106. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ssrc_radiod_host_unit.py +0 -0
  107. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_status_decoder.py +0 -0
  108. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_status_listener.py +0 -0
  109. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ttl_warning.py +0 -0
  110. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune.py +0 -0
  111. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_cli.py +0 -0
  112. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_debug.py +0 -0
  113. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_method.py +0 -0
  114. {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_upstream_drift.py +0 -0
@@ -1,5 +1,100 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.18.0] - 2026-06-28
4
+
5
+ ### Added
6
+
7
+ - **`SlotClock` — epoch-aligned slot boundaries in RTP-timestamp space**
8
+ (`ka9q.slot_clock`). The canonical, drift-immune timing primitive for every
9
+ sigmond slot/period recorder (psk FT8/FT4, wspr WSPR/FST4W, meteor-scatter,
10
+ …). Slot boundaries are driven by radiod's GPSDO-disciplined RTP timestamp
11
+ (which advances exactly once per output sample of real time) rather than a
12
+ delivered-sample-count projection that silently drifts when the receive path
13
+ over/under-counts samples — the failure that labels a WAV with a UTC the audio
14
+ doesn't match and zeroes out decodes on good RF. Boundary stepping is exact
15
+ integer arithmetic (`cadence_sec * sample_rate` must be integer). Absolute
16
+ positions are tracked as an unwrapped 64-bit count relative to a monotonic
17
+ high-water, so harvesting keeps working past the 2³¹-sample signed-32 window
18
+ (~49.7 h @ 12 kHz / ~9.3 h @ 64 kHz IQ) — a long WSPR run no longer stalls.
19
+ Exposes `SlotClock`, `Slot`, and `rtp_diff` (Karn signed-32 difference).
20
+ Pure timing logic — owns no socket, ring, or thread.
21
+
22
+ ### Notes
23
+
24
+ - This is the release that promotes `SlotClock` (previously parked) to public,
25
+ consumed API: the sigmond recorders are migrating their slot/period timing
26
+ onto it so an upstream timing fix lands once instead of being re-patched per
27
+ client. Pairs with `hamsci_dsp.timing.acquire_anchor_utc` (the shared RTP→UTC
28
+ anchor) on the sigmond side.
29
+
30
+ ## [3.17.0] - 2026-06-28
31
+
32
+ First release marked **Production/Stable** (trove classifier 4 → 5). Folds in
33
+ 23 commits of feature and resilience work that had accumulated on `main` past
34
+ the `v3.16.1` tag with no version bump.
35
+
36
+ ### Added
37
+
38
+ - **`RadiodControl.poll_channel(ssrc)`** (`control.py`) — a targeted, O(1)
39
+ status probe for a single channel. Replaces the previous reliance on a full
40
+ channel discovery sweep when only one channel's state is needed.
41
+ - **`MultiStream.prune_frequency(...)`** (`multi_stream.py`) — releases a
42
+ superseded channel slot *and its ring buffer*, fixing a ring leak when a
43
+ frequency is retuned/replaced within a live `MultiStream`.
44
+ - **mDNS hostname + port surfaced by discovery** (`discovery.py`) — additive
45
+ fields on the discovery result; existing consumers are unaffected.
46
+ - **RTP↔GPS offset-step detection (`anchor_epoch`)** (`stream.py`) — a
47
+ `RadiodStream` now detects a step in radiod's RTP↔GPS offset and re-anchors
48
+ its timing reference instead of carrying a stale anchor.
49
+
50
+ ### Changed
51
+
52
+ - **`ensure_channel` now verifies via `poll_channel` (O(1))** instead of a dead
53
+ discovery sweep (`control.py`). Faster, more reliable channel confirmation.
54
+ Note a deliberate relaxation on the *create* path: the channel is now accepted
55
+ on **frequency match alone** — a rate/preset divergence is logged as a warning
56
+ (not raised) and the destination is no longer re-verified. Callers should treat
57
+ the returned `ChannelInfo` as authoritative for the granted encoding/rate
58
+ (the sigmond recorders already do). The reuse path still verifies strictly.
59
+ - **`RadiodStream` binds the channel's multicast group**, not `0.0.0.0`
60
+ (`stream.py`) — avoids cross-talk on hosts carrying multiple multicast groups.
61
+ - **`rtp_to_wallclock` renamed to `rtp_to_utc`** (`rtp_recorder.py`). The name
62
+ "wallclock" wrongly implied a system-clock dependency; the function is purely
63
+ RTP/GPS-referenced. `rtp_to_wallclock` remains as a deprecated alias — no
64
+ caller needs to change.
65
+ - **`anchor_step_threshold_sec` raised 0.25 → 0.75** (`stream.py`) to tolerate
66
+ output-timing jitter on a busy radiod without spuriously re-anchoring.
67
+ - **8 MB receive buffer on `poll_channel`'s status listener** (`control.py`) to
68
+ avoid drops while probing on a busy status multicast group.
69
+ - **ka9q-radio compatibility pin advanced to `9b742e6`** (no protocol drift;
70
+ validated by `check_upstream_drift.py`).
71
+ - **Repository moved to the HamSCI org**; project/doc/URL references updated
72
+ from `mijahauan/` to `HamSCI/`.
73
+
74
+ ### Fixed
75
+
76
+ - **Gap storm now treated as a stream-health failure → re-subscribe**
77
+ (`multi_stream.py`, 228a041). A stale `MultiStream` subscription after a
78
+ radiod restart manifests as a sustained packet-gap storm (not silence); the
79
+ health monitor now detects it and re-subscribes, restoring delivery without
80
+ an external restart. See the sigmond `stale-subscription-gap-storm-protection`
81
+ note.
82
+
83
+ ### Packaging
84
+
85
+ - **Removed the redundant `setup.py`.** All project metadata lives in
86
+ `pyproject.toml`'s PEP 621 `[project]` table (`setuptools.build_meta`
87
+ backend). `setup.py` had drifted to a stale `3.10.0` / `4 - Beta` duplicate
88
+ and was an unused second source of truth.
89
+
90
+ ### Docs / Tests
91
+
92
+ - Added a **requirements baseline** (`docs/REQUIREMENTS.md`, `KQP-*` spec).
93
+ - `CLAUDE.md` documents `MultiStream` as the fourth abstraction layer.
94
+ - The **live channel-verification suite is now gated behind explicit opt-in**
95
+ (`--radiod-host`), so the default `pytest` run no longer hangs waiting on a
96
+ live radiod. Unit suite: 363 passed, 27 skipped.
97
+
3
98
  ## [3.16.1] - 2026-05-24
4
99
 
5
100
  ### Fixed
@@ -1,17 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.16.1
3
+ Version: 3.18.0
4
4
  Summary: Python interface for ka9q-radio control and monitoring
5
- Home-page: https://github.com/mijahauan/ka9q-python
6
- Author: Michael Hauan AC0G
7
5
  Author-email: Michael Hauan AC0G <ac0g@hauan.org>
8
6
  License: MIT
9
- Project-URL: Homepage, https://github.com/mijahauan/ka9q-python
10
- Project-URL: Documentation, https://github.com/mijahauan/ka9q-python/blob/main/README.md
11
- Project-URL: Repository, https://github.com/mijahauan/ka9q-python
12
- Project-URL: Issues, https://github.com/mijahauan/ka9q-python/issues
7
+ Project-URL: Homepage, https://github.com/HamSCI/ka9q-python
8
+ Project-URL: Documentation, https://github.com/HamSCI/ka9q-python/blob/main/README.md
9
+ Project-URL: Repository, https://github.com/HamSCI/ka9q-python
10
+ Project-URL: Issues, https://github.com/HamSCI/ka9q-python/issues
13
11
  Keywords: ka9q-radio,sdr,ham-radio,radio-control
14
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
15
13
  Classifier: Intended Audience :: Science/Research
16
14
  Classifier: Intended Audience :: Telecommunications Industry
17
15
  Classifier: Programming Language :: Python :: 3
@@ -32,10 +30,7 @@ Provides-Extra: tui
32
30
  Requires-Dist: textual>=0.50; extra == "tui"
33
31
  Provides-Extra: opus
34
32
  Requires-Dist: opuslib>=3.0; extra == "opus"
35
- Dynamic: author
36
- Dynamic: home-page
37
33
  Dynamic: license-file
38
- Dynamic: requires-python
39
34
 
40
35
  # ka9q-python
41
36
 
@@ -95,7 +90,7 @@ pip install "ka9q-python[tui,opus]" # multiple
95
90
  Or install from source:
96
91
 
97
92
  ```bash
98
- git clone https://github.com/mijahauan/ka9q-python.git
93
+ git clone https://github.com/HamSCI/ka9q-python.git
99
94
  cd ka9q-python
100
95
  pip install -e .
101
96
  ```
@@ -56,7 +56,7 @@ pip install "ka9q-python[tui,opus]" # multiple
56
56
  Or install from source:
57
57
 
58
58
  ```bash
59
- git clone https://github.com/mijahauan/ka9q-python.git
59
+ git clone https://github.com/HamSCI/ka9q-python.git
60
60
  cd ka9q-python
61
61
  pip install -e .
62
62
  ```
@@ -25,7 +25,7 @@ pip install ka9q-python
25
25
  Or, if you want to install from source:
26
26
 
27
27
  ```bash
28
- git clone https://github.com/mijahauan/ka9q-python.git
28
+ git clone https://github.com/HamSCI/ka9q-python.git
29
29
  cd ka9q-python
30
30
  pip install -e .
31
31
  ```
@@ -235,7 +235,7 @@ Congratulations! You've created your first `ka9q-python` application and learned
235
235
 
236
236
  3. **Learn About Advanced Features**: Check out `examples/advanced_features_demo.py` to see how to use Doppler tracking, PLL configuration, squelch, and more.
237
237
 
238
- 4. **Join the Community**: If you have questions or want to contribute, visit the [GitHub repository](https://github.com/mijahauan/ka9q-python).
238
+ 4. **Join the Community**: If you have questions or want to contribute, visit the [GitHub repository](https://github.com/HamSCI/ka9q-python).
239
239
 
240
240
  ---
241
241
 
@@ -13,12 +13,12 @@ The distribution name is `ka9q-python`; the import name is `ka9q`.
13
13
 
14
14
  ### From GitHub (development version)
15
15
  ```bash
16
- pip install git+https://github.com/mijahauan/ka9q-python.git
16
+ pip install git+https://github.com/HamSCI/ka9q-python.git
17
17
  ```
18
18
 
19
19
  ### From Local Clone
20
20
  ```bash
21
- git clone https://github.com/mijahauan/ka9q-python.git
21
+ git clone https://github.com/HamSCI/ka9q-python.git
22
22
  cd ka9q-python
23
23
  pip install .
24
24
  ```
@@ -27,7 +27,7 @@ pip install .
27
27
 
28
28
  ### Editable Install
29
29
  ```bash
30
- git clone https://github.com/mijahauan/ka9q-python.git
30
+ git clone https://github.com/HamSCI/ka9q-python.git
31
31
  cd ka9q-python
32
32
  pip install -e .
33
33
  ```
@@ -218,7 +218,7 @@ twine upload dist/*
218
218
 
219
219
  ```bash
220
220
  # Clone and install in editable mode
221
- git clone https://github.com/mijahauan/ka9q-python.git
221
+ git clone https://github.com/HamSCI/ka9q-python.git
222
222
  cd ka9q-python
223
223
  pip install -e ".[dev]"
224
224
 
@@ -285,5 +285,5 @@ if __name__ == '__main__':
285
285
  ## Support
286
286
 
287
287
  - Documentation: See README.md and other docs in the repository
288
- - Issues: https://github.com/mijahauan/ka9q-python/issues
288
+ - Issues: https://github.com/HamSCI/ka9q-python/issues
289
289
  - Examples: See `examples/` directory
@@ -38,10 +38,10 @@ Keep `ManagedStream` when:
38
38
  - You specifically want each channel's receive path isolated.
39
39
 
40
40
  Production users:
41
- [psk-recorder](https://github.com/mijahauan/psk-recorder) runs 20
41
+ [psk-recorder](https://github.com/HamSCI/psk-recorder) runs 20
42
42
  channels (10 FT4 + 10 FT8) on bee3 through a single `MultiStream`.
43
- [wspr-recorder](https://github.com/mijahauan/wspr-recorder) and
44
- [hf-timestd](https://github.com/mijahauan/hf-timestd) use the same
43
+ [wspr-recorder](https://github.com/HamSCI/wspr-recorder) and
44
+ [hf-timestd](https://github.com/HamSCI/hf-timestd) use the same
45
45
  pattern.
46
46
 
47
47
  ---
@@ -130,9 +130,9 @@ matters.
130
130
  ## Recipe 2 — Fixed sets of same-type channels (WSPR, PSK, FT8, timing)
131
131
 
132
132
  This is the pattern used by
133
- [wspr-recorder](https://github.com/mijahauan/wspr-recorder),
134
- [psk-recorder](https://github.com/mijahauan/psk-recorder), and
135
- [hf-timestd](https://github.com/mijahauan/hf-timestd):
133
+ [wspr-recorder](https://github.com/HamSCI/wspr-recorder),
134
+ [psk-recorder](https://github.com/HamSCI/psk-recorder), and
135
+ [hf-timestd](https://github.com/HamSCI/hf-timestd):
136
136
 
137
137
  1. Read a band plan (list of frequencies + preset + sample rate).
138
138
  2. For each entry, call `ensure_channel()` — deterministic SSRC
@@ -0,0 +1,409 @@
1
+ # ka9q-python — Requirements Specification
2
+
3
+ **Status:** v0.1 baseline (retroactive). **Owner:** Michael Hauan (AC0G).
4
+ **Last reconciled against code:** ka9q-python `3.16.1` (`v3.16.1-17-g10f7e47`, ka9q-radio pin `9b742e60`) (2026-06-25).
5
+ **Prefix:** `KQP`.
6
+
7
+ > Retroactive, greenfield-grade application of
8
+ > [sigmond/docs/REQUIREMENTS-TEMPLATE.md](https://github.com/HamSCI/sigmond/blob/main/docs/REQUIREMENTS-TEMPLATE.md)
9
+ > to a **library**, not a contract client. ka9q-python is the shared Python
10
+ > binding to ka9q-radio's `radiod` (TLV status/command API + RTP MultiStream
11
+ > subscription) that *every* RF client in the suite imports. It therefore has
12
+ > **no** sigmond client-contract surface of its own (no `inventory`/`validate`,
13
+ > no `deploy.toml`, no systemd unit). Its "interface" is its **public Python
14
+ > API** plus the **radiod wire protocol** it tracks; §8.3 documents API
15
+ > stability and the upstream-ka9q-radio compatibility surface rather than the
16
+ > [client contract](https://github.com/HamSCI/sigmond/blob/main/docs/CLIENT-CONTRACT.md).
17
+ > Provenance tags: `[DOC]` documented · `[CODE]` implicit-in-code · `[NEW]`
18
+ > surfaced by this review. Status: ✅ implemented · 🟡 partial/unverified · ⬜ planned.
19
+
20
+ ## 1. Context & problem statement
21
+
22
+ Every RF client in the HamSCI/DASI2 suite consumes its IQ from one place:
23
+ `radiod` (the ka9q-radio daemon), which publishes demodulated/IQ channels as
24
+ RTP MultiStreams over multicast UDP and exposes a binary TLV (Type-Length-Value)
25
+ status/command protocol for provisioning those channels. Without a shared,
26
+ correct Python binding, each client (wspr-recorder, psk-recorder, hf-timestd,
27
+ hfdl-recorder, codar-sounder, superdarn-sounder, meteor-scatter, hf-tec) would
28
+ re-implement RTP receive, packet resequencing, status decode, and channel
29
+ provisioning — duplicating subtle, timing-critical code and drifting against
30
+ the C protocol independently. ka9q-python is that single binding: it owns the
31
+ RTP receive path and the radiod control path on behalf of the whole suite.
32
+
33
+ This is the integration substrate identified as **PSWS charette issue #6:30**
34
+ ("ka9q-python / radiod interface"). Because it is imported, not invoked, its
35
+ objectives/inputs/outputs **are an API contract**: a breaking change to
36
+ `RadiodControl.ensure_channel(...)`, `MultiStream`, the `ChannelStatus` decode,
37
+ or the `StatusType`/`Encoding` enums ripples into every downstream service at
38
+ once. The library's defining design principle follows from that: it tracks a
39
+ *pinned* ka9q-radio commit, regenerates its protocol enums from the C headers,
40
+ and ships a drift watcher so the suite learns about a wire-protocol change
41
+ *before* deploying a radiod that would silently break RTP delivery.
42
+
43
+ The library is **mature**: 39 test files (~375 collected cases), four layered
44
+ stream abstractions, a typed status decoder covering 117 TLV parameters, CLI +
45
+ TUI, published to PyPI (`Development Status :: 4 - Beta`), and in production
46
+ under every suite recorder. Most of its requirements are therefore `[CODE]✅` —
47
+ the honest retroactive picture of a binding that was built before its
48
+ requirements were written down.
49
+
50
+ ## 2. Goals & objectives
51
+
52
+ - Provide one **correct, shared** Python receive path for radiod RTP
53
+ (multicast, resequenced, gap-aware) so no client re-implements it.
54
+ - Provide one **radiod control path** (TLV) for channel create/tune/ensure/
55
+ remove, including wideband channels via `ensure_channel(low_edge, high_edge)`.
56
+ - Decode **every radiod RTP encoding** and the full typed **status** surface so
57
+ clients read frequency/preset/sample-rate/timing fields without parsing bytes.
58
+ - Deliver **sample-accurate RTP↔wallclock** timing (GPS_TIME/RTP_TIMESNAP) — the
59
+ substrate hf-timestd's whole timing hierarchy stands on.
60
+ - Keep the **public API stable** across the suite, and keep its protocol
61
+ definitions **provably in sync** with a pinned ka9q-radio commit (drift
62
+ detectable before deploy).
63
+ - Stay **pure-Python, NumPy-only** at runtime so it installs as an editable
64
+ sibling into every consumer venv with no compiled/optional weight on the core.
65
+
66
+ ## 3. Non-goals / out of scope
67
+
68
+ - **Being radiod.** It does not demodulate, tune hardware, or run an SDR — it
69
+ talks to `radiod`, which talks to the SDR. (Owner: ka9q-radio.)
70
+ - **Being a sigmond client.** It has no `inventory`/`validate`/`deploy.toml`/
71
+ systemd unit and is never lifecycle-managed; sigmond consumes it transitively
72
+ through the clients that import it. (Owner: each client + sigmond.)
73
+ - **Domain science / decoding.** WSPR/FT8/timing/Doppler logic lives in the
74
+ consuming clients and in `hamsci-dsp`, never here.
75
+ - **Owning the ka9q-radio pin policy at deploy time.** The library *declares*
76
+ the validated commit and *detects* drift; deciding when to advance a
77
+ deployed radiod is sigmond's `smd watch ka9q` / operator concern.
78
+ - **Cross-host orchestration.** Multicast discovery is per-LAN/per-interface;
79
+ fleet coordination is sigmond/PSWS scope.
80
+
81
+ ## 4. Stakeholders & actors
82
+
83
+ ka9q-python's "actors" are its consumers and the protocol it tracks:
84
+
85
+ - **`radiod`** (ka9q-radio) — the controlled peer: RTP IQ/audio source and TLV
86
+ status/command server. The pinned C headers are the authority for the wire
87
+ protocol.
88
+ - **Consuming suite clients** (the API consumers) — wspr-recorder, psk-recorder,
89
+ hf-timestd, hfdl-recorder, codar-sounder, superdarn-sounder, meteor-scatter,
90
+ hf-tec. Each imports `from ka9q import …`; their RTP receive + provisioning is
91
+ this library's API.
92
+ - **`hamsci-dsp`** — sibling shared lib; pairs with ka9q-python (timing/DSP) in
93
+ the same consumer venvs.
94
+ - **sigmond** — does not import the runtime package, but wraps
95
+ `scripts/check_upstream_drift.py` as `smd watch ka9q` (drift watcher) and
96
+ installs ka9q-python as an editable sibling into every consumer venv.
97
+ - **Operators / developers** — `ka9q` CLI + TUI for interactive probing/tuning;
98
+ `sync_types.py` maintainer regenerating enums on a ka9q-radio bump.
99
+ - **The PyPI ecosystem** — published package; the `ka9q-radio` upstream project
100
+ is the moving target the compat surface defends against.
101
+
102
+ ## 5. Assumptions & constraints
103
+
104
+ - `KQP-C-001` `[DOC]` ✅ Runtime SHALL be **pure Python with NumPy as the only
105
+ hard dependency** (`numpy>=1.24.0`); `textual`/`opuslib` are opt-in extras
106
+ (`tui`/`opus`) and `pytest` is `dev`.
107
+ - `KQP-C-002` `[DOC]` ✅ SHALL support **Python ≥3.9** (classifiers through 3.12).
108
+ - `KQP-C-003` `[CODE]` ✅ The transport SHALL be **multicast UDP**; there is no
109
+ unicast radiod fallback. Multi-homed hosts SHALL select an interface explicitly.
110
+ - `KQP-C-004` `[DOC]` ✅ Protocol enums (`StatusType`, `Encoding`, `DemodType`,
111
+ `WindowType`) SHALL be **generated from ka9q-radio's C headers** (`status.h`,
112
+ `rtp.h`), never hand-edited, and pinned to a recorded commit.
113
+ - `KQP-C-005` `[CODE]` ✅ SHALL install as an **editable sibling**
114
+ (`[tool.uv.sources] path=…, editable=true`) into each consumer venv so a
115
+ `git pull` propagates without reinstall; the library's `uv.lock` is gitignored
116
+ (a library does not bind downstream consumers — they pin it in their own lock).
117
+ - `KQP-C-006` `[CODE]` ✅ Import name SHALL be `ka9q`; PyPI/distribution name
118
+ SHALL be `ka9q-python` (KA9Q attribution). These SHALL NOT be conflated.
119
+
120
+ ## 6. Functional requirements
121
+
122
+ ### 6.1 radiod control (TLV command path)
123
+ - `KQP-F-001` `[DOC]` ✅ `RadiodControl` SHALL implement the ka9q-radio TLV
124
+ binary protocol over multicast and expose channel create/tune/configure/
125
+ destroy, guarded by an `RLock` for concurrent use.
126
+ - `KQP-F-002` `[DOC]` ✅ SHALL provide `ensure_channel(frequency_hz, preset,
127
+ sample_rate, encoding, …)` — idempotent provision-or-reuse keyed on a
128
+ frequency tolerance — returning the channel info incl. SSRC.
129
+ - `KQP-F-003` `[DOC]` ✅ `ensure_channel`/`create_channel` SHALL accept
130
+ **`low_edge`/`high_edge`** (and `kaiser_beta`) so wideband consumers
131
+ (codar/superdarn) provision an un-clipped filter rather than the ±audio path.
132
+ - `KQP-F-004` `[DOC]` ✅ SHALL remove channels by setting **frequency = 0**
133
+ (`remove_channel(ssrc)` / `set_frequency(ssrc, 0.0)`) so radiod's poller
134
+ reclaims them; teardown SHALL be the documented contract.
135
+ - `KQP-F-005` `[CODE]` ✅ SHALL derive a **deterministic SSRC** (`allocate_ssrc`)
136
+ and **deterministic multicast IP/destination** (`generate_multicast_ip`,
137
+ radiod-host-aware) from channel parameters so identities survive restarts and
138
+ don't collide.
139
+ - `KQP-F-006` `[DOC]` ✅ SHALL support explicit per-channel `lifetime` (TTL) and
140
+ output `encoding`/`destination` selection.
141
+
142
+ ### 6.2 RTP receive abstractions (four layers)
143
+ - `KQP-F-010` `[DOC]` ✅ SHALL expose **`RTPRecorder`** — raw-packet capture with
144
+ precise GPS/RTP timestamps for timing-critical consumers.
145
+ - `KQP-F-011` `[DOC]` ✅ SHALL expose **`RadiodStream`** — continuous sample
146
+ delivery with gap handling, built on `PacketResequencer` for out-of-order
147
+ packets; SHALL bind to the **channel's multicast group**, not `0.0.0.0`.
148
+ - `KQP-F-012` `[DOC]` ✅ SHALL expose **`ManagedStream`** — self-healing
149
+ single-channel wrapper that recovers across radiod restarts / network drops.
150
+ - `KQP-F-013` `[DOC]` ✅ SHALL expose **`MultiStream`** — one socket per
151
+ multicast group demultiplexing many SSRCs, the substrate every multi-band
152
+ recorder uses (avoids kernel over-subscription from per-channel sockets);
153
+ channels added via `add_channel(...)`, with pruning of dead SSRCs.
154
+ - `KQP-F-014` `[DOC]` ✅ SHALL expose **`SpectrumStream`** for spectrum/`powers`
155
+ consumers.
156
+
157
+ ### 6.3 RTP payload decode
158
+ - `KQP-F-020` `[DOC]` ✅ `parse_rtp_samples()` SHALL decode **every native
159
+ radiod encoding** — S16LE/BE, F32LE/BE, F16LE/BE, MULAW, ALAW — in pure NumPy.
160
+ - `KQP-F-021` `[DOC]` ✅ SHALL decode **OPUS / OPUS_VOIP** via an optional
161
+ `OpusDecoder` (`[opus]` extra), degrading to ImportError-guarded absence when
162
+ not installed (core stays NumPy-only).
163
+ - `KQP-F-022` `[DOC]` ✅ SHALL parse RTP headers (`parse_rtp_header`) and expose
164
+ IQ (`complex64`) vs real sample framing per `OUTPUT_CHANNELS`.
165
+
166
+ ### 6.4 Typed status decode
167
+ - `KQP-F-030` `[DOC]` ✅ SHALL decode radiod TLV status packets
168
+ (`decode_status_packet`) into typed objects — `ChannelStatus`,
169
+ `FrontendStatus`, `PllStatus`, `FmStatus`, `SpectrumStatus`, `Filter2Status`,
170
+ `OpusStatus` — with dotted-path field access, covering all 117 TLV parameters.
171
+ - `KQP-F-031` `[DOC]` ✅ SHALL provide a `StatusListener` that refreshes the
172
+ per-channel timing anchor at sub-second cadence for live consumers.
173
+
174
+ ### 6.5 Timing
175
+ - `KQP-F-040` `[DOC]` ✅ SHALL map RTP timestamps to wallclock
176
+ (`rtp_to_wallclock`) using radiod's **GPS_TIME / RTP_TIMESNAP**, sample-accurate.
177
+ - `KQP-F-041` `[CODE]` ✅ `ChannelInfo` SHALL expose an **atomic anchor pair**
178
+ (`get_anchor`/`update_anchor` on `(gps_time, rtp_timesnap)`) so a live
179
+ `StatusListener` refresh cannot yield a torn anchor to `rtp_to_wallclock`.
180
+
181
+ ### 6.6 Discovery
182
+ - `KQP-F-050` `[DOC]` ✅ SHALL enumerate radiod instances and their active
183
+ channels over the LAN: `discover_channels()` primary, with
184
+ `discover_channels_native()` / `discover_channels_via_control()` fallbacks and
185
+ `discover_radiod_services()`; SHALL accept an explicit `interface`.
186
+ - `KQP-F-051` `[DOC]` ✅ SHALL provide `ChannelMonitor` to detect radiod restarts
187
+ and fire channel-recreation callbacks.
188
+
189
+ ### 6.7 CLI / TUI
190
+ - `KQP-F-060` `[DOC]` ✅ SHALL ship a `ka9q` console entry point: `list / query /
191
+ set / tune / tui` for scripted and interactive control.
192
+ - `KQP-F-061` `[DOC]` ✅ The TUI (`ka9q tui`, `[tui]` extra) SHALL be an additive
193
+ Textual lazy-import; absence of `textual` SHALL NOT break the core/CLI.
194
+
195
+ ### 6.8 Protocol-drift tooling (dev/repo surface, not runtime API)
196
+ - `KQP-F-070` `[DOC]` ✅ `scripts/sync_types.py` SHALL regenerate `ka9q/types.py`
197
+ and the pin files (`ka9q_radio_compat`, `ka9q/compat.py`) from a local
198
+ ka9q-radio checkout, with `--check` (exit 1 on drift) / `--diff` / `--apply`,
199
+ updating the three files **atomically**.
200
+ - `KQP-F-071` `[DOC]` ✅ `scripts/check_upstream_drift.py` SHALL compare the
201
+ *pinned* commit against `origin/main` and classify the delta **pass / warn /
202
+ fail**, where `fail` = a **stream-critical** field removed or its TLV/enum
203
+ value shifted (RTP delivery would break). This is the script sigmond's
204
+ `smd watch ka9q` wraps.
205
+ - `KQP-F-072` `[CODE]` ✅ The stream-critical allowlist SHALL enumerate the
206
+ fields whose change is breaking — `OUTPUT_DATA_DEST_SOCKET`,
207
+ `OUTPUT_DATA_SOURCE_SOCKET`, `OUTPUT_SSRC`, `OUTPUT_TTL`, `OUTPUT_SAMPRATE`,
208
+ `OUTPUT_ENCODING`, `OUTPUT_CHANNELS`, `RTP_PT`, `RTP_TIMESNAP`, `GPS_TIME`,
209
+ `STATUS_INTERVAL`, `RADIO_FREQUENCY`, `PRESET`, `DEMOD_TYPE`, `LIFETIME` — plus
210
+ the all-values-critical enums `Encoding`, `DemodType`. It SHALL live in the
211
+ repo dev tool, **not** the `ka9q/` runtime package.
212
+ - `KQP-F-073` `[DOC]` ✅ `tests/test_protocol_compat.py` SHALL fail on drift when
213
+ `../ka9q-radio` is present and **auto-skip** when it is not (CI without the C
214
+ tree unaffected).
215
+
216
+ ## 7. Quality / non-functional requirements
217
+
218
+ - `KQP-Q-001` `[DOC]` ✅ **API stability:** the public surface re-exported from
219
+ `ka9q/__init__.py` (`__all__`) SHALL be treated as a versioned contract;
220
+ breaking changes SHALL bump the version and be reconciled across the named
221
+ consumers before release (see §8.3).
222
+ - `KQP-Q-002` `[CODE]` ✅ All public `RadiodControl` methods SHALL be
223
+ thread-safe (`RLock`); `ManagedStream`/`MultiStream` SHALL be safe for
224
+ concurrent long-running use.
225
+ - `KQP-Q-003` `[CODE]` ✅ Receive sockets SHALL set **`SO_RCVBUF` = 64 MB** on
226
+ both `RadiodStream` and `MultiStream` to resist GIL-stall packet loss.
227
+ - `KQP-Q-004` `[CODE]` ✅ `ManagedStream` SHALL recover automatically from
228
+ radiod restart and network interruption without consumer intervention.
229
+ - `KQP-Q-005` `[DOC]` ✅ Protocol definitions SHALL be **provably in sync** with
230
+ the pinned ka9q-radio commit (drift detectable by `--check` in CI and by
231
+ `check_upstream_drift.py` against upstream HEAD).
232
+ - `KQP-Q-006` `[CODE]` ✅ The anchor refresh SHALL be **atomic** so timing
233
+ consumers (hf-timestd's tight ±0.5 s gates) never read a torn pair.
234
+ - `KQP-Q-007` `[DOC]` ✅ Multi-homed operability: every receive/discovery/control
235
+ entry point SHALL accept an explicit interface selector.
236
+ - `KQP-Q-008` `[CODE]` ✅ Optional capabilities (OPUS, TUI) SHALL degrade to a
237
+ guarded absence and SHALL NEVER hard-fail the NumPy-only core import.
238
+ - `KQP-Q-009` `[NEW]` 🟡 **Public-API regression guard:** there is no automated
239
+ test asserting `ka9q/__init__.__all__` is stable (no symbol silently dropped/
240
+ renamed). A breaking export change today would only surface in a consumer.
241
+ SHALL add an `__all__` snapshot test. *(gap.)*
242
+
243
+ ## 8. External interfaces
244
+
245
+ > This is a library: §8.1/§8.2 describe the **Python API** it provides and the
246
+ > **radiod wire I/O** it speaks, not files/sinks. §8.3 is the stability +
247
+ > upstream-compat surface in place of a client-contract conformance statement.
248
+
249
+ ### 8.1 Inputs (API consumed by callers; wire consumed from radiod)
250
+ - **From callers:** `RadiodControl(host, interface=…)`; channel params
251
+ (`frequency_hz`, `preset`, `sample_rate`, `encoding`, `low_edge`/`high_edge`,
252
+ `lifetime`, `destination`); stream construction (`MultiStream` + `add_channel`,
253
+ `RadiodStream`, `RTPRecorder(channel=…, on_packet=…)`); discovery
254
+ (`discover_channels(host, interface=…)`).
255
+ - **From radiod (wire):** TLV status/command packets over multicast UDP; RTP
256
+ payload streams (S16/F32/F16/MULAW/ALAW/OPUS) carrying GPS_TIME/RTP_TIMESNAP.
257
+ - **Env:** `RADIOD_HOST` / `RADIOD_ADDRESS` (test/CLI host selection);
258
+ `--radiod-host` pytest option for integration tests.
259
+
260
+ ### 8.2 Outputs (API returned to callers; wire emitted to radiod)
261
+ - **To callers:** decoded samples (`complex64` IQ / real), typed status objects
262
+ (`ChannelStatus`, `FrontendStatus`, …), `ChannelInfo` (SSRC, freq, preset,
263
+ sample_rate, anchor pair), gap events / stream quality, wallclock timestamps,
264
+ discovered-channel maps; typed exceptions (`Ka9qError`, `ConnectionError`,
265
+ `CommandError`, `ValidationError`).
266
+ - **To radiod (wire):** TLV command packets (create/tune/ensure/remove,
267
+ frequency=0 teardown) on the channel's multicast group; deterministic SSRC /
268
+ multicast IP.
269
+ - **CLI stdout:** `ka9q list/query/set/tune` human + scriptable output.
270
+ - **Pins (repo artifacts):** `ka9q_radio_compat` + `ka9q/compat.py`
271
+ (`KA9Q_RADIO_COMMIT`) declaring the validated ka9q-radio commit.
272
+
273
+ ### 8.3 Contract / API stability & upstream-compat surface (reference, not restated)
274
+
275
+ > **The HamSCI client contract does NOT apply to ka9q-python.** It is a library,
276
+ > not a client: no `inventory --json`, no `validate --json`, no `deploy.toml`,
277
+ > no systemd unit, no shared-sink writes. It is not lifecycle-managed by sigmond
278
+ > and does not self-describe to the contract adapter. The two interfaces it DOES
279
+ > own are below.
280
+
281
+ - `KQP-I-001` `[CODE]` ✅ **Public Python API contract.** The stable surface is
282
+ the `ka9q/__init__.__all__` re-exports — Control (`RadiodControl`,
283
+ `allocate_ssrc`); Discovery (`discover_channels*`, `ChannelInfo`); Streams
284
+ (`RTPRecorder`, `RadiodStream`, `ManagedStream`, `MultiStream`,
285
+ `SpectrumStream`); Decode (`parse_rtp_samples`, `decode_status_packet`, the
286
+ `*Status` types); Types (`StatusType`, `Encoding`, `DemodType`, `WindowType`);
287
+ Exceptions; Utilities (`generate_multicast_ip`, `ChannelMonitor`,
288
+ `rtp_to_wallclock`). The eight named consumers depend on these symbols and on
289
+ `from ka9q.types import StatusType, Encoding` (enum **names and values**).
290
+ Breaking changes are coordinated with those consumers (§10) and versioned.
291
+ - `KQP-I-002` `[DOC]` ✅ **radiod wire-protocol compat surface.** The library is
292
+ pinned to ka9q-radio commit `9b742e60` (`ka9q_radio_compat` /
293
+ `KA9Q_RADIO_COMMIT`); `types.py` is generated from that commit's `status.h`/
294
+ `rtp.h`. The stream-critical field set (`KQP-F-072`) defines what a wire change
295
+ must not silently break: the **fail** classification means RTP delivery to the
296
+ whole suite would break if the deployed radiod advanced past a value shift
297
+ without a coordinated ka9q-python regen. Upstream tracking is sigmond
298
+ `smd watch ka9q` → `check_upstream_drift.py`.
299
+ - `KQP-I-003` `[DOC]` ✅ **sigmond seam.** sigmond consumes ka9q-python only
300
+ (a) transitively, through clients that import it (editable sibling install per
301
+ the fleet-upgrade pattern), and (b) as the drift watcher wrapper. There is no
302
+ direct runtime import of `ka9q` by `smd` core (which is stdlib-only).
303
+
304
+ ## 9. Data requirements
305
+
306
+ ka9q-python is **stateless and persists nothing** — no database, no on-disk
307
+ products, no retention. Its in-flight data structures: `ChannelInfo` (SSRC,
308
+ frequency, preset, sample_rate, encoding, atomic `(gps_time, rtp_timesnap)`
309
+ anchor); the typed `*Status` decode objects (117 TLV fields, dotted-path);
310
+ `complex64` IQ / real sample buffers; `RTPHeader` / `RTPPacket` /
311
+ `ResequencerStats` / `StreamQuality` / `GapEvent` runtime telemetry. The only
312
+ durable artifacts are the **dev pins** (`ka9q_radio_compat`, `compat.py`) and
313
+ the **generated** `ka9q/types.py` — provenance-labeled with the ka9q-radio
314
+ commit they were validated against. Wire timing provenance (GPS_TIME/
315
+ RTP_TIMESNAP) is passed through, never stored.
316
+
317
+ ## 10. Dependencies & development sequence
318
+
319
+ **Runtime deps:** `numpy>=1.24.0` (only hard dep). **Optional extras:** `tui`
320
+ (`textual>=0.50`), `opus` (`opuslib>=3.0`), `dev` (`pytest`, `pytest-cov`).
321
+ **External peer:** a running `radiod` (ka9q-radio) at the pinned commit; the
322
+ ka9q-radio C source tree at `../ka9q-radio` enables `sync_types.py` regen and
323
+ the drift test (both auto-skip without it).
324
+
325
+ **Must exist first:** ka9q-radio (radiod + headers) — ka9q-python is the binding
326
+ to it, so it cannot precede it. Everything downstream (`hamsci-dsp`, all eight
327
+ RF clients) depends on this library, so it sits at the **base** of the suite
328
+ dependency graph; a breaking change here is the highest-blast-radius change in
329
+ the suite.
330
+
331
+ **Development sequence (intended, recovered as requirement):**
332
+ 1. **Control + decode core** — `RadiodControl` TLV path, `parse_rtp_samples`,
333
+ typed status decode, generated `types.py` + the compat pin.
334
+ 2. **Stream abstraction ladder** — `RTPRecorder` → `RadiodStream`
335
+ (+ resequencer) → `ManagedStream` (self-heal) → `MultiStream`
336
+ (shared-socket multi-SSRC, the recorder substrate).
337
+ 3. **Timing hardening** — GPS_TIME/RTP_TIMESNAP `rtp_to_wallclock`, then the
338
+ `StatusListener` sub-second anchor refresh + atomic anchor pair (3.16.x).
339
+ 4. **Drift defense** — `sync_types.py` + `check_upstream_drift.py` +
340
+ `test_protocol_compat.py`, wired into sigmond as `smd watch ka9q`.
341
+ 5. **Ergonomics** — discovery, CLI, TUI, multi-homed selection, `[opus]` decode.
342
+ 6. **Performance** — 64 MB `SO_RCVBUF` on both stream paths; multicast-group
343
+ bind fix (g10f7e47).
344
+
345
+ Ongoing maintenance cadence: when ka9q-radio advances, run the watcher; regen
346
+ on green/yellow; coordinate consumers on red (§KQP-F-071/072).
347
+
348
+ ## 11. Acceptance criteria & verification
349
+
350
+ - **Protocol sync** → `python scripts/sync_types.py --check` exits 0 against
351
+ `../ka9q-radio`; `tests/test_protocol_compat.py` passes (or skips absent the
352
+ tree). `check_upstream_drift.py` classification surfaced via `smd watch ka9q`.
353
+ - **Stream correctness** → the unit suite (39 files, ~375 cases): resequencer,
354
+ multistream prune, ensure-channel encoding, SSRC/destination derivation,
355
+ RTP-sample parse (incl. IQ 20 kHz F32), managed-stream recovery, timing fields.
356
+ - **Live integration** → `uv run pytest --radiod-host=<host>` against a real
357
+ radiod (e.g. `bee1-hf-status.local`).
358
+ - **API stability** → `KQP-Q-001` is today verified only by downstream breakage;
359
+ acceptance is the proposed `__all__` snapshot test (`KQP-Q-009`).
360
+ - **Decode coverage** → per-encoding parse tests for S16/F32/F16/MULAW/ALAW;
361
+ OPUS path gated on `[opus]`.
362
+ - **Real-world acceptance** → in production under all suite recorders; a clean
363
+ RTP receive (no USB/packet drops attributable to the binding) is the standing
364
+ field check.
365
+
366
+ ## 12. Risks & open questions
367
+
368
+ - `KQP-Q-009` `[NEW]` 🟡 **No `__all__` regression guard** — the API contract
369
+ (§8.3 `KQP-I-001`) every client depends on has no automated stability test; a
370
+ dropped/renamed export ships silently. *(candidate #18 issue.)*
371
+ - `KQP-D-001` `[NEW]` ⬜ **No machine-readable consumer-compat matrix.** Which
372
+ ka9q-python version each client requires lives only in eight separate
373
+ `uv.lock`/pyproject pins; there is no single declared "client X needs API ≥N"
374
+ map, so a breaking bump's blast radius must be reasoned out by hand. SHALL
375
+ publish a compat matrix (or a contract-style version floor per consumer).
376
+ - `KQP-F-074` `[NEW]` ⬜ **Drift watcher is operator-triggered only.** No
377
+ scheduler runs `check_upstream_drift.py`; a stream-critical upstream change can
378
+ sit undetected until someone reruns it before a deploy. SHALL either schedule
379
+ it (sigmond timer) or document the manual-before-deploy gate as the accepted
380
+ control.
381
+ - `KQP-Q-010` `[NEW]` ⬜ **Beta classifier vs production reality.** pyproject
382
+ declares `Development Status :: 4 - Beta` though the library is the production
383
+ substrate for the whole suite; either promote to `5 - Production/Stable` or
384
+ document why it's held at Beta (API still mutating).
385
+ - **Doc/code surface drift:** README/CLAUDE list `RadiodControl` and the four
386
+ stream layers consistently, but the API-surface ground truth is
387
+ `__init__.__all__`; keep `docs/API_REFERENCE.md` reconciled against it (no
388
+ enforced check today).
389
+ - **Pin policy clarity:** `ka9q_radio_compat` (`9b742e60`) is the *validated*
390
+ commit; the deployed radiod commit is sigmond/operator-controlled. The
391
+ library cannot enforce that the running radiod matches its pin — it can only
392
+ detect upstream drift. This boundary SHALL stay explicit (§KQP-I-002/003).
393
+
394
+ ## 13. Traceability
395
+
396
+ | Requirement | #18 issue | Verification | PSWS #6 |
397
+ |---|---|---|---|
398
+ | KQP-I-001 (public API contract) | Clients: ka9q-python API stability | downstream import + (proposed) `__all__` test | #6:30 |
399
+ | KQP-I-002 (radiod wire compat / pin) | ka9q-watch | `sync_types --check`, `test_protocol_compat` | #6:30 |
400
+ | KQP-F-071/072 (drift classification) | smd watch ka9q | `check_upstream_drift.py` pass/warn/fail | #6:30 |
401
+ | KQP-F-013 (MultiStream substrate) | — | `test_multistream_prune`, recorder field use | #6:31 (sensor integ.) |
402
+ | KQP-F-040/041 (RTP↔wallclock, atomic anchor) | Clients: hf-timestd timing | timing-fields test; 3.16.1 anchor tests | #6:50 (timing tiering) |
403
+ | KQP-Q-009 (`__all__` guard) | *(new — file)* | snapshot test | — |
404
+ | KQP-D-001 (consumer-compat matrix) | *(new — file)* | published matrix | #6:30 |
405
+ | KQP-F-074 (scheduled drift watch) | *(new — file)* | sigmond timer / documented gate | #6:30 |
406
+ | KQP-Q-010 (Beta→Stable classifier) | *(new — file)* | pyproject review | — |
407
+
408
+ *New rows (KQP-Q-009, KQP-D-001, KQP-F-074, KQP-Q-010) are this review's surfaced
409
+ gaps; promote to #18 under the ka9q-python / PSWS #6:30 interface epic.*