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.
Files changed (75) hide show
  1. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/MANIFEST.in +6 -0
  2. {ka9q_python-3.4.1/ka9q_python.egg-info → ka9q_python-3.5.0}/PKG-INFO +2 -1
  3. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/README.md +1 -0
  4. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/stream_example.py +4 -6
  5. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/__init__.py +2 -1
  6. ka9q_python-3.5.0/ka9q/compat.py +15 -0
  7. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/control.py +15 -10
  8. ka9q_python-3.5.0/ka9q/types.py +158 -0
  9. {ka9q_python-3.4.1 → ka9q_python-3.5.0/ka9q_python.egg-info}/PKG-INFO +2 -1
  10. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/SOURCES.txt +4 -0
  11. ka9q_python-3.5.0/ka9q_radio_compat +3 -0
  12. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/pyproject.toml +1 -1
  13. ka9q_python-3.5.0/scripts/sync_types.py +402 -0
  14. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/setup.py +1 -1
  15. ka9q_python-3.5.0/tests/test_protocol_compat.py +82 -0
  16. ka9q_python-3.4.1/ka9q/types.py +0 -175
  17. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/LICENSE +0 -0
  18. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/advanced_features_demo.py +0 -0
  19. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/channel_cleanup_example.py +0 -0
  20. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/codar_oceanography.py +0 -0
  21. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/diagnostics/diagnose_packets.py +0 -0
  22. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/diagnostics/repro_utc_bug.py +0 -0
  23. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/discover_example.py +0 -0
  24. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/grape_integration_example.py +0 -0
  25. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/hf_band_scanner.py +0 -0
  26. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/rtp_recorder_example.py +0 -0
  27. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/simple_am_radio.py +0 -0
  28. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/superdarn_recorder.py +0 -0
  29. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/test_channel_operations.py +0 -0
  30. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/test_improvements.py +0 -0
  31. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/test_timing_fields.py +0 -0
  32. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/tune.py +0 -0
  33. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/examples/tune_example.py +0 -0
  34. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/addressing.py +0 -0
  35. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/discovery.py +0 -0
  36. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/exceptions.py +0 -0
  37. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/managed_stream.py +0 -0
  38. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/monitor.py +0 -0
  39. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/resequencer.py +0 -0
  40. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/rtp_recorder.py +0 -0
  41. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/stream.py +0 -0
  42. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/stream_quality.py +0 -0
  43. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q/utils.py +0 -0
  44. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/dependency_links.txt +0 -0
  45. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/requires.txt +0 -0
  46. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/ka9q_python.egg-info/top_level.txt +0 -0
  47. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/setup.cfg +0 -0
  48. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/__init__.py +0 -0
  49. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/conftest.py +0 -0
  50. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_addressing.py +0 -0
  51. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_channel_verification.py +0 -0
  52. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_create_split_encoding.py +0 -0
  53. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_decode_functions.py +0 -0
  54. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_encode_functions.py +0 -0
  55. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_encode_socket.py +0 -0
  56. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ensure_channel_encoding.py +0 -0
  57. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_integration.py +0 -0
  58. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_iq_20khz_f32.py +0 -0
  59. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_listen_multicast.py +0 -0
  60. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_managed_stream_recovery.py +0 -0
  61. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_monitor.py +0 -0
  62. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_multihomed.py +0 -0
  63. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_native_discovery.py +0 -0
  64. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_performance_fixes.py +0 -0
  65. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_remove_channel.py +0 -0
  66. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_rtp_recorder.py +0 -0
  67. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_security_features.py +0 -0
  68. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ssrc_dest_unit.py +0 -0
  69. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ssrc_encoding_unit.py +0 -0
  70. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_ttl_warning.py +0 -0
  71. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune.py +0 -0
  72. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_cli.py +0 -0
  73. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_debug.py +0 -0
  74. {ka9q_python-3.4.1 → ka9q_python-3.5.0}/tests/test_tune_live.py +0 -0
  75. {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.4.1
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
- channel = None
127
- for ch in channels:
128
- if ch.ssrc == args.ssrc:
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: {[ch.ssrc for ch in channels]}")
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.4.1'
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 set_packet_buffering(self, ssrc: int, min_blocks: int):
2410
+ def set_max_delay(self, ssrc: int, max_blocks: int):
2411
2411
  """
2412
- Set minimum packet buffering (0-4 blocks)
2412
+ Set maximum allowable aggregation delay in blocks (0-5)
2413
2413
 
2414
- Controls how many blocks to buffer before sending a packet.
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
- min_blocks: Minimum blocks (0-4). At 20ms/block: 0=no minimum, 4=80ms minimum
2419
+ max_blocks: Maximum delay in blocks (0-5). At 20ms/block: 0=immediate, 5=100ms
2420
2420
 
2421
2421
  Raises:
2422
- ValidationError: If min_blocks is not 0-4
2422
+ ValidationError: If max_blocks is not 0-5
2423
2423
 
2424
2424
  Example:
2425
- >>> control.set_packet_buffering(ssrc=12345, min_blocks=2) # 40ms minimum
2425
+ >>> control.set_max_delay(ssrc=12345, max_blocks=2) # up to 40ms
2426
2426
  """
2427
2427
  _validate_ssrc(ssrc)
2428
- if not (0 <= min_blocks <= 4):
2429
- raise ValidationError(f"min_blocks must be 0-4, got {min_blocks}")
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.MINPACKET, min_blocks)
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 packet buffering for SSRC {ssrc}: {min_blocks} blocks")
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.4.1
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,3 @@
1
+ # ka9q-radio commit that ka9q/types.py was last validated against
2
+ # Updated by: scripts/sync_types.py --apply
3
+ 6b0fec7dae82bf5f4d80cad88ec343453d6e6950
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ka9q-python"
7
- version = "3.4.1"
7
+ version = "3.5.0"
8
8
  description = "Python interface for ka9q-radio control and monitoring"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -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.4.1',
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
+ )
@@ -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