ka9q-python 3.3.0__tar.gz → 3.4.1__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.3.0/ka9q_python.egg-info → ka9q_python-3.4.1}/PKG-INFO +30 -10
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/README.md +29 -9
- ka9q_python-3.4.1/examples/diagnostics/diagnose_packets.py +215 -0
- ka9q_python-3.4.1/examples/diagnostics/repro_utc_bug.py +51 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/__init__.py +1 -1
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/control.py +271 -17
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/types.py +22 -9
- {ka9q_python-3.3.0 → ka9q_python-3.4.1/ka9q_python.egg-info}/PKG-INFO +30 -10
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/SOURCES.txt +3 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/pyproject.toml +1 -1
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/setup.py +1 -1
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/conftest.py +1 -1
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_channel_verification.py +55 -15
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_decode_functions.py +18 -0
- ka9q_python-3.4.1/tests/test_ttl_warning.py +49 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune.py +6 -5
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune_cli.py +14 -12
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/LICENSE +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/MANIFEST.in +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/discover_example.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/stream_example.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/test_improvements.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/tune.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/tune_example.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/addressing.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/discovery.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/monitor.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/stream.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/utils.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/setup.cfg +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/__init__.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_addressing.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_ensure_channel_encoding.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_integration.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_monitor.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_security_features.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune_live.py +0 -0
- {ka9q_python-3.3.0 → ka9q_python-3.4.1}/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.
|
|
3
|
+
Version: 3.4.1
|
|
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
|
|
@@ -82,13 +82,15 @@ pip install -e .
|
|
|
82
82
|
|
|
83
83
|
## Quick Start
|
|
84
84
|
|
|
85
|
+
> **Host selection:** All examples reference `bee1-hf-status.local`, which is the default integration test radiod in this repo. Replace it with your own radiod host or set `RADIOD_HOST`, `RADIOD_ADDRESS`, or the `--radiod-host` pytest option when running in other environments.
|
|
86
|
+
|
|
85
87
|
### Listen to AM Broadcast
|
|
86
88
|
|
|
87
89
|
```python
|
|
88
90
|
from ka9q import RadiodControl
|
|
89
91
|
|
|
90
|
-
# Connect to radiod
|
|
91
|
-
control = RadiodControl("
|
|
92
|
+
# Connect to radiod (default test host: bee1-hf-status.local)
|
|
93
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
92
94
|
|
|
93
95
|
# Create AM channel on 10 MHz WWV
|
|
94
96
|
control.create_channel(
|
|
@@ -105,7 +107,7 @@ control.create_channel(
|
|
|
105
107
|
```python
|
|
106
108
|
from ka9q import RadiodControl, Encoding
|
|
107
109
|
|
|
108
|
-
control = RadiodControl("
|
|
110
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
109
111
|
|
|
110
112
|
# Create a channel with 32-bit float output (highest quality)
|
|
111
113
|
control.ensure_channel(
|
|
@@ -122,7 +124,7 @@ control.ensure_channel(
|
|
|
122
124
|
```python
|
|
123
125
|
from ka9q import RadiodControl
|
|
124
126
|
|
|
125
|
-
control = RadiodControl("
|
|
127
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
126
128
|
|
|
127
129
|
wspr_bands = [
|
|
128
130
|
(1.8366e6, "160m"),
|
|
@@ -147,7 +149,7 @@ for freq, band in wspr_bands:
|
|
|
147
149
|
```python
|
|
148
150
|
from ka9q import discover_channels
|
|
149
151
|
|
|
150
|
-
channels = discover_channels("
|
|
152
|
+
channels = discover_channels("bee1-hf-status.local")
|
|
151
153
|
for ssrc, info in channels.items():
|
|
152
154
|
print(f"{ssrc}: {info.frequency/1e6:.3f} MHz, {info.preset}, {info.sample_rate} Hz")
|
|
153
155
|
```
|
|
@@ -159,7 +161,7 @@ from ka9q import discover_channels, RTPRecorder
|
|
|
159
161
|
import time
|
|
160
162
|
|
|
161
163
|
# Get channel with timing info
|
|
162
|
-
channels = discover_channels("
|
|
164
|
+
channels = discover_channels("bee1-hf-status.local")
|
|
163
165
|
channel = channels[14074000]
|
|
164
166
|
|
|
165
167
|
# Define packet handler
|
|
@@ -186,10 +188,10 @@ from ka9q import RadiodControl, discover_channels
|
|
|
186
188
|
my_interface = "192.168.1.100"
|
|
187
189
|
|
|
188
190
|
# Create control with specific interface
|
|
189
|
-
control = RadiodControl("
|
|
191
|
+
control = RadiodControl("bee1-hf-status.local", interface=my_interface)
|
|
190
192
|
|
|
191
193
|
# Discovery on specific interface
|
|
192
|
-
channels = discover_channels("
|
|
194
|
+
channels = discover_channels("bee1-hf-status.local", interface=my_interface)
|
|
193
195
|
```
|
|
194
196
|
|
|
195
197
|
### Automatic Channel Recovery
|
|
@@ -199,7 +201,7 @@ ensure your channels survive radiod restarts:
|
|
|
199
201
|
```python
|
|
200
202
|
from ka9q import RadiodControl, ChannelMonitor
|
|
201
203
|
|
|
202
|
-
control = RadiodControl("
|
|
204
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
203
205
|
monitor = ChannelMonitor(control)
|
|
204
206
|
monitor.start()
|
|
205
207
|
|
|
@@ -209,8 +211,26 @@ monitor.monitor_channel(
|
|
|
209
211
|
preset="usb",
|
|
210
212
|
sample_rate=12000
|
|
211
213
|
)
|
|
214
|
+
|
|
215
|
+
### Channel Cleanup (frequency = 0)
|
|
216
|
+
|
|
217
|
+
`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:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
with RadiodControl("bee1-hf-status.local") as control:
|
|
221
|
+
info = control.ensure_channel(
|
|
222
|
+
frequency_hz=10e6,
|
|
223
|
+
preset="iq",
|
|
224
|
+
sample_rate=16000
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# ... use channel ...
|
|
228
|
+
|
|
229
|
+
control.remove_channel(info.ssrc) # marks frequency=0
|
|
212
230
|
```
|
|
213
231
|
|
|
232
|
+
> Note: `remove_channel()` finishes instantly on the client; radiod’s poller typically purges the channel within the next second.
|
|
233
|
+
|
|
214
234
|
## Documentation
|
|
215
235
|
|
|
216
236
|
For detailed information, please refer to the documentation in the `docs/` directory:
|
|
@@ -47,13 +47,15 @@ pip install -e .
|
|
|
47
47
|
|
|
48
48
|
## Quick Start
|
|
49
49
|
|
|
50
|
+
> **Host selection:** All examples reference `bee1-hf-status.local`, which is the default integration test radiod in this repo. Replace it with your own radiod host or set `RADIOD_HOST`, `RADIOD_ADDRESS`, or the `--radiod-host` pytest option when running in other environments.
|
|
51
|
+
|
|
50
52
|
### Listen to AM Broadcast
|
|
51
53
|
|
|
52
54
|
```python
|
|
53
55
|
from ka9q import RadiodControl
|
|
54
56
|
|
|
55
|
-
# Connect to radiod
|
|
56
|
-
control = RadiodControl("
|
|
57
|
+
# Connect to radiod (default test host: bee1-hf-status.local)
|
|
58
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
57
59
|
|
|
58
60
|
# Create AM channel on 10 MHz WWV
|
|
59
61
|
control.create_channel(
|
|
@@ -70,7 +72,7 @@ control.create_channel(
|
|
|
70
72
|
```python
|
|
71
73
|
from ka9q import RadiodControl, Encoding
|
|
72
74
|
|
|
73
|
-
control = RadiodControl("
|
|
75
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
74
76
|
|
|
75
77
|
# Create a channel with 32-bit float output (highest quality)
|
|
76
78
|
control.ensure_channel(
|
|
@@ -87,7 +89,7 @@ control.ensure_channel(
|
|
|
87
89
|
```python
|
|
88
90
|
from ka9q import RadiodControl
|
|
89
91
|
|
|
90
|
-
control = RadiodControl("
|
|
92
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
91
93
|
|
|
92
94
|
wspr_bands = [
|
|
93
95
|
(1.8366e6, "160m"),
|
|
@@ -112,7 +114,7 @@ for freq, band in wspr_bands:
|
|
|
112
114
|
```python
|
|
113
115
|
from ka9q import discover_channels
|
|
114
116
|
|
|
115
|
-
channels = discover_channels("
|
|
117
|
+
channels = discover_channels("bee1-hf-status.local")
|
|
116
118
|
for ssrc, info in channels.items():
|
|
117
119
|
print(f"{ssrc}: {info.frequency/1e6:.3f} MHz, {info.preset}, {info.sample_rate} Hz")
|
|
118
120
|
```
|
|
@@ -124,7 +126,7 @@ from ka9q import discover_channels, RTPRecorder
|
|
|
124
126
|
import time
|
|
125
127
|
|
|
126
128
|
# Get channel with timing info
|
|
127
|
-
channels = discover_channels("
|
|
129
|
+
channels = discover_channels("bee1-hf-status.local")
|
|
128
130
|
channel = channels[14074000]
|
|
129
131
|
|
|
130
132
|
# Define packet handler
|
|
@@ -151,10 +153,10 @@ from ka9q import RadiodControl, discover_channels
|
|
|
151
153
|
my_interface = "192.168.1.100"
|
|
152
154
|
|
|
153
155
|
# Create control with specific interface
|
|
154
|
-
control = RadiodControl("
|
|
156
|
+
control = RadiodControl("bee1-hf-status.local", interface=my_interface)
|
|
155
157
|
|
|
156
158
|
# Discovery on specific interface
|
|
157
|
-
channels = discover_channels("
|
|
159
|
+
channels = discover_channels("bee1-hf-status.local", interface=my_interface)
|
|
158
160
|
```
|
|
159
161
|
|
|
160
162
|
### Automatic Channel Recovery
|
|
@@ -164,7 +166,7 @@ ensure your channels survive radiod restarts:
|
|
|
164
166
|
```python
|
|
165
167
|
from ka9q import RadiodControl, ChannelMonitor
|
|
166
168
|
|
|
167
|
-
control = RadiodControl("
|
|
169
|
+
control = RadiodControl("bee1-hf-status.local")
|
|
168
170
|
monitor = ChannelMonitor(control)
|
|
169
171
|
monitor.start()
|
|
170
172
|
|
|
@@ -174,8 +176,26 @@ monitor.monitor_channel(
|
|
|
174
176
|
preset="usb",
|
|
175
177
|
sample_rate=12000
|
|
176
178
|
)
|
|
179
|
+
|
|
180
|
+
### Channel Cleanup (frequency = 0)
|
|
181
|
+
|
|
182
|
+
`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:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
with RadiodControl("bee1-hf-status.local") as control:
|
|
186
|
+
info = control.ensure_channel(
|
|
187
|
+
frequency_hz=10e6,
|
|
188
|
+
preset="iq",
|
|
189
|
+
sample_rate=16000
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# ... use channel ...
|
|
193
|
+
|
|
194
|
+
control.remove_channel(info.ssrc) # marks frequency=0
|
|
177
195
|
```
|
|
178
196
|
|
|
197
|
+
> Note: `remove_channel()` finishes instantly on the client; radiod’s poller typically purges the channel within the next second.
|
|
198
|
+
|
|
179
199
|
## Documentation
|
|
180
200
|
|
|
181
201
|
For detailed information, please refer to the documentation in the `docs/` directory:
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Diagnostic script to analyze RTP packet sizes from radiod.
|
|
4
|
+
|
|
5
|
+
This script captures packets and reports:
|
|
6
|
+
- Actual payload sizes received
|
|
7
|
+
- Expected vs actual sample counts
|
|
8
|
+
- Any variability in packet sizes
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import socket
|
|
12
|
+
import struct
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from ka9q import discover_channels
|
|
17
|
+
|
|
18
|
+
def parse_rtp_header(data: bytes):
|
|
19
|
+
"""Parse RTP header, return (header_len, ssrc, timestamp, sequence)"""
|
|
20
|
+
if len(data) < 12:
|
|
21
|
+
return None
|
|
22
|
+
first_byte = data[0]
|
|
23
|
+
csrc_count = first_byte & 0x0F
|
|
24
|
+
sequence = struct.unpack('>H', data[2:4])[0]
|
|
25
|
+
timestamp = struct.unpack('>I', data[4:8])[0]
|
|
26
|
+
ssrc = struct.unpack('>I', data[8:12])[0]
|
|
27
|
+
header_len = 12 + (4 * csrc_count)
|
|
28
|
+
return header_len, ssrc, timestamp, sequence
|
|
29
|
+
|
|
30
|
+
def diagnose_stream(radiod_address: str, ssrc: int = None, duration: float = 10.0):
|
|
31
|
+
"""
|
|
32
|
+
Capture packets from a radiod stream and analyze sizes.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
radiod_address: Address of radiod (e.g., 'radiod.local')
|
|
36
|
+
ssrc: Specific SSRC to filter (None = first found)
|
|
37
|
+
duration: How long to capture (seconds)
|
|
38
|
+
"""
|
|
39
|
+
print(f"Discovering channels on {radiod_address}...")
|
|
40
|
+
channels = discover_channels(radiod_address, timeout=5.0)
|
|
41
|
+
|
|
42
|
+
if not channels:
|
|
43
|
+
print("ERROR: No channels found!")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print(f"Found {len(channels)} channel(s):")
|
|
47
|
+
for ch_ssrc, ch in channels.items():
|
|
48
|
+
print(f" SSRC {ch_ssrc}: {ch.frequency/1e6:.3f} MHz, {ch.preset}, {ch.sample_rate} Hz")
|
|
49
|
+
|
|
50
|
+
# Select channel
|
|
51
|
+
if ssrc is None:
|
|
52
|
+
ssrc = list(channels.keys())[0]
|
|
53
|
+
print(f"\nUsing first channel: SSRC {ssrc}")
|
|
54
|
+
|
|
55
|
+
if ssrc not in channels:
|
|
56
|
+
print(f"ERROR: SSRC {ssrc} not found!")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
channel = channels[ssrc]
|
|
60
|
+
print(f"\nChannel details:")
|
|
61
|
+
print(f" Frequency: {channel.frequency/1e6:.6f} MHz")
|
|
62
|
+
print(f" Preset: {channel.preset}")
|
|
63
|
+
print(f" Sample rate: {channel.sample_rate} Hz")
|
|
64
|
+
print(f" Multicast: {channel.multicast_address}:{channel.port}")
|
|
65
|
+
|
|
66
|
+
# Calculate expected payload size
|
|
67
|
+
is_iq = channel.preset.lower() in ('iq', 'spectrum')
|
|
68
|
+
|
|
69
|
+
# radiod typically sends 20ms of audio or equivalent IQ
|
|
70
|
+
# At 16kHz audio: 320 samples * 4 bytes = 1280 bytes
|
|
71
|
+
# At 16kHz IQ: 160 complex samples * 8 bytes = 1280 bytes (or 320 floats * 4 = 1280)
|
|
72
|
+
expected_samples_per_20ms = channel.sample_rate // 50 # 20ms worth
|
|
73
|
+
|
|
74
|
+
if is_iq:
|
|
75
|
+
# IQ: complex64 = 8 bytes per sample
|
|
76
|
+
expected_payload_bytes = expected_samples_per_20ms * 8
|
|
77
|
+
sample_type = "complex64 (IQ)"
|
|
78
|
+
else:
|
|
79
|
+
# Audio: float32 = 4 bytes per sample
|
|
80
|
+
expected_payload_bytes = expected_samples_per_20ms * 4
|
|
81
|
+
sample_type = "float32 (audio)"
|
|
82
|
+
|
|
83
|
+
print(f"\nExpected packet structure:")
|
|
84
|
+
print(f" Sample type: {sample_type}")
|
|
85
|
+
print(f" Samples per 20ms: {expected_samples_per_20ms}")
|
|
86
|
+
print(f" Expected payload: {expected_payload_bytes} bytes")
|
|
87
|
+
|
|
88
|
+
# Create socket
|
|
89
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
90
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
91
|
+
sock.bind(('', channel.port))
|
|
92
|
+
|
|
93
|
+
# Join multicast
|
|
94
|
+
mreq = struct.pack('4s4s',
|
|
95
|
+
socket.inet_aton(channel.multicast_address),
|
|
96
|
+
socket.inet_aton('0.0.0.0'))
|
|
97
|
+
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
98
|
+
sock.settimeout(1.0)
|
|
99
|
+
|
|
100
|
+
print(f"\nCapturing packets for {duration} seconds...")
|
|
101
|
+
|
|
102
|
+
# Collect stats
|
|
103
|
+
payload_sizes = Counter()
|
|
104
|
+
total_packets = 0
|
|
105
|
+
filtered_packets = 0
|
|
106
|
+
timestamps = []
|
|
107
|
+
sequences = []
|
|
108
|
+
|
|
109
|
+
start_time = time.time()
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
while time.time() - start_time < duration:
|
|
113
|
+
try:
|
|
114
|
+
data, addr = sock.recvfrom(8192)
|
|
115
|
+
total_packets += 1
|
|
116
|
+
|
|
117
|
+
parsed = parse_rtp_header(data)
|
|
118
|
+
if parsed is None:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
header_len, pkt_ssrc, timestamp, sequence = parsed
|
|
122
|
+
|
|
123
|
+
# Filter by SSRC
|
|
124
|
+
if pkt_ssrc != ssrc:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
filtered_packets += 1
|
|
128
|
+
payload = data[header_len:]
|
|
129
|
+
payload_sizes[len(payload)] += 1
|
|
130
|
+
timestamps.append(timestamp)
|
|
131
|
+
sequences.append(sequence)
|
|
132
|
+
|
|
133
|
+
except socket.timeout:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
finally:
|
|
137
|
+
sock.close()
|
|
138
|
+
|
|
139
|
+
# Report results
|
|
140
|
+
print(f"\n{'='*60}")
|
|
141
|
+
print("RESULTS")
|
|
142
|
+
print(f"{'='*60}")
|
|
143
|
+
print(f"Total packets received: {total_packets}")
|
|
144
|
+
print(f"Packets for SSRC {ssrc}: {filtered_packets}")
|
|
145
|
+
|
|
146
|
+
if not payload_sizes:
|
|
147
|
+
print("ERROR: No packets captured for this SSRC!")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
print(f"\nPayload size distribution:")
|
|
151
|
+
for size, count in sorted(payload_sizes.items()):
|
|
152
|
+
pct = 100 * count / filtered_packets
|
|
153
|
+
|
|
154
|
+
# Calculate what this size means
|
|
155
|
+
if is_iq:
|
|
156
|
+
samples = size // 8 # complex64
|
|
157
|
+
floats = size // 4
|
|
158
|
+
interpretation = f"{samples} complex samples ({floats} floats)"
|
|
159
|
+
else:
|
|
160
|
+
samples = size // 4 # float32
|
|
161
|
+
interpretation = f"{samples} float32 samples"
|
|
162
|
+
|
|
163
|
+
match = "✓ EXPECTED" if size == expected_payload_bytes else "⚠ UNEXPECTED"
|
|
164
|
+
print(f" {size:5d} bytes: {count:6d} packets ({pct:5.1f}%) - {interpretation} {match}")
|
|
165
|
+
|
|
166
|
+
# Timestamp analysis
|
|
167
|
+
if len(timestamps) > 1:
|
|
168
|
+
print(f"\nTimestamp analysis:")
|
|
169
|
+
ts_diffs = []
|
|
170
|
+
for i in range(1, len(timestamps)):
|
|
171
|
+
# Handle 32-bit wraparound
|
|
172
|
+
diff = (timestamps[i] - timestamps[i-1]) & 0xFFFFFFFF
|
|
173
|
+
if diff > 0x80000000:
|
|
174
|
+
diff -= 0x100000000
|
|
175
|
+
ts_diffs.append(diff)
|
|
176
|
+
|
|
177
|
+
ts_diff_counts = Counter(ts_diffs)
|
|
178
|
+
print(f" Timestamp increments (samples between packets):")
|
|
179
|
+
for diff, count in sorted(ts_diff_counts.items()):
|
|
180
|
+
pct = 100 * count / len(ts_diffs)
|
|
181
|
+
duration_ms = 1000 * diff / channel.sample_rate
|
|
182
|
+
print(f" {diff:6d} samples ({duration_ms:6.2f} ms): {count:5d} ({pct:5.1f}%)")
|
|
183
|
+
|
|
184
|
+
# Sequence analysis
|
|
185
|
+
if len(sequences) > 1:
|
|
186
|
+
print(f"\nSequence analysis:")
|
|
187
|
+
seq_gaps = 0
|
|
188
|
+
for i in range(1, len(sequences)):
|
|
189
|
+
expected = (sequences[i-1] + 1) & 0xFFFF
|
|
190
|
+
if sequences[i] != expected:
|
|
191
|
+
seq_gaps += 1
|
|
192
|
+
print(f" Sequence gaps detected: {seq_gaps}")
|
|
193
|
+
|
|
194
|
+
print(f"\n{'='*60}")
|
|
195
|
+
if len(payload_sizes) == 1 and list(payload_sizes.keys())[0] == expected_payload_bytes:
|
|
196
|
+
print("✓ All packets have expected size - radiod output looks correct")
|
|
197
|
+
else:
|
|
198
|
+
print("⚠ Variable or unexpected packet sizes detected!")
|
|
199
|
+
print(" This may indicate:")
|
|
200
|
+
print(" - Mismatched samples_per_packet configuration")
|
|
201
|
+
print(" - Different encoding than expected")
|
|
202
|
+
print(" - Multiple streams on same multicast group")
|
|
203
|
+
|
|
204
|
+
if __name__ == '__main__':
|
|
205
|
+
if len(sys.argv) < 2:
|
|
206
|
+
print("Usage: python diagnose_packets.py <radiod_address> [ssrc] [duration]")
|
|
207
|
+
print("Example: python diagnose_packets.py radiod.local")
|
|
208
|
+
print("Example: python diagnose_packets.py radiod.local 10000000 30")
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
radiod = sys.argv[1]
|
|
212
|
+
ssrc = int(sys.argv[2]) if len(sys.argv) > 2 else None
|
|
213
|
+
duration = float(sys.argv[3]) if len(sys.argv) > 3 else 10.0
|
|
214
|
+
|
|
215
|
+
diagnose_stream(radiod, ssrc, duration)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
from ka9q.rtp_recorder import rtp_to_wallclock
|
|
3
|
+
from ka9q.discovery import ChannelInfo
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
def test_utc_conversion():
|
|
7
|
+
# Current Leap Seconds (GPS - UTC) = 18 seconds
|
|
8
|
+
LEAP_SECONDS = 18
|
|
9
|
+
GPS_UTC_OFFSET = 315964800
|
|
10
|
+
|
|
11
|
+
# Pick a known UTC time: 2024-01-01 00:00:00 UTC
|
|
12
|
+
# Unix timestamp: 1704067200
|
|
13
|
+
target_unix_time = 1704067200
|
|
14
|
+
|
|
15
|
+
# Calculate corresponding GPS time
|
|
16
|
+
# GPS = unix - offset + leap_seconds
|
|
17
|
+
gps_time_seconds = target_unix_time - GPS_UTC_OFFSET + LEAP_SECONDS
|
|
18
|
+
gps_time_ns = gps_time_seconds * 1_000_000_000
|
|
19
|
+
|
|
20
|
+
print(f"Target Unix Time: {target_unix_time}")
|
|
21
|
+
print(f"Calculated GPS Time (s): {gps_time_seconds}")
|
|
22
|
+
|
|
23
|
+
channel = ChannelInfo(
|
|
24
|
+
ssrc=1234,
|
|
25
|
+
preset="test",
|
|
26
|
+
sample_rate=48000,
|
|
27
|
+
frequency=100.0,
|
|
28
|
+
snr=0.0,
|
|
29
|
+
multicast_address="239.1.2.3",
|
|
30
|
+
port=5004,
|
|
31
|
+
gps_time=gps_time_ns,
|
|
32
|
+
rtp_timesnap=1000
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# helper returns float seconds
|
|
36
|
+
calculated_unix_time = rtp_to_wallclock(1000, channel)
|
|
37
|
+
|
|
38
|
+
print(f"Function Result: {calculated_unix_time}")
|
|
39
|
+
|
|
40
|
+
diff = calculated_unix_time - target_unix_time
|
|
41
|
+
print(f"Difference (Result - Target): {diff} seconds")
|
|
42
|
+
|
|
43
|
+
if abs(diff - 18.0) < 0.001:
|
|
44
|
+
print("FAIL: Result is 18 seconds ahead (Missing leap second correction).")
|
|
45
|
+
elif abs(diff) < 0.001:
|
|
46
|
+
print("PASS: Result matches target UTC time.")
|
|
47
|
+
else:
|
|
48
|
+
print(f"FAIL: Unexpected difference: {diff}")
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
test_utc_conversion()
|