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.
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/CHANGELOG.md +95 -0
- {ka9q_python-3.16.1/ka9q_python.egg-info → ka9q_python-3.18.0}/PKG-INFO +7 -12
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/README.md +1 -1
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/GETTING_STARTED.md +2 -2
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/INSTALLATION.md +5 -5
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/MULTI_STREAM.md +3 -3
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/RECIPES.md +3 -3
- ka9q_python-3.18.0/docs/REQUIREMENTS.md +409 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/__init__.py +9 -1
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/compat.py +1 -1
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/control.py +145 -59
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/discovery.py +95 -9
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/multi_stream.py +88 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/rtp_recorder.py +17 -2
- ka9q_python-3.18.0/ka9q/slot_clock.py +258 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/stream.py +82 -29
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/types.py +1 -1
- {ka9q_python-3.16.1 → ka9q_python-3.18.0/ka9q_python.egg-info}/PKG-INFO +7 -12
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/SOURCES.txt +5 -1
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_radio_compat +1 -1
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/pyproject.toml +6 -6
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/conftest.py +4 -3
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_channel_verification.py +134 -13
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_client_id_destination.py +16 -16
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ensure_channel_encoding.py +10 -8
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_filter_edges.py +17 -20
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_iq_20khz_f32.py +13 -7
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_listen_multicast.py +13 -5
- ka9q_python-3.18.0/tests/test_multistream_gap_storm.py +57 -0
- ka9q_python-3.18.0/tests/test_multistream_prune.py +84 -0
- ka9q_python-3.18.0/tests/test_slot_clock.py +145 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_live.py +13 -3
- ka9q_python-3.16.1/setup.py +0 -47
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/LICENSE +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/MANIFEST.in +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/API_REFERENCE.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/ARCHITECTURE.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/CLI_GUIDE.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/RTP_TIMING_SUPPORT.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/SECURITY.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/TESTING_GUIDE.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/docs/TUI_GUIDE.md +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/diagnostics/diagnose_packets.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/diagnostics/repro_utc_bug.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/discover_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/multi_stream_smoke.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/spectrum_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/stream_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/test_improvements.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/tune.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/examples/tune_example.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/_multicast.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/addressing.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/cli.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/monitor.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/pps_calibrator.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/spectrum_stream.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/status.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/status_listener.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/tui.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q/utils.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/entry_points.txt +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/scripts/check_upstream_drift.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/scripts/sync_types.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/setup.cfg +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/__init__.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_addressing.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_decode_description.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_integration.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_lifetime.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_monitor.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_multicast_helpers.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_parse_rtp_samples_iq.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_protocol_compat.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_security_features.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_spectrum.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ssrc_radiod_host_unit.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_status_decoder.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_status_listener.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_ttl_warning.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.16.1 → ka9q_python-3.18.0}/tests/test_tune_method.py +0 -0
- {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.
|
|
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/
|
|
10
|
-
Project-URL: Documentation, https://github.com/
|
|
11
|
-
Project-URL: Repository, https://github.com/
|
|
12
|
-
Project-URL: Issues, https://github.com/
|
|
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 ::
|
|
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/
|
|
93
|
+
git clone https://github.com/HamSCI/ka9q-python.git
|
|
99
94
|
cd ka9q-python
|
|
100
95
|
pip install -e .
|
|
101
96
|
```
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
44
|
-
[hf-timestd](https://github.com/
|
|
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/
|
|
134
|
-
[psk-recorder](https://github.com/
|
|
135
|
-
[hf-timestd](https://github.com/
|
|
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.*
|