ka9q-python 3.12.0__tar.gz → 3.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/CHANGELOG.md +37 -0
  2. {ka9q_python-3.12.0/ka9q_python.egg-info → ka9q_python-3.13.0}/PKG-INFO +1 -1
  3. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/__init__.py +1 -1
  4. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/multi_stream.py +32 -0
  5. {ka9q_python-3.12.0 → ka9q_python-3.13.0/ka9q_python.egg-info}/PKG-INFO +1 -1
  6. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/pyproject.toml +1 -1
  7. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_lifetime.py +88 -0
  8. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/LICENSE +0 -0
  9. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/MANIFEST.in +0 -0
  10. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/README.md +0 -0
  11. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/API_REFERENCE.md +0 -0
  12. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/ARCHITECTURE.md +0 -0
  13. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/CLI_GUIDE.md +0 -0
  14. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/GETTING_STARTED.md +0 -0
  15. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/INSTALLATION.md +0 -0
  16. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/MULTI_STREAM.md +0 -0
  17. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/RECIPES.md +0 -0
  18. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/RTP_TIMING_SUPPORT.md +0 -0
  19. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/SECURITY.md +0 -0
  20. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/TESTING_GUIDE.md +0 -0
  21. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/docs/TUI_GUIDE.md +0 -0
  22. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/advanced_features_demo.py +0 -0
  23. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/channel_cleanup_example.py +0 -0
  24. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/codar_oceanography.py +0 -0
  25. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/diagnostics/diagnose_packets.py +0 -0
  26. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/diagnostics/repro_utc_bug.py +0 -0
  27. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/discover_example.py +0 -0
  28. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/grape_integration_example.py +0 -0
  29. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/hf_band_scanner.py +0 -0
  30. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/multi_stream_smoke.py +0 -0
  31. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/rtp_recorder_example.py +0 -0
  32. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/simple_am_radio.py +0 -0
  33. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/spectrum_example.py +0 -0
  34. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/stream_example.py +0 -0
  35. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/superdarn_recorder.py +0 -0
  36. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/test_channel_operations.py +0 -0
  37. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/test_improvements.py +0 -0
  38. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/test_timing_fields.py +0 -0
  39. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/tune.py +0 -0
  40. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/examples/tune_example.py +0 -0
  41. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/addressing.py +0 -0
  42. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/cli.py +0 -0
  43. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/compat.py +0 -0
  44. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/control.py +0 -0
  45. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/discovery.py +0 -0
  46. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/exceptions.py +0 -0
  47. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/managed_stream.py +0 -0
  48. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/monitor.py +0 -0
  49. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/pps_calibrator.py +0 -0
  50. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/resequencer.py +0 -0
  51. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/rtp_recorder.py +0 -0
  52. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/spectrum_stream.py +0 -0
  53. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/status.py +0 -0
  54. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/stream.py +0 -0
  55. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/stream_quality.py +0 -0
  56. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/tui.py +0 -0
  57. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/types.py +0 -0
  58. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q/utils.py +0 -0
  59. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q_python.egg-info/SOURCES.txt +0 -0
  60. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
  61. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q_python.egg-info/entry_points.txt +0 -0
  62. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q_python.egg-info/requires.txt +0 -0
  63. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q_python.egg-info/top_level.txt +0 -0
  64. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/ka9q_radio_compat +0 -0
  65. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/scripts/check_upstream_drift.py +0 -0
  66. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/scripts/sync_types.py +0 -0
  67. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/setup.cfg +0 -0
  68. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/setup.py +0 -0
  69. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/__init__.py +0 -0
  70. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/conftest.py +0 -0
  71. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_addressing.py +0 -0
  72. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_channel_verification.py +0 -0
  73. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_create_split_encoding.py +0 -0
  74. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_decode_description.py +0 -0
  75. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_decode_functions.py +0 -0
  76. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_encode_functions.py +0 -0
  77. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_encode_socket.py +0 -0
  78. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_ensure_channel_encoding.py +0 -0
  79. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_filter_edges.py +0 -0
  80. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_integration.py +0 -0
  81. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_iq_20khz_f32.py +0 -0
  82. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_listen_multicast.py +0 -0
  83. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_managed_stream_recovery.py +0 -0
  84. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_monitor.py +0 -0
  85. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_multihomed.py +0 -0
  86. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_native_discovery.py +0 -0
  87. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_performance_fixes.py +0 -0
  88. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_protocol_compat.py +0 -0
  89. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_remove_channel.py +0 -0
  90. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_rtp_recorder.py +0 -0
  91. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_security_features.py +0 -0
  92. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_spectrum.py +0 -0
  93. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_ssrc_dest_unit.py +0 -0
  94. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_ssrc_encoding_unit.py +0 -0
  95. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_ssrc_radiod_host_unit.py +0 -0
  96. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_status_decoder.py +0 -0
  97. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_ttl_warning.py +0 -0
  98. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_tune.py +0 -0
  99. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_tune_cli.py +0 -0
  100. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_tune_debug.py +0 -0
  101. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_tune_live.py +0 -0
  102. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_tune_method.py +0 -0
  103. {ka9q_python-3.12.0 → ka9q_python-3.13.0}/tests/test_upstream_drift.py +0 -0
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.13.0] - 2026-05-08
4
+
5
+ ### Added
6
+
7
+ - **`MultiStream` channel-lifetime support**: closes the gap from 3.10.0 where
8
+ `RadiodControl.{create_channel,ensure_channel,tune}` accepted a `lifetime=`
9
+ kwarg but `MultiStream.add_channel()` did not, leaving MultiStream-based
10
+ clients (psk-recorder, hfdl-recorder, hf-timestd) unable to opt into
11
+ radiod's channel self-destruct timer for crash-resilient cleanup.
12
+ - `MultiStream.add_channel(..., lifetime=None)` — optional kwarg, forwarded
13
+ to the internal `ensure_channel` call. Stored per-slot.
14
+ - **Drop/restore path now re-applies lifetime**: `_attempt_restore` reads
15
+ the stored slot lifetime and passes it to `ensure_channel`. Previously,
16
+ a channel that radiod self-destructed and MultiStream restored would
17
+ silently lose its LIFETIME until the next external keep-alive — the
18
+ most dangerous failure mode now closed.
19
+ - `MultiStream.set_channel_lifetime(ssrc, lifetime)` — keep-alive method
20
+ that updates both the wire (via `RadiodControl.set_channel_lifetime`)
21
+ and the slot's stored lifetime, so the value survives subsequent
22
+ drop/restore cycles.
23
+
24
+ ### Backward Compatibility
25
+
26
+ - Default behavior unchanged: omitting `lifetime` produces a packet with no
27
+ LIFETIME tag (ChannelSlot.lifetime defaults to None). Existing MultiStream
28
+ callers see no change in wire behavior.
29
+
30
+ ### Tests
31
+
32
+ - 5 new unit tests in `tests/test_lifetime.py::TestMultiStreamLifetime` cover:
33
+ forward-on-add, lifetime=None default, restore-reapplies-lifetime,
34
+ set_channel_lifetime updates slot+wire, set_channel_lifetime is a no-op for
35
+ unknown SSRC. 258 unit tests still green.
36
+
37
+ ---
38
+
39
+
3
40
  ## [3.12.0] - 2026-05-07
