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.
Files changed (70) hide show
  1. {ka9q_python-3.3.0/ka9q_python.egg-info → ka9q_python-3.4.1}/PKG-INFO +30 -10
  2. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/README.md +29 -9
  3. ka9q_python-3.4.1/examples/diagnostics/diagnose_packets.py +215 -0
  4. ka9q_python-3.4.1/examples/diagnostics/repro_utc_bug.py +51 -0
  5. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/__init__.py +1 -1
  6. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/control.py +271 -17
  7. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/types.py +22 -9
  8. {ka9q_python-3.3.0 → ka9q_python-3.4.1/ka9q_python.egg-info}/PKG-INFO +30 -10
  9. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/SOURCES.txt +3 -0
  10. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/pyproject.toml +1 -1
  11. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/setup.py +1 -1
  12. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/conftest.py +1 -1
  13. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_channel_verification.py +55 -15
  14. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_decode_functions.py +18 -0
  15. ka9q_python-3.4.1/tests/test_ttl_warning.py +49 -0
  16. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune.py +6 -5
  17. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune_cli.py +14 -12
  18. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/LICENSE +0 -0
  19. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/MANIFEST.in +0 -0
  20. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/advanced_features_demo.py +0 -0
  21. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/channel_cleanup_example.py +0 -0
  22. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/codar_oceanography.py +0 -0
  23. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/discover_example.py +0 -0
  24. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/grape_integration_example.py +0 -0
  25. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/hf_band_scanner.py +0 -0
  26. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/rtp_recorder_example.py +0 -0
  27. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/simple_am_radio.py +0 -0
  28. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/stream_example.py +0 -0
  29. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/superdarn_recorder.py +0 -0
  30. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/test_channel_operations.py +0 -0
  31. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/test_improvements.py +0 -0
  32. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/test_timing_fields.py +0 -0
  33. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/tune.py +0 -0
  34. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/examples/tune_example.py +0 -0
  35. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/addressing.py +0 -0
  36. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/discovery.py +0 -0
  37. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/exceptions.py +0 -0
  38. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/managed_stream.py +0 -0
  39. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/monitor.py +0 -0
  40. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/resequencer.py +0 -0
  41. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/rtp_recorder.py +0 -0
  42. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/stream.py +0 -0
  43. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/stream_quality.py +0 -0
  44. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q/utils.py +0 -0
  45. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/dependency_links.txt +0 -0
  46. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/requires.txt +0 -0
  47. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/ka9q_python.egg-info/top_level.txt +0 -0
  48. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/setup.cfg +0 -0
  49. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/__init__.py +0 -0
  50. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_addressing.py +0 -0
  51. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_create_split_encoding.py +0 -0
  52. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_encode_functions.py +0 -0
  53. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_encode_socket.py +0 -0
  54. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_ensure_channel_encoding.py +0 -0
  55. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_integration.py +0 -0
  56. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_iq_20khz_f32.py +0 -0
  57. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_listen_multicast.py +0 -0
  58. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_managed_stream_recovery.py +0 -0
  59. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_monitor.py +0 -0
  60. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_multihomed.py +0 -0
  61. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_native_discovery.py +0 -0
  62. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_performance_fixes.py +0 -0
  63. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_remove_channel.py +0 -0
  64. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_rtp_recorder.py +0 -0
  65. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_security_features.py +0 -0
  66. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_ssrc_dest_unit.py +0 -0
  67. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_ssrc_encoding_unit.py +0 -0
  68. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune_debug.py +0 -0
  69. {ka9q_python-3.3.0 → ka9q_python-3.4.1}/tests/test_tune_live.py +0 -0
  70. {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.0
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local", interface=my_interface)
191
+ control = RadiodControl("bee1-hf-status.local", interface=my_interface)
190
192
 
191
193
  # Discovery on specific interface
192
- channels = discover_channels("radiod.local", interface=my_interface)
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local")
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("radiod.local", interface=my_interface)
156
+ control = RadiodControl("bee1-hf-status.local", interface=my_interface)
155
157
 
156
158
  # Discovery on specific interface
157
- channels = discover_channels("radiod.local", interface=my_interface)
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("radiod.local")
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()
@@ -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.2.7'
59
+ __version__ = '3.4.1'
60
60
  __author__ = 'Michael Hauan AC0G'
61
61
 
62
62
  from .control import RadiodControl, allocate_ssrc