ka9q-python 3.4.0__tar.gz → 3.4.2__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.4.0/ka9q_python.egg-info → ka9q_python-3.4.2}/PKG-INFO +2 -1
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/README.md +1 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/stream_example.py +4 -6
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/__init__.py +1 -1
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/control.py +21 -11
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/discovery.py +3 -1
- {ka9q_python-3.4.0 → ka9q_python-3.4.2/ka9q_python.egg-info}/PKG-INFO +2 -1
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q_python.egg-info/SOURCES.txt +1 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/pyproject.toml +1 -1
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/setup.py +1 -1
- ka9q_python-3.4.2/tests/test_decode_description.py +46 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/LICENSE +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/MANIFEST.in +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/diagnostics/diagnose_packets.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/diagnostics/repro_utc_bug.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/discover_example.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/test_improvements.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/tune.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/examples/tune_example.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/addressing.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/monitor.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/stream.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/types.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q/utils.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/setup.cfg +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/__init__.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/conftest.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_addressing.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_channel_verification.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_ensure_channel_encoding.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_integration.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_monitor.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_security_features.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_ttl_warning.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_tune.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/tests/test_tune_live.py +0 -0
- {ka9q_python-3.4.0 → ka9q_python-3.4.2}/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.4.
|
|
3
|
+
Version: 3.4.2
|
|
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
|
|
@@ -48,6 +48,7 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
48
48
|
|
|
49
49
|
- [Features](#features)
|
|
50
50
|
- [Installation](#installation)
|
|
51
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
51
52
|
- [Quick Start](#quick-start)
|
|
52
53
|
- [Documentation](#documentation)
|
|
53
54
|
- [Examples](#examples)
|
|
@@ -13,6 +13,7 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
13
13
|
|
|
14
14
|
- [Features](#features)
|
|
15
15
|
- [Installation](#installation)
|
|
16
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
16
17
|
- [Quick Start](#quick-start)
|
|
17
18
|
- [Documentation](#documentation)
|
|
18
19
|
- [Examples](#examples)
|
|
@@ -123,15 +123,13 @@ def main():
|
|
|
123
123
|
if args.discover:
|
|
124
124
|
print("Discovering channels...")
|
|
125
125
|
channels = discover_channels(timeout=3.0)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
channel = ch
|
|
130
|
-
break
|
|
126
|
+
|
|
127
|
+
# FIX: Properly access the channel from the dictionary
|
|
128
|
+
channel = channels.get(args.ssrc)
|
|
131
129
|
|
|
132
130
|
if channel is None:
|
|
133
131
|
print(f"SSRC {args.ssrc} not found in discovered channels")
|
|
134
|
-
print(f"Available SSRCs: {
|
|
132
|
+
print(f"Available SSRCs: {list(channels.keys())}")
|
|
135
133
|
return 1
|
|
136
134
|
|
|
137
135
|
print(f"Found channel: {channel.frequency/1e6:.3f} MHz, {channel.sample_rate} Hz")
|
|
@@ -25,6 +25,7 @@ import struct
|
|
|
25
25
|
import secrets
|
|
26
26
|
import logging
|
|
27
27
|
import threading
|
|
28
|
+
import hashlib
|
|
28
29
|
import re
|
|
29
30
|
import time
|
|
30
31
|
from dataclasses import dataclass, field
|
|
@@ -100,18 +101,25 @@ def allocate_ssrc(
|
|
|
100
101
|
>>> ssrc2 = allocate_ssrc(10.0e6, "iq", 16000)
|
|
101
102
|
>>> assert ssrc == ssrc2
|
|
102
103
|
"""
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
# Create a stable string key from parameters
|
|
105
|
+
# The format and rounding match signal-recorder's StreamSpec hash logic.
|
|
106
|
+
key_str = (
|
|
107
|
+
f"{round(frequency_hz)}|" # Frequency rounded to nearest Hz
|
|
108
|
+
f"{preset.lower()}|" # Preset normalized to lowercase
|
|
109
|
+
f"{sample_rate}|" # Sample rate
|
|
110
|
+
f"{'1' if agc else '0'}|" # AGC as 1/0
|
|
111
|
+
f"{round(gain, 1)}|" # Gain rounded to 0.1 dB
|
|
112
|
+
f"{destination or ''}|" # Destination (empty string if None)
|
|
113
|
+
f"{encoding}" # Encoding type
|
|
112
114
|
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
|
|
116
|
+
# Use SHA-256 for a stable, platform-independent hash
|
|
117
|
+
h = hashlib.sha256(key_str.encode()).digest()
|
|
118
|
+
|
|
119
|
+
# Convert first 4 bytes to integer (big-endian)
|
|
120
|
+
# Keep positive and 31 bits to match signal-recorder's SSRC range
|
|
121
|
+
ssrc_full = int.from_bytes(h[:4], byteorder='big')
|
|
122
|
+
return ssrc_full & 0x7FFFFFFF
|
|
115
123
|
|
|
116
124
|
|
|
117
125
|
# Input validation functions
|
|
@@ -1964,6 +1972,8 @@ class RadiodControl:
|
|
|
1964
1972
|
status['rf_atten'] = decode_float(data, optlen)
|
|
1965
1973
|
elif type_val == StatusType.RF_AGC:
|
|
1966
1974
|
status['rf_agc'] = decode_int(data, optlen)
|
|
1975
|
+
elif type_val == StatusType.DESCRIPTION:
|
|
1976
|
+
status['description'] = decode_string(data, optlen)
|
|
1967
1977
|
elif type_val == StatusType.PRESET:
|
|
1968
1978
|
status['preset'] = decode_string(data, optlen)
|
|
1969
1979
|
elif type_val == StatusType.LOW_EDGE:
|
|
@@ -33,6 +33,7 @@ class ChannelInfo:
|
|
|
33
33
|
gps_time: Optional[int] = None # GPS nanoseconds when RTP_TIMESNAP was captured
|
|
34
34
|
rtp_timesnap: Optional[int] = None # RTP timestamp at GPS_TIME
|
|
35
35
|
encoding: int = 0 # stream encoding (0=none, 4=F32, etc)
|
|
36
|
+
description: Optional[str] = None # SDR hardware description
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def _create_status_listener_socket(multicast_addr: str, interface: Optional[str] = None) -> socket.socket:
|
|
@@ -208,7 +209,8 @@ def discover_channels_native(status_address: str, listen_duration: float = 2.0,
|
|
|
208
209
|
port=port,
|
|
209
210
|
gps_time=status.get('gps_time'),
|
|
210
211
|
rtp_timesnap=status.get('rtp_timesnap'),
|
|
211
|
-
encoding=status.get('encoding', 0)
|
|
212
|
+
encoding=status.get('encoding', 0),
|
|
213
|
+
description=status.get('description')
|
|
212
214
|
)
|
|
213
215
|
|
|
214
216
|
# Store or update channel info
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ka9q-python
|
|
3
|
-
Version: 3.4.
|
|
3
|
+
Version: 3.4.2
|
|
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
|
|
@@ -48,6 +48,7 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
48
48
|
|
|
49
49
|
- [Features](#features)
|
|
50
50
|
- [Installation](#installation)
|
|
51
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
51
52
|
- [Quick Start](#quick-start)
|
|
52
53
|
- [Documentation](#documentation)
|
|
53
54
|
- [Examples](#examples)
|
|
@@ -12,7 +12,7 @@ long_description = readme.read_text() if readme.exists() else ''
|
|
|
12
12
|
|
|
13
13
|
setup(
|
|
14
14
|
name='ka9q-python',
|
|
15
|
-
version='3.4.
|
|
15
|
+
version='3.4.2',
|
|
16
16
|
description='Python interface for ka9q-radio control and monitoring',
|
|
17
17
|
long_description=long_description,
|
|
18
18
|
long_description_content_type='text/markdown',
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch, MagicMock
|
|
5
|
+
|
|
6
|
+
# Add parent directory to path
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
8
|
+
|
|
9
|
+
from ka9q.control import RadiodControl
|
|
10
|
+
from ka9q.types import StatusType
|
|
11
|
+
|
|
12
|
+
class TestDescriptionDecoding(unittest.TestCase):
|
|
13
|
+
"""Test SDR description decoding"""
|
|
14
|
+
|
|
15
|
+
@patch('ka9q.control.RadiodControl._connect', return_value=None)
|
|
16
|
+
def test_decode_description(self, mock_connect):
|
|
17
|
+
"""Verify that _decode_status_response decodes StatusType.DESCRIPTION correctly"""
|
|
18
|
+
# Create a RadiodControl instance (now safe due to mock)
|
|
19
|
+
control = RadiodControl("radiod.local")
|
|
20
|
+
|
|
21
|
+
# Manually set attributes that __init__ might not have set due to mocked _connect
|
|
22
|
+
control.status_mcast_addr = "239.1.1.1"
|
|
23
|
+
control.metrics = MagicMock()
|
|
24
|
+
# Create a dummy status packet with a description
|
|
25
|
+
# Format: Packet Type (0), Tag (4), Length (N), Data (SDR Name)
|
|
26
|
+
description = "AirspyHF+"
|
|
27
|
+
desc_bytes = description.encode('utf-8')
|
|
28
|
+
desc_len = len(desc_bytes)
|
|
29
|
+
|
|
30
|
+
# Construct buffer
|
|
31
|
+
# 0x00: Status packet type
|
|
32
|
+
# 0x04: StatusType.DESCRIPTION
|
|
33
|
+
# desc_len: Length
|
|
34
|
+
# desc_bytes: SDR Name
|
|
35
|
+
# 0xff: StatusType.EOL
|
|
36
|
+
buffer = bytes([0, StatusType.DESCRIPTION, desc_len]) + desc_bytes + bytes([StatusType.EOL])
|
|
37
|
+
|
|
38
|
+
# Decode
|
|
39
|
+
status = control._decode_status_response(buffer)
|
|
40
|
+
|
|
41
|
+
# Verify
|
|
42
|
+
self.assertIn('description', status)
|
|
43
|
+
self.assertEqual(status['description'], description)
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|