4
41
 
5
42
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.12.0
3
+ Version: 3.13.0
4
4
  Summary: Python interface for ka9q-radio control and monitoring
5
5
  Home-page: https://github.com/mijahauan/ka9q-python
6
6
  Author: Michael Hauan AC0G
@@ -56,7 +56,7 @@ Lower-level usage (explicit control):
56
56
  )
57
57
  print(f"Created channel with SSRC: {ssrc}")
58
58
  """
59
- __version__ = '3.12.0'
59
+ __version__ = '3.13.0'
60
60
  __author__ = 'Michael Hauan AC0G'
61
61
 
62
62
  from .control import RadiodControl, allocate_ssrc
@@ -81,6 +81,7 @@ class _ChannelSlot:
81
81
  last_packet_time: float = 0.0
82
82
  dropped: bool = False
83
83
  first_rtp_timestamp: Optional[int] = None
84
+ lifetime: Optional[int] = None
84
85
 
85
86
 
86
87
  class MultiStream:
@@ -132,6 +133,7 @@ class MultiStream:
132
133
  low_edge: Optional[float] = None,
133
134
  high_edge: Optional[float] = None,
134
135
  kaiser_beta: Optional[float] = None,
136
+ lifetime: Optional[int] = None,
135
137
  ) -> ChannelInfo:
136
138
  """Provision a channel and register it for reception.
