ka9q-python 3.4.1__tar.gz → 3.5.0__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.1 → ka9q_python-3.5.0}/MANIFEST.in +6 -0
- {ka9q_python-3.4.1/ka9q_python.egg-info → ka9q_python-3.5.0}/PKG-INFO +2 -1
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/README.md +1 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/stream_example.py +4 -6
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/__init__.py +2 -1
- ka9q_python-3.5.0/ka9q/compat.py +15 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/control.py +15 -10
- ka9q_python-3.5.0/ka9q/types.py +158 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0/ka9q_python.egg-info}/PKG-INFO +2 -1
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/SOURCES.txt +4 -0
- ka9q_python-3.5.0/ka9q_radio_compat +3 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/pyproject.toml +1 -1
- ka9q_python-3.5.0/scripts/sync_types.py +402 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/setup.py +1 -1
- ka9q_python-3.5.0/tests/test_protocol_compat.py +82 -0
- ka9q_python-3.4.1/ka9q/types.py +0 -175
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/LICENSE +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/diagnostics/diagnose_packets.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/diagnostics/repro_utc_bug.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/discover_example.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/test_improvements.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/tune.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/tune_example.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/addressing.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/discovery.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/monitor.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/stream.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/utils.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/requires.txt +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/setup.cfg +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/__init__.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/conftest.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_addressing.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_channel_verification.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ensure_channel_encoding.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_integration.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_monitor.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_security_features.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ttl_warning.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_live.py +0 -0
- {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_method.py +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
include README.md
|
|
3
3
|
include LICENSE
|
|
4
4
|
include SUMMARY.md
|
|
5
|
+
|
|
6
|
+
# Include protocol compatibility pin
|
|
7
|
+
include ka9q_radio_compat
|
|
8
|
+
|
|
9
|
+
# Include sync tooling
|
|
10
|
+
recursive-include scripts *.py
|
|
5
11
|
include TUNE_IMPLEMENTATION.md
|
|
6
12
|
include NATIVE_DISCOVERY.md
|
|
7
13
|
include CROSS_PLATFORM_SUPPORT.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ka9q-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
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")
|
|
@@ -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.
|
|
59
|
+
__version__ = '3.5.0'
|
|
60
60
|
__author__ = 'Michael Hauan AC0G'
|
|
61
61
|
|
|
62
62
|
from .control import RadiodControl, allocate_ssrc
|
|
@@ -147,3 +147,4 @@ __all__ = [
|
|
|
147
147
|
|
|
148
148
|
from .addressing import generate_multicast_ip
|
|
149
149
|
from .monitor import ChannelMonitor
|
|
150
|
+
from .compat import KA9Q_RADIO_COMMIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ka9q-radio compatibility pin.
|
|
3
|
+
|
|
4
|
+
Exposes the ka9q-radio commit hash that this version of ka9q-python
|
|
5
|
+
was validated against. Intended for consumption by ka9q-update and
|
|
6
|
+
other deployment tooling.
|
|
7
|
+
|
|
8
|
+
Auto-updated by: scripts/sync_types.py --apply
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from ka9q.compat import KA9Q_RADIO_COMMIT
|
|
12
|
+
print(f"Compatible with ka9q-radio at {KA9Q_RADIO_COMMIT}")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
KA9Q_RADIO_COMMIT: str = "6b0fec7dae82bf5f4d80cad88ec343453d6e6950"
|
|
@@ -2407,38 +2407,43 @@ class RadiodControl:
|
|
|
2407
2407
|
logger.info(f"Setting Opus FEC for SSRC {ssrc}: {loss_percent}% expected loss")
|
|
2408
2408
|
self.send_command(cmdbuffer)
|
|
2409
2409
|
|
|
2410
|
-
def
|
|
2410
|
+
def set_max_delay(self, ssrc: int, max_blocks: int):
|
|
2411
2411
|
"""
|
|
2412
|
-
Set
|
|
2412
|
+
Set maximum allowable aggregation delay in blocks (0-5)
|
|
2413
2413
|
|
|
2414
|
-
Controls how many blocks
|
|
2414
|
+
Controls how many blocks radiod may aggregate before sending a packet.
|
|
2415
2415
|
Higher values reduce packet rate but increase latency.
|
|
2416
2416
|
|
|
2417
2417
|
Args:
|
|
2418
2418
|
ssrc: SSRC of the channel
|
|
2419
|
-
|
|
2419
|
+
max_blocks: Maximum delay in blocks (0-5). At 20ms/block: 0=immediate, 5=100ms
|
|
2420
2420
|
|
|
2421
2421
|
Raises:
|
|
2422
|
-
ValidationError: If
|
|
2422
|
+
ValidationError: If max_blocks is not 0-5
|
|
2423
2423
|
|
|
2424
2424
|
Example:
|
|
2425
|
-
>>> control.
|
|
2425
|
+
>>> control.set_max_delay(ssrc=12345, max_blocks=2) # up to 40ms
|
|
2426
2426
|
"""
|
|
2427
2427
|
_validate_ssrc(ssrc)
|
|
2428
|
-
if not (0 <=
|
|
2429
|
-
raise ValidationError(f"
|
|
2428
|
+
if not (0 <= max_blocks <= 5):
|
|
2429
|
+
raise ValidationError(f"max_blocks must be 0-5, got {max_blocks}")
|
|
2430
2430
|
|
|
2431
2431
|
cmdbuffer = bytearray()
|
|
2432
2432
|
cmdbuffer.append(CMD)
|
|
2433
2433
|
|
|
2434
|
-
encode_int(cmdbuffer, StatusType.
|
|
2434
|
+
encode_int(cmdbuffer, StatusType.MAXDELAY, max_blocks)
|
|
2435
2435
|
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
2436
2436
|
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
2437
2437
|
encode_eol(cmdbuffer)
|
|
2438
2438
|
|
|
2439
|
-
logger.info(f"Setting
|
|
2439
|
+
logger.info(f"Setting max delay for SSRC {ssrc}: {max_blocks} blocks")
|
|
2440
2440
|
self.send_command(cmdbuffer)
|
|
2441
2441
|
|
|
2442
|
+
# Backward compatibility alias
|
|
2443
|
+
def set_packet_buffering(self, ssrc: int, min_blocks: int):
|
|
2444
|
+
"""Deprecated: use set_max_delay() instead."""
|
|
2445
|
+
self.set_max_delay(ssrc, min_blocks)
|
|
2446
|
+
|
|
2442
2447
|
def set_filter2(self, ssrc: int, blocksize: int, kaiser_beta: Optional[float] = None):
|
|
2443
2448
|
"""
|
|
2444
2449
|
Configure secondary filter (linear modes only)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ka9q-radio protocol types and constants
|
|
3
|
+
|
|
4
|
+
Auto-generated by scripts/sync_types.py from ka9q-radio C headers.
|
|
5
|
+
Validated against ka9q-radio commit: 6b0fec7dae82
|
|
6
|
+
|
|
7
|
+
DO NOT EDIT MANUALLY — run: python scripts/sync_types.py --apply
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StatusType:
|
|
12
|
+
"""TLV type identifiers for radiod status/control protocol"""
|
|
13
|
+
|
|
14
|
+
EOL = 0
|
|
15
|
+
COMMAND_TAG = 1 # Echoes tag from requester
|
|
16
|
+
CMD_CNT = 2 # Count of input commands
|
|
17
|
+
GPS_TIME = 3 # Nanoseconds since GPS epoch (remember to update the leap second tables!)
|
|
18
|
+
DESCRIPTION = 4 # Free form text describing source
|
|
19
|
+
STATUS_DEST_SOCKET = 5
|
|
20
|
+
SETOPTS = 6
|
|
21
|
+
CLEAROPTS = 7
|
|
22
|
+
RTP_TIMESNAP = 8 # snapshot of current real-time-protocol timestamp, for linking RTP timestamps to clock time via GPS_TIME
|
|
23
|
+
BIN_BYTE_DATA = 9 # Vector of 1-byte spectrum analyzer data
|
|
24
|
+
INPUT_SAMPRATE = 10 # Nominal sample rate (integer)
|
|
25
|
+
SPECTRUM_BASE = 11 # base level of 1-byte analyzer data, dB
|
|
26
|
+
SPECTRUM_AVG = 12 # Number of FFTs averaged into each spectrum response
|
|
27
|
+
INPUT_SAMPLES = 13
|
|
28
|
+
WINDOW_TYPE = 14 # Window type for FFT analyzer
|
|
29
|
+
NOISE_BW = 15 # Noise bandwidth of FFT spectrum bin, in bins
|
|
30
|
+
OUTPUT_DATA_SOURCE_SOCKET = 16
|
|
31
|
+
OUTPUT_DATA_DEST_SOCKET = 17
|
|
32
|
+
OUTPUT_SSRC = 18
|
|
33
|
+
OUTPUT_TTL = 19
|
|
34
|
+
OUTPUT_SAMPRATE = 20
|
|
35
|
+
OUTPUT_METADATA_PACKETS = 21
|
|
36
|
+
OUTPUT_DATA_PACKETS = 22
|
|
37
|
+
OUTPUT_ERRORS = 23
|
|
38
|
+
CALIBRATE = 24
|
|
39
|
+
LNA_GAIN = 25
|
|
40
|
+
MIXER_GAIN = 26
|
|
41
|
+
IF_GAIN = 27
|
|
42
|
+
DC_I_OFFSET = 28
|
|
43
|
+
DC_Q_OFFSET = 29
|
|
44
|
+
IQ_IMBALANCE = 30
|
|
45
|
+
IQ_PHASE = 31
|
|
46
|
+
DIRECT_CONVERSION = 32 # Boolean indicating SDR is direct conversion -- should avoid DC
|
|
47
|
+
RADIO_FREQUENCY = 33
|
|
48
|
+
FIRST_LO_FREQUENCY = 34
|
|
49
|
+
SECOND_LO_FREQUENCY = 35
|
|
50
|
+
SHIFT_FREQUENCY = 36
|
|
51
|
+
DOPPLER_FREQUENCY = 37
|
|
52
|
+
DOPPLER_FREQUENCY_RATE = 38
|
|
53
|
+
LOW_EDGE = 39
|
|
54
|
+
HIGH_EDGE = 40
|
|
55
|
+
KAISER_BETA = 41
|
|
56
|
+
FILTER_BLOCKSIZE = 42
|
|
57
|
+
FILTER_FIR_LENGTH = 43
|
|
58
|
+
FILTER2 = 44
|
|
59
|
+
IF_POWER = 45
|
|
60
|
+
BASEBAND_POWER = 46
|
|
61
|
+
NOISE_DENSITY = 47
|
|
62
|
+
DEMOD_TYPE = 48 # 0 = linear (default), 1 = FM, 2 = WFM/Stereo, 3 = spectrum
|
|
63
|
+
OUTPUT_CHANNELS = 49 # 1 or 2 in Linear and WFM, 1 in FM
|
|
64
|
+
INDEPENDENT_SIDEBAND = 50 # Linear only
|
|
65
|
+
PLL_ENABLE = 51
|
|
66
|
+
PLL_LOCK = 52 # Linear PLL
|
|
67
|
+
PLL_SQUARE = 53 # Linear PLL
|
|
68
|
+
PLL_PHASE = 54 # Linear PLL
|
|
69
|
+
PLL_BW = 55 # PLL loop bandwidth
|
|
70
|
+
ENVELOPE = 56 # Envelope detection in linear mode
|
|
71
|
+
SNR_SQUELCH = 57 # Enable SNR squelch, all modes
|
|
72
|
+
PLL_SNR = 58 # FM, PLL linear
|
|
73
|
+
FREQ_OFFSET = 59 # FM, PLL linear
|
|
74
|
+
PEAK_DEVIATION = 60 # FM only
|
|
75
|
+
PL_TONE = 61 # PL tone squelch frequency (FM only)
|
|
76
|
+
AGC_ENABLE = 62 # boolean, linear modes only
|
|
77
|
+
HEADROOM = 63 # Audio level headroom, stored as amplitude ratio, exchanged as dB
|
|
78
|
+
AGC_HANGTIME = 64 # AGC hang time, stored as samples, exchanged as sec
|
|
79
|
+
AGC_RECOVERY_RATE = 65 # stored as amplitude ratio/sample, exchanged as dB/sec
|
|
80
|
+
FM_SNR = 66 # selected FM SNR (variance or signal snr)
|
|
81
|
+
AGC_THRESHOLD = 67 # stored as amplitude ratio, exchanged as dB
|
|
82
|
+
GAIN = 68 # AM, Linear only, stored as amplitude ratio, exchanged as dB
|
|
83
|
+
OUTPUT_LEVEL = 69 # All modes
|
|
84
|
+
OUTPUT_SAMPLES = 70
|
|
85
|
+
OPUS_BIT_RATE = 71
|
|
86
|
+
MAXDELAY = 72 # Maximum allowable aggregation delay, blocks (0-5)
|
|
87
|
+
FILTER2_BLOCKSIZE = 73
|
|
88
|
+
FILTER2_FIR_LENGTH = 74
|
|
89
|
+
FILTER2_KAISER_BETA = 75
|
|
90
|
+
SPECTRUM_FFT_N = 76
|
|
91
|
+
FILTER_DROPS = 77
|
|
92
|
+
LOCK = 78 # Tuner is locked, will ignore retune commands (boolean)
|
|
93
|
+
TP1 = 79 # General purpose test points (floating point)
|
|
94
|
+
TP2 = 80
|
|
95
|
+
UNUSED4 = 81
|
|
96
|
+
AD_BITS_PER_SAMPLE = 82 # Front end A/D width, used for gain scaling
|
|
97
|
+
SQUELCH_OPEN = 83 # Squelch opening threshold SNR
|
|
98
|
+
SQUELCH_CLOSE = 84 # and closing
|
|
99
|
+
PRESET = 85 # char string containing mode presets
|
|
100
|
+
DEEMPH_TC = 86 # De-emphasis time constant (FM only)
|
|
101
|
+
DEEMPH_GAIN = 87 # De-emphasis gain (FM only)
|
|
102
|
+
UNUSED3 = 88
|
|
103
|
+
PL_DEVIATION = 89 # Measured PL tone deviation, Hz (FM only)
|
|
104
|
+
THRESH_EXTEND = 90 # threshold extension enable (FM only)
|
|
105
|
+
SPECTRUM_SHAPE = 91 # parameter for spectrum analysis window (eg, Kaiser beta)
|
|
106
|
+
UNUSED2 = 92
|
|
107
|
+
RESOLUTION_BW = 93 # Bandwidth (Hz) of noncoherent integration bin, some multiple of COHERENT_BIN_SPACING
|
|
108
|
+
BIN_COUNT = 94 # Integer number of bins accumulating energy noncoherently
|
|
109
|
+
CROSSOVER = 95 # Frequency in Hz where spectrum algorithm changes
|
|
110
|
+
BIN_DATA = 96 # Vector of relative bin energies, real (I^2 + Q^2)
|
|
111
|
+
RF_ATTEN = 97 # Front end attenuation (introduced with rx888)
|
|
112
|
+
RF_GAIN = 98 # Front end gain (introduced with rx888)
|
|
113
|
+
RF_AGC = 99 # Front end AGC on/off
|
|
114
|
+
FE_LOW_EDGE = 100 # edges of front end filter
|
|
115
|
+
FE_HIGH_EDGE = 101
|
|
116
|
+
FE_ISREAL = 102 # Boolean, true -> front end uses real sampling, false -> front end uses complex
|
|
117
|
+
UNUSED = 103
|
|
118
|
+
AD_OVER = 104 # A/D full scale samples, proxy for overranges
|
|
119
|
+
RTP_PT = 105 # Real Time Protocol Payload Type
|
|
120
|
+
STATUS_INTERVAL = 106 # Automatically send channel status over *data* channel every STATUS_INTERVAL frames
|
|
121
|
+
OUTPUT_ENCODING = 107 # Output data encoding (see enum encoding in multicast.h)
|
|
122
|
+
SAMPLES_SINCE_OVER = 108 # Samples since last A/D overrange
|
|
123
|
+
PLL_WRAPS = 109 # Count of complete linear mode PLL rotations
|
|
124
|
+
RF_LEVEL_CAL = 110 # Adjustment relating dBm to dBFS
|
|
125
|
+
OPUS_DTX = 111 # Opus encoder discontinuous transmission enable/disable
|
|
126
|
+
OPUS_APPLICATION = 112 # Opus encoder application voice/audio/etc
|
|
127
|
+
OPUS_BANDWIDTH = 113 # Opus encoder audio bandwidth limita
|
|
128
|
+
OPUS_FEC = 114 # Opus encoder forward error correction loss rate, %
|
|
129
|
+
SPECTRUM_STEP = 115 # size of byte spectrum data level step, dB
|
|
130
|
+
SPECTRUM_OVERLAP = 116 # Overlap of FFT windows when averaging (0-1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Command packet type
|
|
134
|
+
CMD = 1
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Encoding types — auto-generated from ka9q-radio/src/rtp.h
|
|
138
|
+
class Encoding:
|
|
139
|
+
"""Output encoding types — values must match ka9q-radio/src/rtp.h enum encoding"""
|
|
140
|
+
|
|
141
|
+
NO_ENCODING = 0
|
|
142
|
+
S16LE = 1
|
|
143
|
+
S16BE = 2
|
|
144
|
+
OPUS = 3
|
|
145
|
+
F32LE = 4
|
|
146
|
+
AX25 = 5
|
|
147
|
+
F16LE = 6
|
|
148
|
+
OPUS_VOIP = 7 # Opus with APPLICATION_VOIP
|
|
149
|
+
F32BE = 8
|
|
150
|
+
F16BE = 9
|
|
151
|
+
MULAW = 10
|
|
152
|
+
ALAW = 11
|
|
153
|
+
UNUSED_ENCODING = 12 # Sentinel, not used
|
|
154
|
+
|
|
155
|
+
# Backward compatibility aliases
|
|
156
|
+
F32 = F32LE
|
|
157
|
+
F16 = F16LE
|
|
158
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ka9q-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
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)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.md
|
|
4
|
+
ka9q_radio_compat
|
|
4
5
|
pyproject.toml
|
|
5
6
|
setup.py
|
|
6
7
|
examples/advanced_features_demo.py
|
|
@@ -22,6 +23,7 @@ examples/diagnostics/diagnose_packets.py
|
|
|
22
23
|
examples/diagnostics/repro_utc_bug.py
|
|
23
24
|
ka9q/__init__.py
|
|
24
25
|
ka9q/addressing.py
|
|
26
|
+
ka9q/compat.py
|
|
25
27
|
ka9q/control.py
|
|
26
28
|
ka9q/discovery.py
|
|
27
29
|
ka9q/exceptions.py
|
|
@@ -38,6 +40,7 @@ ka9q_python.egg-info/SOURCES.txt
|
|
|
38
40
|
ka9q_python.egg-info/dependency_links.txt
|
|
39
41
|
ka9q_python.egg-info/requires.txt
|
|
40
42
|
ka9q_python.egg-info/top_level.txt
|
|
43
|
+
scripts/sync_types.py
|
|
41
44
|
tests/__init__.py
|
|
42
45
|
tests/conftest.py
|
|
43
46
|
tests/test_addressing.py
|
|
@@ -55,6 +58,7 @@ tests/test_monitor.py
|
|
|
55
58
|
tests/test_multihomed.py
|
|
56
59
|
tests/test_native_discovery.py
|
|
57
60
|
tests/test_performance_fixes.py
|
|
61
|
+
tests/test_protocol_compat.py
|
|
58
62
|
tests/test_remove_channel.py
|
|
59
63
|
tests/test_rtp_recorder.py
|
|
60
64
|
tests/test_security_features.py
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Synchronize ka9q/types.py with ka9q-radio C header files.
|
|
4
|
+
|
|
5
|
+
Parses enum status_type from status.h and enum encoding from rtp.h,
|
|
6
|
+
then either checks for drift or regenerates types.py.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python scripts/sync_types.py --check # exit non-zero if drift detected
|
|
10
|
+
python scripts/sync_types.py --apply # regenerate types.py and update pin
|
|
11
|
+
python scripts/sync_types.py --diff # show what would change (dry run)
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--ka9q-radio PATH Path to ka9q-radio source tree
|
|
15
|
+
(default: ../ka9q-radio)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import textwrap
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Paths (relative to this script's location)
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
30
|
+
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
31
|
+
TYPES_PY = PROJECT_ROOT / "ka9q" / "types.py"
|
|
32
|
+
COMPAT_PY = PROJECT_ROOT / "ka9q" / "compat.py"
|
|
33
|
+
COMPAT_FILE = PROJECT_ROOT / "ka9q_radio_compat"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# C enum parser
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
def parse_c_enum(header_text: str, enum_name: str) -> List[Tuple[str, int, str]]:
|
|
40
|
+
"""
|
|
41
|
+
Parse a C enum from header text.
|
|
42
|
+
|
|
43
|
+
Returns list of (name, value, comment) tuples in declaration order.
|
|
44
|
+
"""
|
|
45
|
+
# Match the enum block — handles both "enum foo {" and "enum foo\n{"
|
|
46
|
+
pattern = rf"enum\s+{enum_name}\s*\{{(.*?)\}}"
|
|
47
|
+
match = re.search(pattern, header_text, re.DOTALL)
|
|
48
|
+
if not match:
|
|
49
|
+
raise ValueError(f"Could not find 'enum {enum_name}' in header text")
|
|
50
|
+
|
|
51
|
+
body = match.group(1)
|
|
52
|
+
entries: List[Tuple[str, int, str]] = []
|
|
53
|
+
value = 0
|
|
54
|
+
|
|
55
|
+
for line in body.split("\n"):
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if not line or line.startswith("//") or line.startswith("/*"):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Match: NAME, NAME = 3, NAME = 3, // comment NAME, // comment
|
|
61
|
+
m = re.match(
|
|
62
|
+
r"([A-Z][A-Z0-9_]*)\s*(?:=\s*(\d+))?\s*,?\s*(?://\s*(.*))?\s*$",
|
|
63
|
+
line,
|
|
64
|
+
)
|
|
65
|
+
if not m:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
name = m.group(1)
|
|
69
|
+
if m.group(2) is not None:
|
|
70
|
+
value = int(m.group(2))
|
|
71
|
+
comment = (m.group(3) or "").strip()
|
|
72
|
+
|
|
73
|
+
entries.append((name, value, comment))
|
|
74
|
+
value += 1
|
|
75
|
+
|
|
76
|
+
return entries
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_git_commit(repo_path: Path) -> str:
|
|
80
|
+
"""Return the full SHA-1 of HEAD in the given repo."""
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
["git", "-C", str(repo_path), "rev-parse", "HEAD"],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
check=True,
|
|
86
|
+
)
|
|
87
|
+
return result.stdout.strip()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# types.py parser — reads the CURRENT file to learn what Python already has
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
def parse_types_py() -> Tuple[Dict[str, int], Dict[str, int]]:
|
|
94
|
+
"""
|
|
95
|
+
Parse the existing types.py and return:
|
|
96
|
+
(status_entries, encoding_entries)
|
|
97
|
+
Each is {name: value}. Aliases (F32 = F32LE) are excluded.
|
|
98
|
+
"""
|
|
99
|
+
# We import the module directly so we get the truth including aliases
|
|
100
|
+
import importlib.util
|
|
101
|
+
|
|
102
|
+
spec = importlib.util.spec_from_file_location("_types", str(TYPES_PY))
|
|
103
|
+
mod = importlib.util.module_from_spec(spec)
|
|
104
|
+
spec.loader.exec_module(mod)
|
|
105
|
+
|
|
106
|
+
def _class_entries(cls) -> Dict[str, int]:
|
|
107
|
+
seen_values: Set[int] = set()
|
|
108
|
+
entries: Dict[str, int] = {}
|
|
109
|
+
# First pass: collect non-alias attributes (declared first wins)
|
|
110
|
+
for attr in sorted(dir(cls)):
|
|
111
|
+
if attr.startswith("_"):
|
|
112
|
+
continue
|
|
113
|
+
val = getattr(cls, attr)
|
|
114
|
+
if not isinstance(val, int):
|
|
115
|
+
continue
|
|
116
|
+
entries[attr] = val
|
|
117
|
+
return entries
|
|
118
|
+
|
|
119
|
+
return _class_entries(mod.StatusType), _class_entries(mod.Encoding)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Code generator
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
# Comments extracted from the C headers, keyed by enum value ranges.
|
|
127
|
+
# The generator preserves the section-comment structure of the original
|
|
128
|
+
# types.py while replacing the constant definitions.
|
|
129
|
+
|
|
130
|
+
STATUS_SECTIONS = [
|
|
131
|
+
(None, None, None), # section header emitted inline
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def generate_types_py(
|
|
136
|
+
status_entries: List[Tuple[str, int, str]],
|
|
137
|
+
encoding_entries: List[Tuple[str, int, str]],
|
|
138
|
+
commit_hash: str,
|
|
139
|
+
) -> str:
|
|
140
|
+
"""Generate the full contents of ka9q/types.py from parsed enums."""
|
|
141
|
+
|
|
142
|
+
lines: List[str] = []
|
|
143
|
+
|
|
144
|
+
lines.append('"""')
|
|
145
|
+
lines.append("ka9q-radio protocol types and constants")
|
|
146
|
+
lines.append("")
|
|
147
|
+
lines.append("Auto-generated by scripts/sync_types.py from ka9q-radio C headers.")
|
|
148
|
+
lines.append(f"Validated against ka9q-radio commit: {commit_hash[:12]}")
|
|
149
|
+
lines.append("")
|
|
150
|
+
lines.append("DO NOT EDIT MANUALLY — run: python scripts/sync_types.py --apply")
|
|
151
|
+
lines.append('"""')
|
|
152
|
+
lines.append("")
|
|
153
|
+
lines.append("")
|
|
154
|
+
lines.append("class StatusType:")
|
|
155
|
+
lines.append(' """TLV type identifiers for radiod status/control protocol"""')
|
|
156
|
+
lines.append("")
|
|
157
|
+
|
|
158
|
+
for name, value, comment in status_entries:
|
|
159
|
+
suffix = f" # {comment}" if comment else ""
|
|
160
|
+
lines.append(f" {name} = {value}{suffix}")
|
|
161
|
+
|
|
162
|
+
lines.append("")
|
|
163
|
+
lines.append("")
|
|
164
|
+
lines.append("# Command packet type")
|
|
165
|
+
lines.append("CMD = 1")
|
|
166
|
+
lines.append("")
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append("# Encoding types — auto-generated from ka9q-radio/src/rtp.h")
|
|
169
|
+
lines.append("class Encoding:")
|
|
170
|
+
lines.append(
|
|
171
|
+
' """Output encoding types — values must match '
|
|
172
|
+
'ka9q-radio/src/rtp.h enum encoding"""'
|
|
173
|
+
)
|
|
174
|
+
lines.append("")
|
|
175
|
+
|
|
176
|
+
for name, value, comment in encoding_entries:
|
|
177
|
+
suffix = f" # {comment}" if comment else ""
|
|
178
|
+
lines.append(f" {name} = {value}{suffix}")
|
|
179
|
+
|
|
180
|
+
# Backward-compat aliases
|
|
181
|
+
lines.append("")
|
|
182
|
+
lines.append(" # Backward compatibility aliases")
|
|
183
|
+
# Only emit aliases if the canonical names exist
|
|
184
|
+
enc_names = {n for n, _, _ in encoding_entries}
|
|
185
|
+
if "F32LE" in enc_names:
|
|
186
|
+
lines.append(" F32 = F32LE")
|
|
187
|
+
if "F16LE" in enc_names:
|
|
188
|
+
lines.append(" F16 = F16LE")
|
|
189
|
+
|
|
190
|
+
lines.append("") # final newline
|
|
191
|
+
return "\n".join(lines) + "\n"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Diff / check / apply
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
def compute_drift(
|
|
198
|
+
c_status: List[Tuple[str, int, str]],
|
|
199
|
+
c_encoding: List[Tuple[str, int, str]],
|
|
200
|
+
) -> List[str]:
|
|
201
|
+
"""
|
|
202
|
+
Compare C headers against current types.py.
|
|
203
|
+
Returns a list of human-readable drift descriptions (empty = in sync).
|
|
204
|
+
"""
|
|
205
|
+
py_status, py_encoding = parse_types_py()
|
|
206
|
+
issues: List[str] = []
|
|
207
|
+
|
|
208
|
+
# --- StatusType ---
|
|
209
|
+
c_status_map = {name: val for name, val, _ in c_status}
|
|
210
|
+
|
|
211
|
+
# Missing in Python
|
|
212
|
+
for name, val, comment in c_status:
|
|
213
|
+
if name not in py_status:
|
|
214
|
+
issues.append(f"StatusType: MISSING {name} = {val} // {comment}")
|
|
215
|
+
elif py_status[name] != val:
|
|
216
|
+
issues.append(
|
|
217
|
+
f"StatusType: VALUE MISMATCH {name}: "
|
|
218
|
+
f"C={val}, Python={py_status[name]}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Extra in Python (removed from C or renamed)
|
|
222
|
+
for name, val in sorted(py_status.items(), key=lambda x: x[1]):
|
|
223
|
+
if name not in c_status_map:
|
|
224
|
+
issues.append(f"StatusType: EXTRA in Python {name} = {val}")
|
|
225
|
+
|
|
226
|
+
# --- Encoding ---
|
|
227
|
+
c_enc_map = {name: val for name, val, _ in c_encoding}
|
|
228
|
+
# Exclude known aliases from comparison
|
|
229
|
+
alias_names = {"F32", "F16"}
|
|
230
|
+
|
|
231
|
+
for name, val, comment in c_encoding:
|
|
232
|
+
if name not in py_encoding:
|
|
233
|
+
issues.append(f"Encoding: MISSING {name} = {val} // {comment}")
|
|
234
|
+
elif py_encoding[name] != val:
|
|
235
|
+
issues.append(
|
|
236
|
+
f"Encoding: VALUE MISMATCH {name}: "
|
|
237
|
+
f"C={val}, Python={py_encoding[name]}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
for name, val in sorted(py_encoding.items(), key=lambda x: x[1]):
|
|
241
|
+
if name in alias_names:
|
|
242
|
+
continue
|
|
243
|
+
if name not in c_enc_map:
|
|
244
|
+
issues.append(f"Encoding: EXTRA in Python {name} = {val}")
|
|
245
|
+
|
|
246
|
+
return issues
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main() -> int:
|
|
250
|
+
parser = argparse.ArgumentParser(
|
|
251
|
+
description="Synchronize ka9q/types.py with ka9q-radio C headers"
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument(
|
|
254
|
+
"--ka9q-radio",
|
|
255
|
+
type=Path,
|
|
256
|
+
default=None,
|
|
257
|
+
help="Path to ka9q-radio source tree (default: ../ka9q-radio relative to project root)",
|
|
258
|
+
)
|
|
259
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
260
|
+
group.add_argument(
|
|
261
|
+
"--check",
|
|
262
|
+
action="store_true",
|
|
263
|
+
help="Exit non-zero if types.py is out of sync",
|
|
264
|
+
)
|
|
265
|
+
group.add_argument(
|
|
266
|
+
"--apply",
|
|
267
|
+
action="store_true",
|
|
268
|
+
help="Regenerate types.py and update ka9q_radio_compat",
|
|
269
|
+
)
|
|
270
|
+
group.add_argument(
|
|
271
|
+
"--diff",
|
|
272
|
+
action="store_true",
|
|
273
|
+
help="Show drift without modifying anything",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
args = parser.parse_args()
|
|
277
|
+
|
|
278
|
+
# Resolve ka9q-radio path
|
|
279
|
+
if args.ka9q_radio:
|
|
280
|
+
radio_path = args.ka9q_radio.resolve()
|
|
281
|
+
else:
|
|
282
|
+
# Try ../ka9q-radio relative to project root
|
|
283
|
+
radio_path = (PROJECT_ROOT / ".." / "ka9q-radio").resolve()
|
|
284
|
+
|
|
285
|
+
status_h = radio_path / "src" / "status.h"
|
|
286
|
+
rtp_h = radio_path / "src" / "rtp.h"
|
|
287
|
+
|
|
288
|
+
if not status_h.exists():
|
|
289
|
+
print(f"ERROR: {status_h} not found", file=sys.stderr)
|
|
290
|
+
print(
|
|
291
|
+
f" Provide --ka9q-radio PATH or ensure ka9q-radio is at {radio_path}",
|
|
292
|
+
file=sys.stderr,
|
|
293
|
+
)
|
|
294
|
+
return 2
|
|
295
|
+
|
|
296
|
+
if not rtp_h.exists():
|
|
297
|
+
print(f"ERROR: {rtp_h} not found", file=sys.stderr)
|
|
298
|
+
return 2
|
|
299
|
+
|
|
300
|
+
# Parse C headers
|
|
301
|
+
status_text = status_h.read_text()
|
|
302
|
+
rtp_text = rtp_h.read_text()
|
|
303
|
+
|
|
304
|
+
c_status = parse_c_enum(status_text, "status_type")
|
|
305
|
+
c_encoding = parse_c_enum(rtp_text, "encoding")
|
|
306
|
+
|
|
307
|
+
commit = get_git_commit(radio_path)
|
|
308
|
+
|
|
309
|
+
if args.check or args.diff:
|
|
310
|
+
issues = compute_drift(c_status, c_encoding)
|
|
311
|
+
if issues:
|
|
312
|
+
print(f"Protocol drift detected vs ka9q-radio {commit[:12]}:")
|
|
313
|
+
print()
|
|
314
|
+
for issue in issues:
|
|
315
|
+
print(f" {issue}")
|
|
316
|
+
print()
|
|
317
|
+
print(f" {len(issues)} issue(s) found")
|
|
318
|
+
print()
|
|
319
|
+
print("Run 'python scripts/sync_types.py --apply' to synchronize.")
|
|
320
|
+
return 1
|
|
321
|
+
else:
|
|
322
|
+
print(f"types.py is in sync with ka9q-radio {commit[:12]}")
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
# --apply
|
|
326
|
+
new_content = generate_types_py(c_status, c_encoding, commit)
|
|
327
|
+
|
|
328
|
+
# Read current for comparison
|
|
329
|
+
old_content = TYPES_PY.read_text() if TYPES_PY.exists() else ""
|
|
330
|
+
|
|
331
|
+
if new_content == old_content:
|
|
332
|
+
print(f"types.py is already in sync with ka9q-radio {commit[:12]}")
|
|
333
|
+
else:
|
|
334
|
+
TYPES_PY.write_text(new_content)
|
|
335
|
+
print(f"Updated {TYPES_PY}")
|
|
336
|
+
|
|
337
|
+
# Update compat pin (plain text file for scripts/humans)
|
|
338
|
+
pin_content = (
|
|
339
|
+
"# ka9q-radio commit that ka9q/types.py was last validated against\n"
|
|
340
|
+
"# Updated by: scripts/sync_types.py --apply\n"
|
|
341
|
+
f"{commit}\n"
|
|
342
|
+
)
|
|
343
|
+
COMPAT_FILE.write_text(pin_content)
|
|
344
|
+
print(f"Updated {COMPAT_FILE} → {commit[:12]}")
|
|
345
|
+
|
|
346
|
+
# Update ka9q/compat.py (importable constant for ka9q-update)
|
|
347
|
+
compat_py_content = (
|
|
348
|
+
'"""\n'
|
|
349
|
+
"ka9q-radio compatibility pin.\n"
|
|
350
|
+
"\n"
|
|
351
|
+
"Exposes the ka9q-radio commit hash that this version of ka9q-python\n"
|
|
352
|
+
"was validated against. Intended for consumption by ka9q-update and\n"
|
|
353
|
+
"other deployment tooling.\n"
|
|
354
|
+
"\n"
|
|
355
|
+
"Auto-updated by: scripts/sync_types.py --apply\n"
|
|
356
|
+
"\n"
|
|
357
|
+
"Usage:\n"
|
|
358
|
+
" from ka9q.compat import KA9Q_RADIO_COMMIT\n"
|
|
359
|
+
' print(f"Compatible with ka9q-radio at {KA9Q_RADIO_COMMIT}")\n'
|
|
360
|
+
'"""\n'
|
|
361
|
+
"\n"
|
|
362
|
+
f'KA9Q_RADIO_COMMIT: str = "{commit}"\n'
|
|
363
|
+
)
|
|
364
|
+
COMPAT_PY.write_text(compat_py_content)
|
|
365
|
+
print(f"Updated {COMPAT_PY} → {commit[:12]}")
|
|
366
|
+
|
|
367
|
+
# Report what changed
|
|
368
|
+
issues = []
|
|
369
|
+
if old_content:
|
|
370
|
+
# Re-parse to show the delta
|
|
371
|
+
py_status_old, py_enc_old = parse_types_py()
|
|
372
|
+
c_status_map = {name: val for name, val, _ in c_status}
|
|
373
|
+
c_enc_map = {name: val for name, val, _ in c_encoding}
|
|
374
|
+
|
|
375
|
+
added_s = [n for n, v, _ in c_status if n not in py_status_old]
|
|
376
|
+
removed_s = [n for n in py_status_old if n not in c_status_map]
|
|
377
|
+
added_e = [n for n, v, _ in c_encoding if n not in py_enc_old]
|
|
378
|
+
removed_e = [
|
|
379
|
+
n for n in py_enc_old if n not in c_enc_map and n not in {"F32", "F16"}
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
if added_s:
|
|
383
|
+
print(f" StatusType added: {', '.join(added_s)}")
|
|
384
|
+
if removed_s:
|
|
385
|
+
print(f" StatusType removed: {', '.join(removed_s)}")
|
|
386
|
+
if added_e:
|
|
387
|
+
print(f" Encoding added: {', '.join(added_e)}")
|
|
388
|
+
if removed_e:
|
|
389
|
+
print(f" Encoding removed: {', '.join(removed_e)}")
|
|
390
|
+
|
|
391
|
+
print()
|
|
392
|
+
print("Next steps:")
|
|
393
|
+
print(" 1. Review the diff: git diff ka9q/types.py")
|
|
394
|
+
print(" 2. Update control.py if any names were renamed/removed")
|
|
395
|
+
print(" 3. Run tests: python -m pytest tests/")
|
|
396
|
+
print(" 4. Commit and bump version")
|
|
397
|
+
|
|
398
|
+
return 0
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
sys.exit(main())
|
|
@@ -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.
|
|
15
|
+
version='3.5.0',
|
|
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,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocol compatibility test — catches drift between ka9q/types.py
|
|
3
|
+
and the ka9q-radio C headers (status.h, rtp.h).
|
|
4
|
+
|
|
5
|
+
Skipped automatically when ka9q-radio source is not available on disk.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
# Resolve paths relative to this file
|
|
16
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
SYNC_SCRIPT = PROJECT_ROOT / "scripts" / "sync_types.py"
|
|
18
|
+
KA9Q_RADIO_DEFAULT = PROJECT_ROOT.parent / "ka9q-radio"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _find_ka9q_radio() -> Optional[Path]:
|
|
22
|
+
"""Return the ka9q-radio source path, or None if unavailable."""
|
|
23
|
+
# Check default sibling location
|
|
24
|
+
if (KA9Q_RADIO_DEFAULT / "src" / "status.h").exists():
|
|
25
|
+
return KA9Q_RADIO_DEFAULT
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ka9q_radio_path = _find_ka9q_radio()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.skipif(
|
|
33
|
+
ka9q_radio_path is None,
|
|
34
|
+
reason="ka9q-radio source tree not found at ../ka9q-radio",
|
|
35
|
+
)
|
|
36
|
+
def test_types_match_status_h():
|
|
37
|
+
"""types.py must match the ka9q-radio C headers exactly."""
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
[sys.executable, str(SYNC_SCRIPT), "--check",
|
|
40
|
+
"--ka9q-radio", str(ka9q_radio_path)],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
cwd=str(PROJECT_ROOT),
|
|
44
|
+
)
|
|
45
|
+
assert result.returncode == 0, (
|
|
46
|
+
f"types.py is out of sync with ka9q-radio status.h / rtp.h:\n"
|
|
47
|
+
f"{result.stdout}\n{result.stderr}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.skipif(
|
|
52
|
+
ka9q_radio_path is None,
|
|
53
|
+
reason="ka9q-radio source tree not found at ../ka9q-radio",
|
|
54
|
+
)
|
|
55
|
+
def test_compat_pin_matches_ka9q_radio_head():
|
|
56
|
+
"""ka9q_radio_compat pin should match the ka9q-radio HEAD we validated against."""
|
|
57
|
+
compat_file = PROJECT_ROOT / "ka9q_radio_compat"
|
|
58
|
+
assert compat_file.exists(), "ka9q_radio_compat pin file is missing"
|
|
59
|
+
|
|
60
|
+
pinned = None
|
|
61
|
+
for line in compat_file.read_text().splitlines():
|
|
62
|
+
line = line.strip()
|
|
63
|
+
if line and not line.startswith("#"):
|
|
64
|
+
pinned = line
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
assert pinned, "ka9q_radio_compat contains no commit hash"
|
|
68
|
+
|
|
69
|
+
# Get ka9q-radio HEAD
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["git", "-C", str(ka9q_radio_path), "rev-parse", "HEAD"],
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
check=True,
|
|
75
|
+
)
|
|
76
|
+
head = result.stdout.strip()
|
|
77
|
+
|
|
78
|
+
assert pinned == head, (
|
|
79
|
+
f"ka9q_radio_compat pin ({pinned[:12]}) does not match "
|
|
80
|
+
f"ka9q-radio HEAD ({head[:12]}). "
|
|
81
|
+
f"Run: python scripts/sync_types.py --apply"
|
|
82
|
+
)
|
ka9q_python-3.4.1/ka9q/types.py
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ka9q-radio protocol types and constants
|
|
3
|
-
|
|
4
|
-
Status types from ka9q-radio/src/status.h
|
|
5
|
-
These MUST match the enum values in status.h exactly!
|
|
6
|
-
Verified against https://github.com/ka9q/ka9q-radio (official repository)
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
class StatusType:
|
|
10
|
-
"""TLV type identifiers for radiod status/control protocol"""
|
|
11
|
-
|
|
12
|
-
EOL = 0
|
|
13
|
-
COMMAND_TAG = 1
|
|
14
|
-
CMD_CNT = 2
|
|
15
|
-
GPS_TIME = 3
|
|
16
|
-
|
|
17
|
-
DESCRIPTION = 4
|
|
18
|
-
STATUS_DEST_SOCKET = 5
|
|
19
|
-
SETOPTS = 6
|
|
20
|
-
CLEAROPTS = 7
|
|
21
|
-
RTP_TIMESNAP = 8
|
|
22
|
-
BIN_BYTE_DATA = 9
|
|
23
|
-
INPUT_SAMPRATE = 10
|
|
24
|
-
SPECTRUM_BASE = 11
|
|
25
|
-
SPECTRUM_AVG = 12
|
|
26
|
-
INPUT_SAMPLES = 13
|
|
27
|
-
WINDOW_TYPE = 14
|
|
28
|
-
NOISE_BW = 15
|
|
29
|
-
|
|
30
|
-
OUTPUT_DATA_SOURCE_SOCKET = 16
|
|
31
|
-
OUTPUT_DATA_DEST_SOCKET = 17
|
|
32
|
-
OUTPUT_SSRC = 18
|
|
33
|
-
OUTPUT_TTL = 19
|
|
34
|
-
OUTPUT_SAMPRATE = 20
|
|
35
|
-
OUTPUT_METADATA_PACKETS = 21
|
|
36
|
-
OUTPUT_DATA_PACKETS = 22
|
|
37
|
-
OUTPUT_ERRORS = 23
|
|
38
|
-
|
|
39
|
-
# Hardware
|
|
40
|
-
CALIBRATE = 24
|
|
41
|
-
LNA_GAIN = 25
|
|
42
|
-
MIXER_GAIN = 26
|
|
43
|
-
IF_GAIN = 27
|
|
44
|
-
|
|
45
|
-
DC_I_OFFSET = 28
|
|
46
|
-
DC_Q_OFFSET = 29
|
|
47
|
-
IQ_IMBALANCE = 30
|
|
48
|
-
IQ_PHASE = 31
|
|
49
|
-
DIRECT_CONVERSION = 32
|
|
50
|
-
|
|
51
|
-
# Tuning
|
|
52
|
-
RADIO_FREQUENCY = 33
|
|
53
|
-
FIRST_LO_FREQUENCY = 34
|
|
54
|
-
SECOND_LO_FREQUENCY = 35
|
|
55
|
-
SHIFT_FREQUENCY = 36
|
|
56
|
-
DOPPLER_FREQUENCY = 37
|
|
57
|
-
DOPPLER_FREQUENCY_RATE = 38
|
|
58
|
-
|
|
59
|
-
# Filtering
|
|
60
|
-
LOW_EDGE = 39
|
|
61
|
-
HIGH_EDGE = 40
|
|
62
|
-
KAISER_BETA = 41
|
|
63
|
-
FILTER_BLOCKSIZE = 42
|
|
64
|
-
FILTER_FIR_LENGTH = 43
|
|
65
|
-
FILTER2 = 44
|
|
66
|
-
|
|
67
|
-
# Signals
|
|
68
|
-
IF_POWER = 45
|
|
69
|
-
BASEBAND_POWER = 46
|
|
70
|
-
NOISE_DENSITY = 47
|
|
71
|
-
|
|
72
|
-
# Demodulation configuration
|
|
73
|
-
DEMOD_TYPE = 48 # 0 = linear (default), 1 = FM, 2 = WFM/Stereo, 3 = spectrum
|
|
74
|
-
OUTPUT_CHANNELS = 49 # 1 or 2 in Linear, otherwise 1
|
|
75
|
-
INDEPENDENT_SIDEBAND = 50 # Linear only
|
|
76
|
-
PLL_ENABLE = 51
|
|
77
|
-
PLL_LOCK = 52
|
|
78
|
-
PLL_SQUARE = 53
|
|
79
|
-
PLL_PHASE = 54
|
|
80
|
-
PLL_BW = 55
|
|
81
|
-
ENVELOPE = 56
|
|
82
|
-
SNR_SQUELCH = 57
|
|
83
|
-
|
|
84
|
-
# Demodulation status
|
|
85
|
-
PLL_SNR = 58 # FM, PLL linear
|
|
86
|
-
FREQ_OFFSET = 59
|
|
87
|
-
PEAK_DEVIATION = 60
|
|
88
|
-
PL_TONE = 61
|
|
89
|
-
|
|
90
|
-
# Settable gain parameters
|
|
91
|
-
AGC_ENABLE = 62 # Boolean, linear modes only
|
|
92
|
-
HEADROOM = 63
|
|
93
|
-
AGC_HANGTIME = 64
|
|
94
|
-
AGC_RECOVERY_RATE = 65
|
|
95
|
-
FM_SNR = 66
|
|
96
|
-
AGC_THRESHOLD = 67
|
|
97
|
-
|
|
98
|
-
GAIN = 68 # AM, Linear only
|
|
99
|
-
OUTPUT_LEVEL = 69
|
|
100
|
-
OUTPUT_SAMPLES = 70
|
|
101
|
-
|
|
102
|
-
OPUS_BIT_RATE = 71
|
|
103
|
-
MINPACKET = 72
|
|
104
|
-
FILTER2_BLOCKSIZE = 73
|
|
105
|
-
FILTER2_FIR_LENGTH = 74
|
|
106
|
-
FILTER2_KAISER_BETA = 75
|
|
107
|
-
SPECTRUM_FFT_N = 76
|
|
108
|
-
|
|
109
|
-
FILTER_DROPS = 77
|
|
110
|
-
LOCK = 78
|
|
111
|
-
|
|
112
|
-
TP1 = 79 # Test points
|
|
113
|
-
TP2 = 80
|
|
114
|
-
|
|
115
|
-
GAINSTEP = 81
|
|
116
|
-
AD_BITS_PER_SAMPLE = 82
|
|
117
|
-
SQUELCH_OPEN = 83
|
|
118
|
-
SQUELCH_CLOSE = 84
|
|
119
|
-
PRESET = 85 # Mode/preset name (e.g., "iq", "usb", "lsb")
|
|
120
|
-
DEEMPH_TC = 86
|
|
121
|
-
DEEMPH_GAIN = 87
|
|
122
|
-
CONVERTER_OFFSET = 88
|
|
123
|
-
PL_DEVIATION = 89
|
|
124
|
-
THRESH_EXTEND = 90
|
|
125
|
-
|
|
126
|
-
# Spectral analysis
|
|
127
|
-
SPECTRUM_SHAPE = 91
|
|
128
|
-
COHERENT_BIN_SPACING = 92
|
|
129
|
-
RESOLUTION_BW = 93
|
|
130
|
-
BIN_COUNT = 94
|
|
131
|
-
CROSSOVER = 95
|
|
132
|
-
BIN_DATA = 96
|
|
133
|
-
|
|
134
|
-
RF_ATTEN = 97
|
|
135
|
-
RF_GAIN = 98
|
|
136
|
-
RF_AGC = 99
|
|
137
|
-
FE_LOW_EDGE = 100
|
|
138
|
-
FE_HIGH_EDGE = 101
|
|
139
|
-
FE_ISREAL = 102
|
|
140
|
-
BLOCKS_SINCE_POLL = 103
|
|
141
|
-
AD_OVER = 104
|
|
142
|
-
RTP_PT = 105
|
|
143
|
-
STATUS_INTERVAL = 106
|
|
144
|
-
OUTPUT_ENCODING = 107
|
|
145
|
-
SAMPLES_SINCE_OVER = 108
|
|
146
|
-
PLL_WRAPS = 109
|
|
147
|
-
RF_LEVEL_CAL = 110
|
|
148
|
-
OPUS_DTX = 111
|
|
149
|
-
OPUS_APPLICATION = 112
|
|
150
|
-
OPUS_BANDWIDTH = 113
|
|
151
|
-
OPUS_FEC = 114
|
|
152
|
-
SPECTRUM_STEP = 115
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# Command packet type
|
|
156
|
-
CMD = 1
|
|
157
|
-
|
|
158
|
-
# Encoding types - must match enum encoding in ka9q-radio/src/rtp.h
|
|
159
|
-
class Encoding:
|
|
160
|
-
"""Output encoding types - values must match ka9q-radio/src/rtp.h enum encoding"""
|
|
161
|
-
NO_ENCODING = 0
|
|
162
|
-
S16LE = 1 # Signed 16-bit little-endian
|
|
163
|
-
S16BE = 2 # Signed 16-bit big-endian
|
|
164
|
-
OPUS = 3 # Opus codec
|
|
165
|
-
F32LE = 4 # 32-bit float little-endian
|
|
166
|
-
AX25 = 5 # AX.25 packet
|
|
167
|
-
F16LE = 6 # 16-bit float little-endian
|
|
168
|
-
OPUS_VOIP = 7 # Opus with APPLICATION_VOIP
|
|
169
|
-
F32BE = 8 # 32-bit float big-endian
|
|
170
|
-
F16BE = 9 # 16-bit float big-endian
|
|
171
|
-
UNUSED_ENCODING = 10 # Sentinel, not used
|
|
172
|
-
|
|
173
|
-
# Backward compatibility aliases
|
|
174
|
-
F32 = F32LE
|
|
175
|
-
F16 = F16LE
|
|
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
|