ka9q-python 3.2.6__tar.gz → 3.2.7__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 (65) hide show
  1. {ka9q_python-3.2.6/ka9q_python.egg-info → ka9q_python-3.2.7}/PKG-INFO +20 -1
  2. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/README.md +19 -0
  3. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/__init__.py +3 -1
  4. ka9q_python-3.2.7/ka9q/monitor.py +150 -0
  5. {ka9q_python-3.2.6 → ka9q_python-3.2.7/ka9q_python.egg-info}/PKG-INFO +20 -1
  6. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/SOURCES.txt +2 -0
  7. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/pyproject.toml +1 -1
  8. ka9q_python-3.2.7/tests/test_monitor.py +76 -0
  9. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/LICENSE +0 -0
  10. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/MANIFEST.in +0 -0
  11. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/advanced_features_demo.py +0 -0
  12. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/channel_cleanup_example.py +0 -0
  13. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/codar_oceanography.py +0 -0
  14. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/discover_example.py +0 -0
  15. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/grape_integration_example.py +0 -0
  16. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/hf_band_scanner.py +0 -0
  17. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/rtp_recorder_example.py +0 -0
  18. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/simple_am_radio.py +0 -0
  19. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/stream_example.py +0 -0
  20. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/superdarn_recorder.py +0 -0
  21. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/test_channel_operations.py +0 -0
  22. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/test_improvements.py +0 -0
  23. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/test_timing_fields.py +0 -0
  24. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/tune.py +0 -0
  25. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/tune_example.py +0 -0
  26. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/addressing.py +0 -0
  27. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/control.py +0 -0
  28. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/discovery.py +0 -0
  29. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/exceptions.py +0 -0
  30. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/resequencer.py +0 -0
  31. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/rtp_recorder.py +0 -0
  32. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/stream.py +0 -0
  33. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/stream_quality.py +0 -0
  34. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/types.py +0 -0
  35. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/utils.py +0 -0
  36. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/dependency_links.txt +0 -0
  37. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/requires.txt +0 -0
  38. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/top_level.txt +0 -0
  39. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/setup.cfg +0 -0
  40. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/setup.py +0 -0
  41. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/__init__.py +0 -0
  42. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/conftest.py +0 -0
  43. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_addressing.py +0 -0
  44. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_channel_verification.py +0 -0
  45. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_create_split_encoding.py +0 -0
  46. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_decode_functions.py +0 -0
  47. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_encode_functions.py +0 -0
  48. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_encode_socket.py +0 -0
  49. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_ensure_channel_encoding.py +0 -0
  50. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_integration.py +0 -0
  51. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_iq_20khz_f32.py +0 -0
  52. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_listen_multicast.py +0 -0
  53. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_multihomed.py +0 -0
  54. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_native_discovery.py +0 -0
  55. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_performance_fixes.py +0 -0
  56. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_remove_channel.py +0 -0
  57. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_rtp_recorder.py +0 -0
  58. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_security_features.py +0 -0
  59. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_ssrc_dest_unit.py +0 -0
  60. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_ssrc_encoding_unit.py +0 -0
  61. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune.py +0 -0
  62. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_cli.py +0 -0
  63. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_debug.py +0 -0
  64. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_live.py +0 -0
  65. {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_method.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.2.6
3
+ Version: 3.2.7
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
@@ -192,6 +192,25 @@ control = RadiodControl("radiod.local", interface=my_interface)
192
192
  channels = discover_channels("radiod.local", interface=my_interface)
193
193
  ```
194
194
 
195
+ ### Automatic Channel Recovery
196
+
197
+ ensure your channels survive radiod restarts:
198
+
199
+ ```python
200
+ from ka9q import RadiodControl, ChannelMonitor
201
+
202
+ control = RadiodControl("radiod.local")
203
+ monitor = ChannelMonitor(control)
204
+ monitor.start()
205
+
206
+ # This channel will be automatically re-created if it disappears
207
+ monitor.monitor_channel(
208
+ frequency_hz=14.074e6,
209
+ preset="usb",
210
+ sample_rate=12000
211
+ )
212
+ ```
213
+
195
214
  ## Documentation
196
215
 
197
216
  For detailed information, please refer to the documentation in the `docs/` directory:
@@ -157,6 +157,25 @@ control = RadiodControl("radiod.local", interface=my_interface)
157
157
  channels = discover_channels("radiod.local", interface=my_interface)
158
158
  ```
159
159
 
160
+ ### Automatic Channel Recovery
161
+
162
+ ensure your channels survive radiod restarts:
163
+
164
+ ```python
165
+ from ka9q import RadiodControl, ChannelMonitor
166
+
167
+ control = RadiodControl("radiod.local")
168
+ monitor = ChannelMonitor(control)
169
+ monitor.start()
170
+
171
+ # This channel will be automatically re-created if it disappears
172
+ monitor.monitor_channel(
173
+ frequency_hz=14.074e6,
174
+ preset="usb",
175
+ sample_rate=12000
176
+ )
177
+ ```
178
+
160
179
  ## Documentation
161
180
 
162
181
  For detailed information, please refer to the documentation in the `docs/` directory:
@@ -38,7 +38,7 @@ Lower-level usage (explicit control):
38
38
  # Or use allocate_ssrc() directly for coordination
39
39
  ssrc = allocate_ssrc(10.0e6, "iq", 16000)
40
40
  """
41
- __version__ = '3.2.6'
41
+ __version__ = '3.2.7'
42
42
  __author__ = 'Michael Hauan AC0G'
43
43
 
44
44
  from .control import RadiodControl, allocate_ssrc
@@ -112,6 +112,8 @@ __all__ = [
112
112
  'RTPPacket',
113
113
  'ResequencerStats',
114
114
  'generate_multicast_ip',
115
+ 'ChannelMonitor',
115
116
  ]
116
117
 
117
118
  from .addressing import generate_multicast_ip
119
+ from .monitor import ChannelMonitor
@@ -0,0 +1,150 @@
1
+ """
2
+ ChannelMonitor service for automatic channel recovery.
3
+
4
+ This module provides a watchdog service that monitors active channels
5
+ and automatically re-creates them if they disappear (e.g., due to a
6
+ radiod restart).
7
+ """
8
+
9
+ import threading
10
+ import time
11
+ import logging
12
+ from typing import Dict, Any, Optional
13
+
14
+ from .control import RadiodControl
15
+ from .discovery import discover_channels, ChannelInfo
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ChannelMonitor:
21
+ """
22
+ Background service to monitor and recover channels.
23
+
24
+ The ChannelMonitor maintains a list of "desired" channels and their
25
+ configuration. It periodically queries `radiod` to see what channels
26
+ actually exist. If a desired channel is missing, it automatically
27
+ calls `ensure_channel` to restore it.
28
+
29
+ Usage:
30
+ control = RadiodControl("radiod.local")
31
+ monitor = ChannelMonitor(control)
32
+ monitor.start()
33
+
34
+ # Register a channel to keep alive
35
+ monitor.monitor_channel(
36
+ frequency_hz=14.074e6,
37
+ preset="usb",
38
+ sample_rate=12000
39
+ )
40
+ """
41
+
42
+ def __init__(self, control: RadiodControl, check_interval: float = 2.0):
43
+ """
44
+ Initialize the monitor.
45
+
46
+ Args:
47
+ control: RadiodControl instance to use for discovery and creation
48
+ check_interval: How often to check channel status (seconds)
49
+ """
50
+ self.control = control
51
+ self.check_interval = check_interval
52
+ self._monitored_channels: Dict[int, Dict[str, Any]] = {}
53
+ self._running = False
54
+ self._thread: Optional[threading.Thread] = None
55
+ self._lock = threading.Lock()
56
+
57
+ def start(self):
58
+ """Start the monitoring thread."""
59
+ if self._running:
60
+ return
61
+
62
+ self._running = True
63
+ self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
64
+ self._thread.start()
65
+ logger.info(f"ChannelMonitor started (interval={self.check_interval}s)")
66
+
67
+ def stop(self):
68
+ """Stop the monitoring thread."""
69
+ self._running = False
70
+ if self._thread:
71
+ self._thread.join(timeout=self.check_interval * 2)
72
+ self._thread = None
73
+ logger.info("ChannelMonitor stopped")
74
+
75
+ def monitor_channel(self, **kwargs) -> int:
76
+ """
77
+ Register a channel to be monitored/kept alive.
78
+
79
+ Calls `ensure_channel` immediately to create/verify the channel,
80
+ then adds it to the monitoring list.
81
+
82
+ Args:
83
+ **kwargs: Arguments to pass to `ensure_channel`
84
+ (frequency_hz, preset, sample_rate, encoding, etc.)
85
+
86
+ Returns:
87
+ SSRC of the channel
88
+ """
89
+ # First ensure the channel exists
90
+ channel_info = self.control.ensure_channel(**kwargs)
91
+ ssrc = channel_info.ssrc
92
+
93
+ with self._lock:
94
+ # Store parameters for future recovery
95
+ # We filter out 'timeout' as it's a runtime param, not a channel property
96
+ recovery_params = kwargs.copy()
97
+ if 'timeout' in recovery_params:
98
+ del recovery_params['timeout']
99
+
100
+ self._monitored_channels[ssrc] = recovery_params
101
+ logger.info(f"Now monitoring channel {ssrc} for recovery")
102
+
103
+ return ssrc
104
+
105
+ def unmonitor_channel(self, ssrc: int):
106
+ """Stop monitoring a specific channel."""
107
+ with self._lock:
108
+ if ssrc in self._monitored_channels:
109
+ del self._monitored_channels[ssrc]
110
+ logger.info(f"Stopped monitoring channel {ssrc}")
111
+
112
+ def _monitor_loop(self):
113
+ """Main monitoring loop."""
114
+ while self._running:
115
+ try:
116
+ self._check_and_recover()
117
+ except Exception as e:
118
+ logger.error(f"Error in monitor loop: {e}")
119
+
120
+ time.sleep(self.check_interval)
121
+
122
+ def _check_and_recover(self):
123
+ """Perform one check cycle: discover and recover missing channels."""
124
+ # Get list of desired channels safely
125
+ with self._lock:
126
+ if not self._monitored_channels:
127
+ return
128
+ desired = self._monitored_channels.copy()
129
+
130
+ # Discover actual running channels
131
+ try:
132
+ # Use quick discovery
133
+ actual_channels = discover_channels(
134
+ self.control.status_address,
135
+ listen_duration=0.5
136
+ )
137
+ except Exception as e:
138
+ logger.warning(f"Discovery failed during check (radiod down?): {e}")
139
+ return
140
+
141
+ # Check for missing channels
142
+ for ssrc, params in desired.items():
143
+ if ssrc not in actual_channels:
144
+ logger.warning(f"Channel {ssrc} is missing! Attempting recovery...")
145
+ try:
146
+ # Attempt to restore
147
+ self.control.ensure_channel(**params)
148
+ logger.info(f"Successfully recovered channel {ssrc}")
149
+ except Exception as e:
150
+ logger.error(f"Failed to recover channel {ssrc}: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ka9q-python
3
- Version: 3.2.6
3
+ Version: 3.2.7
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
@@ -192,6 +192,25 @@ control = RadiodControl("radiod.local", interface=my_interface)
192
192
  channels = discover_channels("radiod.local", interface=my_interface)
193
193
  ```
194
194
 
195
+ ### Automatic Channel Recovery
196
+
197
+ ensure your channels survive radiod restarts:
198
+
199
+ ```python
200
+ from ka9q import RadiodControl, ChannelMonitor
201
+
202
+ control = RadiodControl("radiod.local")
203
+ monitor = ChannelMonitor(control)
204
+ monitor.start()
205
+
206
+ # This channel will be automatically re-created if it disappears
207
+ monitor.monitor_channel(
208
+ frequency_hz=14.074e6,
209
+ preset="usb",
210
+ sample_rate=12000
211
+ )
212
+ ```
213
+
195
214
  ## Documentation
196
215
 
197
216
  For detailed information, please refer to the documentation in the `docs/` directory:
@@ -23,6 +23,7 @@ ka9q/addressing.py
23
23
  ka9q/control.py
24
24
  ka9q/discovery.py
25
25
  ka9q/exceptions.py
26
+ ka9q/monitor.py
26
27
  ka9q/resequencer.py
27
28
  ka9q/rtp_recorder.py
28
29
  ka9q/stream.py
@@ -46,6 +47,7 @@ tests/test_ensure_channel_encoding.py
46
47
  tests/test_integration.py
47
48
  tests/test_iq_20khz_f32.py
48
49
  tests/test_listen_multicast.py
50
+ tests/test_monitor.py
49
51
  tests/test_multihomed.py
50
52
  tests/test_native_discovery.py
51
53
  tests/test_performance_fixes.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ka9q-python"
7
- version = "3.2.6"
7
+ version = "3.2.7"
8
8
  description = "Python interface for ka9q-radio control and monitoring"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,76 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+ import time
4
+ from ka9q.monitor import ChannelMonitor
5
+ from ka9q.discovery import ChannelInfo
6
+
7
+ class TestChannelMonitor(unittest.TestCase):
8
+ def setUp(self):
9
+ self.control = MagicMock()
10
+ # Mock ensure_channel to return a ChannelInfo object
11
+ self.control.ensure_channel.return_value = ChannelInfo(
12
+ ssrc=12345,
13
+ preset="usb",
14
+ sample_rate=12000,
15
+ frequency=14.074e6,
16
+ snr=30.0,
17
+ multicast_address="239.1.1.1",
18
+ port=5004
19
+ )
20
+ self.monitor = ChannelMonitor(self.control, check_interval=0.1)
21
+
22
+ def tearDown(self):
23
+ self.monitor.stop()
24
+
25
+ def test_monitor_registration(self):
26
+ """Verify channels are registered for monitoring"""
27
+ ssrc = self.monitor.monitor_channel(frequency_hz=14.074e6, preset="usb")
28
+
29
+ self.assertEqual(ssrc, 12345)
30
+ self.assertIn(12345, self.monitor._monitored_channels)
31
+ self.assertEqual(
32
+ self.monitor._monitored_channels[12345],
33
+ {'frequency_hz': 14.074e6, 'preset': 'usb'}
34
+ )
35
+
36
+ # Verify ensure_channel was called
37
+ self.control.ensure_channel.assert_called_with(frequency_hz=14.074e6, preset="usb")
38
+
39
+ def test_unmonitor(self):
40
+ """Verify unmonitoring works"""
41
+ ssrc = self.monitor.monitor_channel(frequency_hz=14.074e6)
42
+ self.monitor.unmonitor_channel(ssrc)
43
+ self.assertNotIn(ssrc, self.monitor._monitored_channels)
44
+
45
+ @patch('ka9q.monitor.discover_channels')
46
+ def test_recovery(self, mock_discover):
47
+ """Verify recovery triggers when channel is missing"""
48
+ # Register channel
49
+ self.monitor.monitor_channel(frequency_hz=14.074e6, preset="usb")
50
+ self.control.ensure_channel.reset_mock()
51
+
52
+ # Scenario 1: Channel exists
53
+ mock_discover.return_value = {
54
+ 12345: ChannelInfo(
55
+ ssrc=12345, frequency=14.074e6, preset="usb", sample_rate=12000,
56
+ snr=0, multicast_address="239.1.1.1", port=5004
57
+ )
58
+ }
59
+
60
+ # Run one loop cycle
61
+ self.monitor._check_and_recover()
62
+
63
+ # Should NOT call ensure_channel
64
+ self.control.ensure_channel.assert_not_called()
65
+
66
+ # Scenario 2: Channel missing (radiod restarted)
67
+ mock_discover.return_value = {} # Empty discovery
68
+
69
+ # Run one loop cycle
70
+ self.monitor._check_and_recover()
71
+
72
+ # Should call ensure_channel to restore it
73
+ self.control.ensure_channel.assert_called_with(frequency_hz=14.074e6, preset="usb")
74
+
75
+ if __name__ == '__main__':
76
+ 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