137
139
 
@@ -142,6 +144,15 @@ class MultiStream:
142
144
  preset's default passband. None = use preset defaults. See
143
145
  RadiodControl.ensure_channel for the full semantics.
144
146
 
147
+ ``lifetime`` opts the channel into radiod's self-destruct timer
148
+ (radiod commit 0f8b622+, see RadiodControl.set_channel_lifetime).
149
+ The value is stored per-slot so the drop/restore path re-applies
150
+ it: a channel that radiod self-destructs and we then restore
151
+ won't silently lose its lifetime. The caller is still
152
+ responsible for periodic keep-alive via
153
+ ``set_channel_lifetime()`` (or by calling
154
+ RadiodControl.set_channel_lifetime directly on the SSRC).
155
+
145
156
  Returns the ChannelInfo from ensure_channel().
146
157
  """
147
158
  channel_info = self._control.ensure_channel(
@@ -154,6 +165,7 @@ class MultiStream:
154
165
  low_edge=low_edge,
155
166
  high_edge=high_edge,
156
167
  kaiser_beta=kaiser_beta,
168
+ lifetime=lifetime,
157
169
  )
158
170
 
159
171
  addr = channel_info.multicast_address
@@ -189,6 +201,7 @@ class MultiStream:
189
201
  on_stream_dropped=on_stream_dropped,
190
202
  on_stream_restored=on_stream_restored,
191
203
  deliver_interval=self._deliver_interval,
204
+ lifetime=lifetime,
192
205
  )
193
206
  self._slots[ssrc] = slot
194
207
 
@@ -224,6 +237,24 @@ class MultiStream:
224
237
  f"{self._multicast_address}:{self._port}"
225
238
  )
226
239
 
240
+ def set_channel_lifetime(self, ssrc: int, lifetime: int) -> None:
241
+ """Refresh the LIFETIME tag on one channel and update slot state.
242
+
243
+ Suitable as a periodic keep-alive: callers should invoke this on
244
+ every active SSRC at a cadence shorter than the lifetime so the
245
+ radiod self-destruct counter never reaches zero. The new value
246
+ is also stored in the slot, so a subsequent drop/restore will
247
+ re-apply this value rather than the original ``add_channel``
248
+ argument.
249
+
250
+ No-op if ``ssrc`` is not in this MultiStream.
251
+ """
252
+ slot = self._slots.get(ssrc)
253
+ if slot is None:
254
+ return
255
+ self._control.set_channel_lifetime(ssrc, lifetime)
256
+ slot.lifetime = lifetime
257
+
227
258
  def stop(self) -> None:
228
259
  """Stop threads and close socket."""
229
260
  if not self._running:
@@ -426,6 +457,7 @@ class MultiStream:
426
457
  preset=slot.preset,
427
458
  sample_rate=slot.sample_rate,
428
459
  encoding=slot.encoding,
460
+ lifetime=slot.lifetime,
429
461
  )
430
462
  new_ssrc = channel_info.ssrc
431
463
  if new_ssrc != ssrc:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.12.0
3
+ Version: 3.13.0
4
4
  Summary: Python interface for ka9q-radio control and monitoring
5
5
  Home-page: https://github.com/mijahauan/ka9q-python
6
6
  Author: Michael Hauan AC0G
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ka9q-python"
7
- version = "3.12.0"
7
+ version = "3.13.0"
8
8
  description = "Python interface for ka9q-radio control and monitoring"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -173,3 +173,91 @@ class TestCreateChannelLifetime:
173
173
  )
174
174
  assert sent
175
175
  assert not _has_lifetime_tag(sent[0])
176
+
177
+
178
+ class TestMultiStreamLifetime:
179
+ """MultiStream stores ``lifetime`` per-slot and forwards it to
180
+ ensure_channel on both the initial add and the drop/restore path,
181
+ plus exposes a set_channel_lifetime() keep-alive method.
182
+
183
+ Asserted at the ``RadiodControl.ensure_channel`` boundary — these
184
+ tests don't exercise the wire encoding (covered above), they verify
185
+ that MultiStream wires the kwarg through correctly.
186
+ """
187
+
188
+ def _make_multi_with_mock_control(self, ssrc=12345):
189
+ from ka9q.multi_stream import MultiStream
190
+ from ka9q.discovery import ChannelInfo
191
+
192
+ control = MagicMock()
193
+ control.ensure_channel.return_value = ChannelInfo(
194
+ ssrc=ssrc,
195
+ preset="iq",
196
+ sample_rate=12000,
197
+ frequency=14_074_000.0,
198
+ snr=0.0,
199
+ multicast_address="239.1.2.3",
200
+ port=5004,
201
+ )
202
+ multi = MultiStream(control=control)
203
+ return multi, control
204
+
205
+ def test_add_channel_forwards_lifetime(self):
206
+ multi, control = self._make_multi_with_mock_control()
207
+ multi.add_channel(
208
+ frequency_hz=14_074_000.0,
209
+ preset="iq",
210
+ sample_rate=12000,
211
+ lifetime=6000,
212
+ )
213
+ kwargs = control.ensure_channel.call_args.kwargs
214
+ assert kwargs["lifetime"] == 6000
215
+
216
+ def test_add_channel_lifetime_none_when_omitted(self):
217
+ multi, control = self._make_multi_with_mock_control()
218
+ multi.add_channel(
219
+ frequency_hz=14_074_000.0,
220
+ preset="iq",
221
+ sample_rate=12000,
222
+ )
223
+ kwargs = control.ensure_channel.call_args.kwargs
224
+ assert kwargs["lifetime"] is None
225
+
226
+ def test_restore_reapplies_stored_lifetime(self):
227
+ """A slot added with lifetime=N must re-pass N on _attempt_restore."""
228
+ multi, control = self._make_multi_with_mock_control()
229
+ multi.add_channel(
230
+ frequency_hz=14_074_000.0,
231
+ preset="iq",
232
+ sample_rate=12000,
233
+ lifetime=6000,
234
+ )
235
+ ssrc = next(iter(multi._slots))
236
+ slot = multi._slots[ssrc]
237
+ slot.dropped = True
238
+ control.ensure_channel.reset_mock()
239
+
240
+ multi._attempt_restore(ssrc, slot)
241
+
242
+ kwargs = control.ensure_channel.call_args.kwargs
243
+ assert kwargs["lifetime"] == 6000
244
+
245
+ def test_set_channel_lifetime_updates_slot_and_wire(self):
246
+ multi, control = self._make_multi_with_mock_control()
247
+ multi.add_channel(
248
+ frequency_hz=14_074_000.0,
249
+ preset="iq",
250
+ sample_rate=12000,
251
+ lifetime=6000,
252
+ )
253
+ ssrc = next(iter(multi._slots))
254
+
255
+ multi.set_channel_lifetime(ssrc, 9000)
256
+
257
+ control.set_channel_lifetime.assert_called_once_with(ssrc, 9000)
258
+ assert multi._slots[ssrc].lifetime == 9000
259
+
260
+ def test_set_channel_lifetime_unknown_ssrc_is_noop(self):
261
+ multi, control = self._make_multi_with_mock_control()
262
+ multi.set_channel_lifetime(99999, 6000)
263
+ control.set_channel_lifetime.assert_not_called()
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