ka9q-python 3.20.0__tar.gz → 3.22.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.20.0 → ka9q_python-3.22.0}/CHANGELOG.md +80 -0
- {ka9q_python-3.20.0/ka9q_python.egg-info → ka9q_python-3.22.0}/PKG-INFO +50 -6
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/README.md +49 -5
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/GETTING_STARTED.md +5 -5
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/RECIPES.md +4 -4
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/REQUIREMENTS.md +6 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/README.md +52 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/clients.md +631 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/contract.md +434 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/control-surface.md +334 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/findings.md +628 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/idempotency.md +974 -0
- ka9q_python-3.22.0/docs/audit/2026-08-12-alignment/upstream.md +328 -0
- ka9q_python-3.22.0/docs/superpowers/plans/2026-08-12-upstream-alignment-and-client-compat.md +937 -0
- ka9q_python-3.22.0/docs/superpowers/plans/2026-08-13-audit-remediation.md +1590 -0
- ka9q_python-3.22.0/docs/superpowers/specs/2026-08-12-upstream-alignment-and-client-compat-design.md +155 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/channel_cleanup_example.py +6 -6
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/codar_oceanography.py +1 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/hf_band_scanner.py +1 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/simple_am_radio.py +1 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/superdarn_recorder.py +1 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/test_improvements.py +7 -7
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/__init__.py +3 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/compat.py +1 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/control.py +367 -88
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/multi_stream.py +138 -2
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/resequencer.py +11 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/spectrum_stream.py +31 -23
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/status.py +8 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/status_listener.py +56 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/stream.py +26 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/stream_quality.py +19 -21
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/types.py +5 -3
- {ka9q_python-3.20.0 → ka9q_python-3.22.0/ka9q_python.egg-info}/PKG-INFO +50 -6
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q_python.egg-info/SOURCES.txt +22 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q_radio_compat +1 -1
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/pyproject.toml +1 -1
- ka9q_python-3.22.0/scripts/gen_client_manifest.py +140 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/scripts/sync_types.py +1 -1
- ka9q_python-3.22.0/tests/test_client_contract.py +56 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_client_id_destination.py +11 -6
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_create_split_encoding.py +1 -1
- ka9q_python-3.22.0/tests/test_delivered_rtp_start.py +120 -0
- ka9q_python-3.22.0/tests/test_destination_required.py +133 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_ensure_channel_encoding.py +4 -2
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_filter_edges.py +6 -3
- ka9q_python-3.22.0/tests/test_gen_client_manifest.py +75 -0
- ka9q_python-3.22.0/tests/test_idempotency_integration.py +184 -0
- ka9q_python-3.22.0/tests/test_keepalive_encoding.py +169 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_lifetime.py +2 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_multicast_helpers.py +9 -0
- ka9q_python-3.22.0/tests/test_multistream_restore.py +90 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_remove_channel.py +3 -2
- ka9q_python-3.22.0/tests/test_set_demod_type.py +46 -0
- ka9q_python-3.22.0/tests/test_set_lock_warns.py +33 -0
- ka9q_python-3.22.0/tests/test_set_spectrum.py +128 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_status_decoder.py +18 -0
- ka9q_python-3.22.0/tests/test_sync_types.py +40 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/LICENSE +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/MANIFEST.in +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/API_REFERENCE.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/ARCHITECTURE.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/CLI_GUIDE.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/INSTALLATION.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/MULTI_STREAM.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/RTP_TIMING_SUPPORT.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/SECURITY.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/TESTING_GUIDE.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/docs/TUI_GUIDE.md +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/diagnostics/diagnose_packets.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/diagnostics/repro_utc_bug.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/discover_example.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/multi_stream_smoke.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/spectrum_example.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/stream_example.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/tune.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/examples/tune_example.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/_multicast.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/addressing.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/cli.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/discovery.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/monitor.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/pps_calibrator.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/slot_clock.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/tui.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q/utils.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q_python.egg-info/entry_points.txt +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/scripts/check_upstream_drift.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/setup.cfg +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/__init__.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/conftest.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_addressing.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_channel_verification.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_decode_description.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_integration.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_monitor.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_multistream_gap_storm.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_multistream_prune.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_parse_rtp_samples_iq.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_protocol_compat.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_security_features.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_slot_clock.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_spectrum.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_ssrc_radiod_host_unit.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_status_listener.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_ttl_warning.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_tune.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_tune_live.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_tune_method.py +0 -0
- {ka9q_python-3.20.0 → ka9q_python-3.22.0}/tests/test_upstream_drift.py +0 -0
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.22.0 (2026-08-13)
|
|
4
|
+
|
|
5
|
+
Audit remediation follow-up (findings F1, F3-F8, F10-F14 from the
|
|
6
|
+
2026-08 upstream-alignment audit; see `docs/audit/` and
|
|
7
|
+
`.superpowers/sdd/2026-08-13-audit-remediation/`). Minor bump: adds
|
|
8
|
+
public capability and one deliberate, owner-ruled behavior change.
|
|
9
|
+
|
|
10
|
+
### Changed (behavior)
|
|
11
|
+
|
|
12
|
+
- **`create_channel()` now derives a per-client multicast destination
|
|
13
|
+
from `client_id` when `destination=` is not given, and raises
|
|
14
|
+
`ValidationError` when *neither* is available** (F5). Previously a
|
|
15
|
+
bare `create_channel()` call with no `client_id` and no
|
|
16
|
+
`destination=` omitted the `OUTPUT_DATA_DEST_SOCKET` TLV entirely,
|
|
17
|
+
so radiod created no channel at all, rather than failing loudly on
|
|
18
|
+
the client side. Any caller that relied on that silent no-op must
|
|
19
|
+
now pass `client_id=` (recommended) or an explicit `destination=`.
|
|
20
|
+
One deliberate side effect: for direct `create_channel()` calls that
|
|
21
|
+
set `client_id`, omit `destination=`, and let the SSRC be
|
|
22
|
+
auto-allocated, the newly derived destination now participates in
|
|
23
|
+
the SSRC hash — SSRCs for that specific path differ from what they
|
|
24
|
+
were pre-3.22.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`set_spectrum()`** gains `window_type`, `avg`, `overlap`, `base`,
|
|
29
|
+
`step` parameters (plus `demod_type`/`frequency_hz`); `SpectrumStream`
|
|
30
|
+
delegates to it (F11).
|
|
31
|
+
- **`ChannelStatus.options`** decodes the `SETOPTS` TLV (F12).
|
|
32
|
+
- **`DemodType.INVALID_DEMOD = -1`** now parses correctly from
|
|
33
|
+
upstream headers (negative enum literals were previously dropped by
|
|
34
|
+
`sync_types.py`) (F10); `set_demod_type()`'s bound now derives from
|
|
35
|
+
`DemodType.N_DEMOD` so `IDLE_DEMOD` is no longer wrongly rejected
|
|
36
|
+
(F13).
|
|
37
|
+
- **`resolve_multicast_address` re-exported from `ka9q`** (F14).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`MultiStream` re-sends `agc`/`gain`/filter-edges/requested-encoding**
|
|
42
|
+
on channel re-creation after a radiod restart — these fields were
|
|
43
|
+
never stored on the `MultiStream` side, so re-creation could not
|
|
44
|
+
re-assert them; this was an origin bug, not a regression (F1, P0).
|
|
45
|
+
- **`tune()` records `_requested_encoding`** so keepalives re-assert
|
|
46
|
+
the caller's chosen encoding instead of drifting back to the
|
|
47
|
+
channel's current one (F4).
|
|
48
|
+
- **`set_lock()` warns** that radiod silently discards `LOCK` rather
|
|
49
|
+
than failing (F6).
|
|
50
|
+
|
|
51
|
+
### Docs
|
|
52
|
+
|
|
53
|
+
- Corrected re-create/lifetime semantics (delta-update only; polls
|
|
54
|
+
don't extend lifetime; units are frames, not seconds) (F3, F7).
|
|
55
|
+
- Corrected a stale pre-3.14 fallback claim in the `client_id`
|
|
56
|
+
docstring (F5 follow-up).
|
|
57
|
+
- README Troubleshooting: a `lo`-route silently swallows control
|
|
58
|
+
commands; use `interface=` (F8).
|
|
59
|
+
|
|
60
|
+
## 3.21.1 (2026-08-11)
|
|
61
|
+
|
|
62
|
+
- **Fix: `StreamQuality.copy()` dropped `delivered_rtp_start`** — the
|
|
63
|
+
hand-maintained field list predated the new field, so every
|
|
64
|
+
RadiodStream callback received `None` (MultiStream, which passes its
|
|
65
|
+
quality object directly, was unaffected). `copy()` now uses
|
|
66
|
+
`dataclasses.replace`, carrying every present and future field; a
|
|
67
|
+
fields-iteration regression test guards the class.
|
|
68
|
+
|
|
69
|
+
## 3.21.0 (2026-08-11)
|
|
70
|
+
|
|
71
|
+
- **`StreamQuality.delivered_rtp_start`** — the true RTP timestamp of the
|
|
72
|
+
FIRST sample of each delivered batch, from the resequencer's emission
|
|
73
|
+
tracking (`PacketResequencer.last_chunk_rtp_start`; zero fills
|
|
74
|
+
included). Both `RadiodStream` and `MultiStream` populate it.
|
|
75
|
+
Consumers that label samples MUST prefer this over
|
|
76
|
+
`last_rtp_timestamp`: the latter is the last RECEIVED packet's header
|
|
77
|
+
(pre-resequencer) and desynchronizes from delivered samples under
|
|
78
|
+
loss — steady-state ±1-packet label wobble, and block-scale label
|
|
79
|
+
slips during stall-and-catchup (root cause of hf-timestd's T6 origin
|
|
80
|
+
instability; see hf-timestd docs/T6-BLOCK-SLIP-ROOT-CAUSE-2026-08-10).
|
|
81
|
+
`None` when nothing has been delivered yet.
|
|
82
|
+
|
|
3
83
|
## [3.20.0] - 2026-06-29
|
|
4
84
|
|
|
5
85
|
### Added — `SlotClock` ambiguous-unwrap guard (fail loud, never silent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ka9q-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.22.0
|
|
4
4
|
Summary: Python interface for ka9q-radio control and monitoring
|
|
5
5
|
Author-email: Michael Hauan AC0G <ac0g@hauan.org>
|
|
6
6
|
License: MIT
|
|
@@ -53,6 +53,7 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
53
53
|
- [Documentation](#documentation)
|
|
54
54
|
- [Examples](#examples)
|
|
55
55
|
- [Use Cases](#use-cases)
|
|
56
|
+
- [Troubleshooting](#troubleshooting)
|
|
56
57
|
- [License](#license)
|
|
57
58
|
|
|
58
59
|
## Features
|
|
@@ -105,7 +106,7 @@ pip install -e .
|
|
|
105
106
|
from ka9q import RadiodControl
|
|
106
107
|
|
|
107
108
|
# Connect to radiod (default test host: bee1-hf-status.local)
|
|
108
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
109
|
+
control = RadiodControl("bee1-hf-status.local", client_id="am-demo")
|
|
109
110
|
|
|
110
111
|
# Create AM channel on 10 MHz WWV
|
|
111
112
|
control.create_channel(
|
|
@@ -123,7 +124,7 @@ control.create_channel(
|
|
|
123
124
|
```python
|
|
124
125
|
from ka9q import RadiodControl, Encoding
|
|
125
126
|
|
|
126
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
127
|
+
control = RadiodControl("bee1-hf-status.local", client_id="hq-encoding-demo")
|
|
127
128
|
|
|
128
129
|
# Create a channel with 32-bit float output (highest quality)
|
|
129
130
|
control.ensure_channel(
|
|
@@ -139,7 +140,7 @@ control.ensure_channel(
|
|
|
139
140
|
```python
|
|
140
141
|
from ka9q import RadiodControl
|
|
141
142
|
|
|
142
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
143
|
+
control = RadiodControl("bee1-hf-status.local", client_id="wspr-demo")
|
|
143
144
|
|
|
144
145
|
wspr_bands = [
|
|
145
146
|
(1.8366e6, "160m"),
|
|
@@ -216,7 +217,7 @@ ensure your channels survive radiod restarts:
|
|
|
216
217
|
```python
|
|
217
218
|
from ka9q import RadiodControl, ChannelMonitor
|
|
218
219
|
|
|
219
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
220
|
+
control = RadiodControl("bee1-hf-status.local", client_id="monitor-demo")
|
|
220
221
|
monitor = ChannelMonitor(control)
|
|
221
222
|
monitor.start()
|
|
222
223
|
|
|
@@ -233,7 +234,7 @@ monitor.monitor_channel(
|
|
|
233
234
|
`radiod` removes channels by polling for streams whose frequency is set to `0 Hz`. Always call `remove_channel(ssrc)` (or explicitly set `set_frequency(ssrc, 0.0)` if you build TLVs yourself) when tearing down a stream so the background poller can reclaim it:
|
|
234
235
|
|
|
235
236
|
```python
|
|
236
|
-
with RadiodControl("bee1-hf-status.local") as control:
|
|
237
|
+
with RadiodControl("bee1-hf-status.local", client_id="cleanup-demo") as control:
|
|
237
238
|
info = control.ensure_channel(
|
|
238
239
|
frequency_hz=10e6,
|
|
239
240
|
preset="iq",
|
|
@@ -339,6 +340,49 @@ See [docs/RECIPES.md](docs/RECIPES.md) for worked examples of:
|
|
|
339
340
|
- **Nimble channel switching** — single-channel SWL-style retuning driven from the CLI or an app
|
|
340
341
|
- **SDR portability** — ka9q-python talks to `radiod`, which talks to the SDR; reporting frontend capabilities via `FrontendStatus`. Primary tested frontend is the RX888; AirspyR2 and Airspy HF+ support is in development.
|
|
341
342
|
|
|
343
|
+
## Troubleshooting
|
|
344
|
+
|
|
345
|
+
### Created channels never appear (silent write-path failure)
|
|
346
|
+
|
|
347
|
+
**Symptom:** `discover_channels()` works — you can see the radiod's
|
|
348
|
+
existing channels — but every `create_channel()` / `ensure_channel()`
|
|
349
|
+
you issue "succeeds" (no exception from the send), and then the channel
|
|
350
|
+
never shows up. The only visible error is a later
|
|
351
|
+
`TimeoutError: Channel SSRC ... not verified within ...s` or a
|
|
352
|
+
`poll_channel()` that returns `None` — neither of which names the cause.
|
|
353
|
+
|
|
354
|
+
**Cause:** some hosts carry a kernel route that sends **all
|
|
355
|
+
locally-originated multicast to loopback** (e.g. a `239.0.0.0/8 dev lo`
|
|
356
|
+
entry). Inbound multicast still arrives on the real NIC — so discovery
|
|
357
|
+
(the read path) looks healthy — while every outbound control command is
|
|
358
|
+
routed to `lo` and never reaches radiod. There is no socket error: the
|
|
359
|
+
packet is delivered, just to the wrong interface. This was
|
|
360
|
+
tcpdump-confirmed during the 2026-08-12 audit: `tcpdump -i <nic>` saw
|
|
361
|
+
zero command packets while `tcpdump -i lo` captured all of them
|
|
362
|
+
(sourced from 127.0.0.1).
|
|
363
|
+
|
|
364
|
+
**Check:**
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Substitute the group your radiod status DNS name resolves to:
|
|
368
|
+
ip route get 239.205.73.40
|
|
369
|
+
# BAD: ... dev lo src ... <- commands never leave this host
|
|
370
|
+
# GOOD: ... dev eth0 src ...
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Remedy:** pass your NIC's IP as `interface=` so ka9q-python sets
|
|
374
|
+
`IP_MULTICAST_IF` on the send socket, overriding the kernel route:
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
control = RadiodControl("bee1-status.local", interface="192.168.1.176")
|
|
378
|
+
channels = discover_channels("bee1-status.local", interface="192.168.1.176")
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Rule of thumb:** a working `discover_channels()` proves only the
|
|
382
|
+
*read* path. If reads work but writes vanish, check `ip route get`
|
|
383
|
+
first — send-side ACKs don't exist in this protocol, so routing
|
|
384
|
+
misconfiguration is otherwise invisible.
|
|
385
|
+
|
|
342
386
|
## License
|
|
343
387
|
|
|
344
388
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -19,6 +19,7 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
19
19
|
- [Documentation](#documentation)
|
|
20
20
|
- [Examples](#examples)
|
|
21
21
|
- [Use Cases](#use-cases)
|
|
22
|
+
- [Troubleshooting](#troubleshooting)
|
|
22
23
|
- [License](#license)
|
|
23
24
|
|
|
24
25
|
## Features
|
|
@@ -71,7 +72,7 @@ pip install -e .
|
|
|
71
72
|
from ka9q import RadiodControl
|
|
72
73
|
|
|
73
74
|
# Connect to radiod (default test host: bee1-hf-status.local)
|
|
74
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
75
|
+
control = RadiodControl("bee1-hf-status.local", client_id="am-demo")
|
|
75
76
|
|
|
76
77
|
# Create AM channel on 10 MHz WWV
|
|
77
78
|
control.create_channel(
|
|
@@ -89,7 +90,7 @@ control.create_channel(
|
|
|
89
90
|
```python
|
|
90
91
|
from ka9q import RadiodControl, Encoding
|
|
91
92
|
|
|
92
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
93
|
+
control = RadiodControl("bee1-hf-status.local", client_id="hq-encoding-demo")
|
|
93
94
|
|
|
94
95
|
# Create a channel with 32-bit float output (highest quality)
|
|
95
96
|
control.ensure_channel(
|
|
@@ -105,7 +106,7 @@ control.ensure_channel(
|
|
|
105
106
|
```python
|
|
106
107
|
from ka9q import RadiodControl
|
|
107
108
|
|
|
108
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
109
|
+
control = RadiodControl("bee1-hf-status.local", client_id="wspr-demo")
|
|
109
110
|
|
|
110
111
|
wspr_bands = [
|
|
111
112
|
(1.8366e6, "160m"),
|
|
@@ -182,7 +183,7 @@ ensure your channels survive radiod restarts:
|
|
|
182
183
|
```python
|
|
183
184
|
from ka9q import RadiodControl, ChannelMonitor
|
|
184
185
|
|
|
185
|
-
control = RadiodControl("bee1-hf-status.local")
|
|
186
|
+
control = RadiodControl("bee1-hf-status.local", client_id="monitor-demo")
|
|
186
187
|
monitor = ChannelMonitor(control)
|
|
187
188
|
monitor.start()
|
|
188
189
|
|
|
@@ -199,7 +200,7 @@ monitor.monitor_channel(
|
|
|
199
200
|
`radiod` removes channels by polling for streams whose frequency is set to `0 Hz`. Always call `remove_channel(ssrc)` (or explicitly set `set_frequency(ssrc, 0.0)` if you build TLVs yourself) when tearing down a stream so the background poller can reclaim it:
|
|
200
201
|
|
|
201
202
|
```python
|
|
202
|
-
with RadiodControl("bee1-hf-status.local") as control:
|
|
203
|
+
with RadiodControl("bee1-hf-status.local", client_id="cleanup-demo") as control:
|
|
203
204
|
info = control.ensure_channel(
|
|
204
205
|
frequency_hz=10e6,
|
|
205
206
|
preset="iq",
|
|
@@ -305,6 +306,49 @@ See [docs/RECIPES.md](docs/RECIPES.md) for worked examples of:
|
|
|
305
306
|
- **Nimble channel switching** — single-channel SWL-style retuning driven from the CLI or an app
|
|
306
307
|
- **SDR portability** — ka9q-python talks to `radiod`, which talks to the SDR; reporting frontend capabilities via `FrontendStatus`. Primary tested frontend is the RX888; AirspyR2 and Airspy HF+ support is in development.
|
|
307
308
|
|
|
309
|
+
## Troubleshooting
|
|
310
|
+
|
|
311
|
+
### Created channels never appear (silent write-path failure)
|
|
312
|
+
|
|
313
|
+
**Symptom:** `discover_channels()` works — you can see the radiod's
|
|
314
|
+
existing channels — but every `create_channel()` / `ensure_channel()`
|
|
315
|
+
you issue "succeeds" (no exception from the send), and then the channel
|
|
316
|
+
never shows up. The only visible error is a later
|
|
317
|
+
`TimeoutError: Channel SSRC ... not verified within ...s` or a
|
|
318
|
+
`poll_channel()` that returns `None` — neither of which names the cause.
|
|
319
|
+
|
|
320
|
+
**Cause:** some hosts carry a kernel route that sends **all
|
|
321
|
+
locally-originated multicast to loopback** (e.g. a `239.0.0.0/8 dev lo`
|
|
322
|
+
entry). Inbound multicast still arrives on the real NIC — so discovery
|
|
323
|
+
(the read path) looks healthy — while every outbound control command is
|
|
324
|
+
routed to `lo` and never reaches radiod. There is no socket error: the
|
|
325
|
+
packet is delivered, just to the wrong interface. This was
|
|
326
|
+
tcpdump-confirmed during the 2026-08-12 audit: `tcpdump -i <nic>` saw
|
|
327
|
+
zero command packets while `tcpdump -i lo` captured all of them
|
|
328
|
+
(sourced from 127.0.0.1).
|
|
329
|
+
|
|
330
|
+
**Check:**
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# Substitute the group your radiod status DNS name resolves to:
|
|
334
|
+
ip route get 239.205.73.40
|
|
335
|
+
# BAD: ... dev lo src ... <- commands never leave this host
|
|
336
|
+
# GOOD: ... dev eth0 src ...
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Remedy:** pass your NIC's IP as `interface=` so ka9q-python sets
|
|
340
|
+
`IP_MULTICAST_IF` on the send socket, overriding the kernel route:
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
control = RadiodControl("bee1-status.local", interface="192.168.1.176")
|
|
344
|
+
channels = discover_channels("bee1-status.local", interface="192.168.1.176")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Rule of thumb:** a working `discover_channels()` proves only the
|
|
348
|
+
*read* path. If reads work but writes vanish, check `ip route get`
|
|
349
|
+
first — send-side ACKs don't exist in this protocol, so routing
|
|
350
|
+
misconfiguration is otherwise invisible.
|
|
351
|
+
|
|
308
352
|
## License
|
|
309
353
|
|
|
310
354
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -47,13 +47,13 @@ The `RadiodControl` class is the primary interface for sending commands to `radi
|
|
|
47
47
|
### Step 2: Connect to radiod
|
|
48
48
|
|
|
49
49
|
```python
|
|
50
|
-
control = RadiodControl("radiod.local")
|
|
50
|
+
control = RadiodControl("radiod.local", client_id="getting-started")
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Replace `"radiod.local"` with the mDNS name or IP address of your `radiod` instance. If you're on a multi-homed system (multiple network interfaces), you may need to specify which interface to use:
|
|
53
|
+
Replace `"radiod.local"` with the mDNS name or IP address of your `radiod` instance. `client_id` identifies this application so ka9q-python can derive a per-client multicast destination for the channels you create. If you're on a multi-homed system (multiple network interfaces), you may need to specify which interface to use:
|
|
54
54
|
|
|
55
55
|
```python
|
|
56
|
-
control = RadiodControl("radiod.local", interface="192.168.1.100")
|
|
56
|
+
control = RadiodControl("radiod.local", client_id="getting-started", interface="192.168.1.100")
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
### Step 3: Create a Channel
|
|
@@ -89,7 +89,7 @@ Here's the full program:
|
|
|
89
89
|
from ka9q import RadiodControl
|
|
90
90
|
|
|
91
91
|
# Connect to radiod
|
|
92
|
-
control = RadiodControl("radiod.local")
|
|
92
|
+
control = RadiodControl("radiod.local", client_id="getting-started")
|
|
93
93
|
|
|
94
94
|
# Create AM channel for WWV 10 MHz
|
|
95
95
|
control.create_channel(
|
|
@@ -194,7 +194,7 @@ def on_stream_restored(channel):
|
|
|
194
194
|
print(f"✓ Stream restored: {channel.frequency/1e6:.3f} MHz")
|
|
195
195
|
|
|
196
196
|
# Connect to radiod
|
|
197
|
-
control = RadiodControl("radiod.local")
|
|
197
|
+
control = RadiodControl("radiod.local", client_id="getting-started")
|
|
198
198
|
|
|
199
199
|
# Create a self-healing stream
|
|
200
200
|
stream = ManagedStream(
|
|
@@ -161,7 +161,7 @@ def make_callback(label):
|
|
|
161
161
|
handle(label, samples, quality)
|
|
162
162
|
return on_samples
|
|
163
163
|
|
|
164
|
-
with RadiodControl("bee3-status.local") as control:
|
|
164
|
+
with RadiodControl("bee3-status.local", client_id="band-recorder") as control:
|
|
165
165
|
multi = MultiStream(control=control)
|
|
166
166
|
for label, fhz, preset, rate in BAND_PLAN:
|
|
167
167
|
multi.add_channel(
|
|
@@ -241,7 +241,7 @@ retune it in place.
|
|
|
241
241
|
```python
|
|
242
242
|
from ka9q import RadiodControl, ManagedStream
|
|
243
243
|
|
|
244
|
-
with RadiodControl("bee1-hf-status.local") as control:
|
|
244
|
+
with RadiodControl("bee1-hf-status.local", client_id="swl-tuner") as control:
|
|
245
245
|
stream = ManagedStream(
|
|
246
246
|
control=control,
|
|
247
247
|
frequency_hz=14.074e6,
|
|
@@ -323,7 +323,7 @@ For a desktop-style tuner, the usual shape is:
|
|
|
323
323
|
```python
|
|
324
324
|
class Receiver:
|
|
325
325
|
def __init__(self, host):
|
|
326
|
-
self._control = RadiodControl(host)
|
|
326
|
+
self._control = RadiodControl(host, client_id="swl-tuner")
|
|
327
327
|
self._stream = ManagedStream(
|
|
328
328
|
control=self._control,
|
|
329
329
|
frequency_hz=14.200e6,
|
|
@@ -653,7 +653,7 @@ frequency with `ManagedStream`. Both use the same `RadiodControl`:
|
|
|
653
653
|
```python
|
|
654
654
|
from ka9q import RadiodControl, SpectrumStream, ManagedStream
|
|
655
655
|
|
|
656
|
-
with RadiodControl("radiod.local") as ctl:
|
|
656
|
+
with RadiodControl("radiod.local", client_id="spectrogram-demo") as ctl:
|
|
657
657
|
# Wideband spectrum display
|
|
658
658
|
spectrum = SpectrumStream(
|
|
659
659
|
control=ctl,
|
|
@@ -224,6 +224,12 @@ ka9q-python's "actors" are its consumers and the protocol it tracks:
|
|
|
224
224
|
concurrent long-running use.
|
|
225
225
|
- `KQP-Q-003` `[CODE]` ✅ Receive sockets SHALL set **`SO_RCVBUF` = 64 MB** on
|
|
226
226
|
both `RadiodStream` and `MultiStream` to resist GIL-stall packet loss.
|
|
227
|
+
- `KQP-Q-010` `[CODE]` ✅ Every delivered sample batch SHALL carry the true
|
|
228
|
+
RTP timestamp of its FIRST sample (`StreamQuality.delivered_rtp_start`,
|
|
229
|
+
gap fills included), computed from the resequencer's emission tracking.
|
|
230
|
+
Consumers labelling samples SHALL prefer it over `last_rtp_timestamp`
|
|
231
|
+
(the last RECEIVED packet's header), which desynchronizes from the
|
|
232
|
+
delivered stream under loss. (2026-08-11; hf-timestd T6 origin slips.)
|
|
227
233
|
- `KQP-Q-004` `[CODE]` ✅ `ManagedStream` SHALL recover automatically from
|
|
228
234
|
radiod restart and network interruption without consumer intervention.
|
|
229
235
|
- `KQP-Q-005` `[DOC]` ✅ Protocol definitions SHALL be **provably in sync** with
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Upstream Alignment & Client-Compat Audit — 2026-08-12
|
|
2
|
+
|
|
3
|
+
Audit of ka9q-python's alignment with `ka9q-radio` upstream
|
|
4
|
+
(`14d780af624e821941708bd0d64fd895a0c80a2a` →
|
|
5
|
+
`cedec349f7b4212078de3e007d142b4c64d36546`, 149 commits) and its
|
|
6
|
+
sigmond-suite client contracts. Read `findings.md` first — it is the
|
|
7
|
+
document Checkpoint A reviews.
|
|
8
|
+
|
|
9
|
+
## Reports
|
|
10
|
+
|
|
11
|
+
| File | Task | Contents |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| [`upstream.md`](./upstream.md) | 2 | Drift-watcher verdict, full 149-commit classification |
|
|
14
|
+
| [`contract.md`](./contract.md) | 3 | Enum-level header diff, `sync_types.py` reconciliation, pin-advance verdict |
|
|
15
|
+
| [`control-surface.md`](./control-surface.md) | 4 | Every writable/readable radiod TLV mapped against `RadiodControl`/`status.py` |
|
|
16
|
+
| [`clients.md`](./clients.md) | 5 | Per-client symbol contract matrix + no-bypass sweep across all `/opt/git/sigmond` repos |
|
|
17
|
+
| [`idempotency.md`](./idempotency.md) | 6–7 | Five idempotency spec questions, code-level verdicts, and live probes against `bee1`/`bee2` radiod |
|
|
18
|
+
| [`findings.md`](./findings.md) | 8 | **This audit's output** — every finding above, ranked P0–P3 with evidence links and remediation |
|
|
19
|
+
|
|
20
|
+
## Executive summary
|
|
21
|
+
|
|
22
|
+
**Drift verdict:** `CONTRACT: CRITICAL-CHANGE — DO NOT ADVANCE` (Task 3).
|
|
23
|
+
The only client-visible enum change in range is `DemodType.N_DEMOD`
|
|
24
|
+
shifting 5→6 (upstream inserted `IDLE_DEMOD`); `check_upstream_drift.py`'s
|
|
25
|
+
`STREAM_CRITICAL_ENUMS` policy classifies this `fail` regardless of usage.
|
|
26
|
+
Zero live code anywhere references `N_DEMOD` by value, so nothing decodes
|
|
27
|
+
wrong today — the block is a mechanical policy trigger, not a demonstrated
|
|
28
|
+
break (see `findings.md` F2).
|
|
29
|
+
|
|
30
|
+
**Is the pin safe to advance?** Not yet, per the mechanical gate — but the
|
|
31
|
+
remediation is small and well-scoped: regenerate `types.py`
|
|
32
|
+
(`sync_types.py --apply`, plus a manual add for the negative-literal
|
|
33
|
+
`INVALID_DEMOD` the parser misses — F10), widen `control.py`'s hardcoded
|
|
34
|
+
demod-range check (F13), then re-run the watcher. None of this is an
|
|
35
|
+
emergency fix for a live bug; it's the normal Task 4/9 capability-exposure
|
|
36
|
+
step this project's process expects before a pin with a flagged enum
|
|
37
|
+
change advances.
|
|
38
|
+
|
|
39
|
+
**Findings:** 23 total — **1 P0, 9 P1, 4 P2, 9 P3**. The P0 (`F1`) is
|
|
40
|
+
`MultiStream`'s restore path silently changing a channel's SSRC identity
|
|
41
|
+
on recovery when a channel has non-default gain/AGC — the shared
|
|
42
|
+
recovery substrate every major sigmond client depends on. Several P1s are
|
|
43
|
+
environment/deployment findings from the live Task 7 probes (silent
|
|
44
|
+
`destination=` failure, a host-routing trap that swallows outbound control
|
|
45
|
+
commands with zero error, and a keepalive docstring claim that doesn't
|
|
46
|
+
hold empirically) — each is explicitly flagged with its own ambiguity
|
|
47
|
+
note rather than forced into a clean bucket, per the audit's ranking
|
|
48
|
+
rubric. Zero confirmed no-bypass violations exist among sigmond-authored
|
|
49
|
+
Python clients (`clients.md`); the one bypass found (`ka9q-web`, vendored
|
|
50
|
+
third-party C) is a policy-scope question, not a ka9q-python code fix.
|
|
51
|
+
|
|
52
|
+
**Artifact:** https://claude.ai/code/artifact/13cb509f-9248-49ed-bb05-0be0354d4361
|