ka9q-python 3.26.1__tar.gz → 3.26.2__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.26.1 → ka9q_python-3.26.2}/CHANGELOG.md +22 -0
- {ka9q_python-3.26.1/ka9q_python.egg-info → ka9q_python-3.26.2}/PKG-INFO +1 -1
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/control.py +39 -4
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/stream.py +6 -5
- {ka9q_python-3.26.1 → ka9q_python-3.26.2/ka9q_python.egg-info}/PKG-INFO +1 -1
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/pyproject.toml +1 -1
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_ensure_channel_encoding.py +63 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/LICENSE +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/MANIFEST.in +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/README.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/API_REFERENCE.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/ARCHITECTURE.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/CLI_GUIDE.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/GETTING_STARTED.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/INSTALLATION.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/MULTI_STREAM.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/POLL-SUPPRESSION-DESIGN.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/RECIPES.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/REQUIREMENTS.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/RTP_TIMING_SUPPORT.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/SECURITY.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/TESTING_GUIDE.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/TUI_GUIDE.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/README.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/clients.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/contract.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/control-surface.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/findings.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/idempotency.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/upstream.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/superpowers/plans/2026-08-12-upstream-alignment-and-client-compat.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/superpowers/plans/2026-08-13-audit-remediation.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/superpowers/plans/2026-08-13-client-side-fixes.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/superpowers/specs/2026-08-12-upstream-alignment-and-client-compat-design.md +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/diagnostics/diagnose_packets.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/diagnostics/repro_utc_bug.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/discover_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/multi_stream_smoke.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/spectrum_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/stream_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/test_improvements.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/tune.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/examples/tune_example.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/__init__.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/_multicast.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/addressing.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/cli.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/compat.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/discovery.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/monitor.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/multi_stream.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/pps_calibrator.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/recovery_ladder.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/slot_clock.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/spectrum_stream.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/status.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/status_listener.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/tui.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/types.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q/utils.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q_python.egg-info/SOURCES.txt +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q_python.egg-info/entry_points.txt +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/ka9q_radio_compat +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/scripts/check_upstream_drift.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/scripts/gen_client_manifest.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/scripts/sync_types.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/setup.cfg +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/__init__.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/conftest.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_addressing.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_channel_verification.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_client_contract.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_client_id_destination.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_compat_pin_reachability.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_decode_description.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_delivered_rtp_start.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_destination_required.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_filter_edges.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_gen_client_manifest.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_idempotency_integration.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_integration.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_keepalive_encoding.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_lifetime.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_monitor.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_multicast_helpers.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_multistream_completeness.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_multistream_gap_storm.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_multistream_prune.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_multistream_restore.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_parameter_vector.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_parse_rtp_samples_iq.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_poll_channel_idle.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_protocol_compat.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_recovery_ladder.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_rtp_to_wallclock_deprecated.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_security_features.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_set_demod_type.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_set_lock_warns.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_set_spectrum.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_slot_clock.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_spectrum.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_ssrc_radiod_host_unit.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_status_decoder.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_status_listener.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_status_socket_drain.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_sync_types.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_ttl_warning.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_tune.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_tune_live.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_tune_method.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_upstream_drift.py +0 -0
- {ka9q_python-3.26.1 → ka9q_python-3.26.2}/tests/test_zero_block_detection.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.26.2 (2026-11-01)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`ensure_channel()` now waits for radiod to report the requested
|
|
8
|
+
encoding.** `create_channel()` sends `OUTPUT_ENCODING` as a separate
|
|
9
|
+
command after the create, and `ensure_channel()` returned the first status
|
|
10
|
+
it saw, which could predate that command. A fresh F32 channel then came
|
|
11
|
+
back labelled S16, and `RadiodStream` decoded 4-byte floats as 2-byte
|
|
12
|
+
ints: noise-like samples on the 1/32768 grid, rms about 0.58 of full
|
|
13
|
+
scale, with "completeness 100%". codar-sounder ran this way on AC0G-ND and
|
|
14
|
+
UACNJ and heard no CODAR all day; with the encoding settled, the same
|
|
15
|
+
channel showed the Rutgers 13 MHz radars at 32 dB. `ensure_channel()` now
|
|
16
|
+
re-polls until the reported encoding matches the request, within its
|
|
17
|
+
`timeout`. If radiod still reports another encoding at the deadline, it
|
|
18
|
+
logs an error and returns radiod's value, so a stream decodes what is on
|
|
19
|
+
the wire. Checked live on UACNJ's radiod: a new F32 channel first reported
|
|
20
|
+
encoding 2 and settled to 4.
|
|
21
|
+
- The comments that blamed radiod for "silently downgrading F32 to S16"
|
|
22
|
+
(`control.py`, `stream.py`) now name the stale status instead. The bee1
|
|
23
|
+
2026-05-15 T6/TSL3 incident they cited was very likely the same race.
|
|
24
|
+
|
|
3
25
|
## 3.26.1 (2026-10-01)
|
|
4
26
|
|
|
5
27
|
### Fixed
|
|
@@ -910,7 +910,11 @@ class RadiodControl:
|
|
|
910
910
|
Sends TLV-encoded commands to radiod's control socket to create
|
|
911
911
|
and configure channels.
|
|
912
912
|
"""
|
|
913
|
-
|
|
913
|
+
|
|
914
|
+
# ensure_channel(): pause between status polls while waiting for radiod
|
|
915
|
+
# to report the requested OUTPUT_ENCODING.
|
|
916
|
+
_ENCODING_SETTLE_POLL_S = 0.1
|
|
917
|
+
|
|
914
918
|
def __init__(self, status_address: str, max_commands_per_sec: int = 100,
|
|
915
919
|
interface: Optional[str] = None,
|
|
916
920
|
client_id: Optional[str] = None):
|
|
@@ -2072,9 +2076,40 @@ class RadiodControl:
|
|
|
2072
2076
|
f"Channel SSRC {ssrc} not verified within {timeout}s. "
|
|
2073
2077
|
f"Requested: {frequency_hz/1e6:.3f} MHz, {preset}, {sample_rate} Hz"
|
|
2074
2078
|
)
|
|
2075
|
-
#
|
|
2076
|
-
#
|
|
2077
|
-
#
|
|
2079
|
+
# create_channel() sends OUTPUT_ENCODING as a separate command after
|
|
2080
|
+
# the create, so the first status can predate it and report the
|
|
2081
|
+
# preset's encoding. Streams pick their decoder from
|
|
2082
|
+
# ChannelInfo.encoding; a stale S16 on an F32 stream decodes 4-byte
|
|
2083
|
+
# floats as 2-byte ints — noise-like garbage at "100% completeness".
|
|
2084
|
+
# Keep polling until radiod reports the requested encoding. If it
|
|
2085
|
+
# still differs at the deadline, return what radiod reports (the
|
|
2086
|
+
# stream must decode what is on the wire) and say so loudly.
|
|
2087
|
+
if encoding != 0 and channel.encoding != encoding:
|
|
2088
|
+
deadline = time.monotonic() + timeout
|
|
2089
|
+
stale = channel.encoding
|
|
2090
|
+
while time.monotonic() < deadline:
|
|
2091
|
+
time.sleep(self._ENCODING_SETTLE_POLL_S)
|
|
2092
|
+
fresh = self.poll_channel(
|
|
2093
|
+
ssrc, expected_freq=frequency_hz,
|
|
2094
|
+
timeout=max(0.1, min(1.0, deadline - time.monotonic())),
|
|
2095
|
+
frequency_tolerance=frequency_tolerance,
|
|
2096
|
+
)
|
|
2097
|
+
if fresh is not None:
|
|
2098
|
+
channel = fresh
|
|
2099
|
+
if channel.encoding == encoding:
|
|
2100
|
+
break
|
|
2101
|
+
if channel.encoding == encoding:
|
|
2102
|
+
logger.info(
|
|
2103
|
+
"ensure_channel: SSRC %s encoding settled %s -> %s",
|
|
2104
|
+
ssrc, stale, channel.encoding,
|
|
2105
|
+
)
|
|
2106
|
+
else:
|
|
2107
|
+
logger.error(
|
|
2108
|
+
"ensure_channel: SSRC %s still reports encoding %s after "
|
|
2109
|
+
"%.1fs (requested %s); returning radiod's value",
|
|
2110
|
+
ssrc, channel.encoding, timeout, encoding,
|
|
2111
|
+
)
|
|
2112
|
+
# A rate/preset divergence is logged
|
|
2078
2113
|
# but not fatal — the channel exists at the requested frequency.
|
|
2079
2114
|
# An ABSENT preset is not a divergence. radiod may decline to echo
|
|
2080
2115
|
# PRESET at all (write-only from the nopreset branch on), and a label
|
|
@@ -129,11 +129,12 @@ def parse_rtp_samples(
|
|
|
129
129
|
if floats is None:
|
|
130
130
|
return None
|
|
131
131
|
if is_iq:
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
132
|
+
# Decode with the encoding the channel reports. A wrong value
|
|
133
|
+
# here corrupts silently: the bee1 2026-05-15 T6/TSL3 "F32→S16
|
|
134
|
+
# downgrade" and codar-sounder's 2026-11-01 garbage IQ were both
|
|
135
|
+
# a stale encoding from ensure_channel (status read before the
|
|
136
|
+
# separate OUTPUT_ENCODING command landed), not radiod changing
|
|
137
|
+
# the encoding. ensure_channel now waits for it to settle.
|
|
137
138
|
if len(floats) % 2 != 0:
|
|
138
139
|
logger.warning(f"Odd number of samples in IQ payload: {len(floats)}")
|
|
139
140
|
return None
|
|
@@ -75,5 +75,68 @@ class TestEnsureChannelEncoding(unittest.TestCase):
|
|
|
75
75
|
_, kwargs = self.control.create_channel.call_args
|
|
76
76
|
self.assertEqual(kwargs['encoding'], Encoding.F32)
|
|
77
77
|
|
|
78
|
+
|
|
79
|
+
def _info(ssrc, encoding):
|
|
80
|
+
return ChannelInfo(ssrc=ssrc, preset="iq", sample_rate=16000,
|
|
81
|
+
frequency=14074000.0, snr=0.0,
|
|
82
|
+
multicast_address="239.1.1.1", port=5004,
|
|
83
|
+
encoding=encoding)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestEnsureChannelEncodingSettles(unittest.TestCase):
|
|
87
|
+
"""The first status after create can predate OUTPUT_ENCODING.
|
|
88
|
+
|
|
89
|
+
Found 2026-11-01: codar-sounder on AC0G-ND and UACNJ got encoding S16
|
|
90
|
+
back for an F32 channel and decoded 4-byte floats as 2-byte ints for a
|
|
91
|
+
whole day. ensure_channel must wait for the requested encoding.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
SSRC = 12345
|
|
95
|
+
|
|
96
|
+
@patch('ka9q.control.RadiodControl._connect')
|
|
97
|
+
def setUp(self, mock_connect):
|
|
98
|
+
self.control = RadiodControl("radiod.local")
|
|
99
|
+
self.control.create_channel = MagicMock()
|
|
100
|
+
self.control._ENCODING_SETTLE_POLL_S = 0.0
|
|
101
|
+
|
|
102
|
+
def _ensure(self, encoding=Encoding.F32, timeout=0.5):
|
|
103
|
+
with patch('ka9q.control.allocate_ssrc', return_value=self.SSRC):
|
|
104
|
+
return self.control.ensure_channel(
|
|
105
|
+
frequency_hz=14.074e6, destination="239.1.1.1",
|
|
106
|
+
encoding=encoding, timeout=timeout)
|
|
107
|
+
|
|
108
|
+
def test_stale_first_status_is_replaced_by_the_settled_one(self):
|
|
109
|
+
# probe (no channel yet), first verify (stale S16), then F32
|
|
110
|
+
self.control.poll_channel = MagicMock(side_effect=[
|
|
111
|
+
None, _info(self.SSRC, Encoding.S16BE),
|
|
112
|
+
_info(self.SSRC, Encoding.S16BE), _info(self.SSRC, Encoding.F32)])
|
|
113
|
+
result = self._ensure()
|
|
114
|
+
self.assertEqual(result.encoding, Encoding.F32)
|
|
115
|
+
self.assertEqual(self.control.poll_channel.call_count, 4)
|
|
116
|
+
|
|
117
|
+
def test_encoding_that_never_settles_returns_radiods_value_and_logs(self):
|
|
118
|
+
def poll(*a, **kw):
|
|
119
|
+
return None if poll.n == 0 else _info(self.SSRC, Encoding.S16BE)
|
|
120
|
+
poll.n = 0
|
|
121
|
+
def counting(*a, **kw):
|
|
122
|
+
r = poll(*a, **kw); poll.n += 1; return r
|
|
123
|
+
self.control.poll_channel = MagicMock(side_effect=counting)
|
|
124
|
+
with self.assertLogs('ka9q.control', level='ERROR') as logs:
|
|
125
|
+
result = self._ensure(timeout=0.2)
|
|
126
|
+
self.assertEqual(result.encoding, Encoding.S16BE)
|
|
127
|
+
self.assertTrue(any("still reports encoding" in m for m in logs.output))
|
|
128
|
+
|
|
129
|
+
def test_matching_first_status_polls_no_more(self):
|
|
130
|
+
self.control.poll_channel = MagicMock(side_effect=[
|
|
131
|
+
None, _info(self.SSRC, Encoding.F32)])
|
|
132
|
+
self.assertEqual(self._ensure().encoding, Encoding.F32)
|
|
133
|
+
self.assertEqual(self.control.poll_channel.call_count, 2)
|
|
134
|
+
|
|
135
|
+
def test_no_encoding_requested_takes_the_first_status(self):
|
|
136
|
+
self.control.poll_channel = MagicMock(side_effect=[
|
|
137
|
+
None, _info(self.SSRC, Encoding.S16BE)])
|
|
138
|
+
self.assertEqual(self._ensure(encoding=0).encoding, Encoding.S16BE)
|
|
139
|
+
self.assertEqual(self.control.poll_channel.call_count, 2)
|
|
140
|
+
|
|
78
141
|
if __name__ == '__main__':
|
|
79
142
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/audit/2026-08-12-alignment/control-surface.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/superpowers/plans/2026-08-13-audit-remediation.md
RENAMED
|
File without changes
|
{ka9q_python-3.26.1 → ka9q_python-3.26.2}/docs/superpowers/plans/2026-08-13-client-side-fixes.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|