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.
- {ka9q_python-3.2.6/ka9q_python.egg-info → ka9q_python-3.2.7}/PKG-INFO +20 -1
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/README.md +19 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/__init__.py +3 -1
- ka9q_python-3.2.7/ka9q/monitor.py +150 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7/ka9q_python.egg-info}/PKG-INFO +20 -1
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/SOURCES.txt +2 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/pyproject.toml +1 -1
- ka9q_python-3.2.7/tests/test_monitor.py +76 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/LICENSE +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/MANIFEST.in +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/discover_example.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/stream_example.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/test_improvements.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/tune.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/examples/tune_example.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/addressing.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/control.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/discovery.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/stream.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/types.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q/utils.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/setup.cfg +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/setup.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/__init__.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/conftest.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_addressing.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_channel_verification.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_ensure_channel_encoding.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_integration.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_security_features.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.2.6 → ka9q_python-3.2.7}/tests/test_tune_live.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|