ka9q-python 3.2.0__py3-none-any.whl
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/__init__.py +98 -0
- ka9q/control.py +2295 -0
- ka9q/discovery.py +485 -0
- ka9q/exceptions.py +23 -0
- ka9q/resequencer.py +389 -0
- ka9q/rtp_recorder.py +457 -0
- ka9q/stream.py +393 -0
- ka9q/stream_quality.py +215 -0
- ka9q/types.py +161 -0
- ka9q/utils.py +202 -0
- ka9q_python-3.2.0.dist-info/METADATA +237 -0
- ka9q_python-3.2.0.dist-info/RECORD +15 -0
- ka9q_python-3.2.0.dist-info/WHEEL +5 -0
- ka9q_python-3.2.0.dist-info/licenses/LICENSE +21 -0
- ka9q_python-3.2.0.dist-info/top_level.txt +1 -0
ka9q/types.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
UNUSED4 = 9
|
|
23
|
+
INPUT_SAMPRATE = 10
|
|
24
|
+
UNUSED6 = 11
|
|
25
|
+
UNUSED7 = 12
|
|
26
|
+
INPUT_SAMPLES = 13
|
|
27
|
+
UNUSED8 = 14
|
|
28
|
+
UNUSED9 = 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_KAISER_BETA = 91
|
|
128
|
+
COHERENT_BIN_SPACING = 92
|
|
129
|
+
NONCOHERENT_BIN_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
|
+
|
|
149
|
+
|
|
150
|
+
# Command packet type
|
|
151
|
+
CMD = 1
|
|
152
|
+
|
|
153
|
+
# Encoding types (from ka9q-radio)
|
|
154
|
+
class Encoding:
|
|
155
|
+
"""Output encoding types"""
|
|
156
|
+
NO_ENCODING = 0
|
|
157
|
+
S16BE = 1 # Signed 16-bit big-endian
|
|
158
|
+
S16LE = 2 # Signed 16-bit little-endian
|
|
159
|
+
F32 = 3 # 32-bit float
|
|
160
|
+
F16 = 4 # 16-bit float
|
|
161
|
+
OPUS = 5 # Opus codec
|
ka9q/utils.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utility functions for ka9q-python
|
|
3
|
+
|
|
4
|
+
This module contains common utilities used across the package, primarily
|
|
5
|
+
for mDNS address resolution and network operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import re
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_multicast_address(address: str, timeout: float = 5.0) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Resolve hostname or mDNS address to IP for multicast operations
|
|
20
|
+
|
|
21
|
+
This function tries multiple resolution methods for cross-platform compatibility:
|
|
22
|
+
1. Check if already an IP address (no resolution needed)
|
|
23
|
+
2. Try avahi-resolve (Linux)
|
|
24
|
+
3. Try dns-sd (macOS)
|
|
25
|
+
4. Fallback to getaddrinfo (works everywhere)
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
address: Hostname, .local mDNS name, or IP address
|
|
29
|
+
timeout: Resolution timeout in seconds (default: 5.0)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Resolved IP address as string (e.g., "239.251.200.193")
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
Exception: If resolution fails after trying all methods
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> resolve_multicast_address("radiod.local")
|
|
39
|
+
'239.251.200.193'
|
|
40
|
+
|
|
41
|
+
>>> resolve_multicast_address("192.168.1.100")
|
|
42
|
+
'192.168.1.100'
|
|
43
|
+
"""
|
|
44
|
+
# Check if already an IP address
|
|
45
|
+
if re.match(r'^\d+\.\d+\.\d+\.\d+$', address):
|
|
46
|
+
logger.debug(f"Address {address} is already an IP")
|
|
47
|
+
return address
|
|
48
|
+
|
|
49
|
+
# Try avahi-resolve (Linux)
|
|
50
|
+
try:
|
|
51
|
+
result = subprocess.run(
|
|
52
|
+
['avahi-resolve', '-n', address],
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=timeout
|
|
56
|
+
)
|
|
57
|
+
if result.returncode == 0:
|
|
58
|
+
# Parse output: "hostname ip_address"
|
|
59
|
+
parts = result.stdout.strip().split()
|
|
60
|
+
if len(parts) >= 2:
|
|
61
|
+
resolved = parts[1]
|
|
62
|
+
logger.debug(f"Resolved via avahi-resolve: {address} -> {resolved}")
|
|
63
|
+
return resolved
|
|
64
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
65
|
+
logger.debug(f"avahi-resolve not available: {e}")
|
|
66
|
+
|
|
67
|
+
# Try dns-sd (macOS)
|
|
68
|
+
try:
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
['dns-sd', '-G', 'v4', address],
|
|
71
|
+
capture_output=True,
|
|
72
|
+
text=True,
|
|
73
|
+
timeout=timeout
|
|
74
|
+
)
|
|
75
|
+
if result.returncode == 0:
|
|
76
|
+
# Parse dns-sd output for IP address
|
|
77
|
+
for line in result.stdout.split('\n'):
|
|
78
|
+
# Look for lines containing the address and an IP
|
|
79
|
+
match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line)
|
|
80
|
+
if match and address in line:
|
|
81
|
+
resolved = match.group(1)
|
|
82
|
+
logger.debug(f"Resolved via dns-sd: {address} -> {resolved}")
|
|
83
|
+
return resolved
|
|
84
|
+
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
85
|
+
logger.debug(f"dns-sd not available: {e}")
|
|
86
|
+
|
|
87
|
+
# Fallback to getaddrinfo (works everywhere)
|
|
88
|
+
# Note: getaddrinfo doesn't support timeout, so we set socket default timeout
|
|
89
|
+
try:
|
|
90
|
+
old_timeout = socket.getdefaulttimeout()
|
|
91
|
+
socket.setdefaulttimeout(timeout)
|
|
92
|
+
try:
|
|
93
|
+
addr_info = socket.getaddrinfo(address, None, socket.AF_INET, socket.SOCK_DGRAM)
|
|
94
|
+
resolved = addr_info[0][4][0]
|
|
95
|
+
logger.debug(f"Resolved via getaddrinfo: {address} -> {resolved}")
|
|
96
|
+
return resolved
|
|
97
|
+
finally:
|
|
98
|
+
socket.setdefaulttimeout(old_timeout)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise Exception(f"Failed to resolve {address}: {e}") from e
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_multicast_socket(multicast_addr: str, port: int = 5006,
|
|
104
|
+
bind_addr: str = '0.0.0.0',
|
|
105
|
+
interface: Optional[str] = None) -> socket.socket:
|
|
106
|
+
"""
|
|
107
|
+
Create and configure a UDP socket for multicast operations
|
|
108
|
+
|
|
109
|
+
This is a convenience function that sets up all the necessary socket options
|
|
110
|
+
for sending to or receiving from a multicast group.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
multicast_addr: Multicast group IP address
|
|
114
|
+
port: Port number (default: 5006 for radiod)
|
|
115
|
+
bind_addr: Address to bind to (default: '0.0.0.0' for all interfaces)
|
|
116
|
+
interface: IP address of network interface for multicast membership
|
|
117
|
+
(e.g., '192.168.1.100'). Required on multi-homed systems.
|
|
118
|
+
If None, uses INADDR_ANY (0.0.0.0).
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Configured socket ready for multicast operations
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
OSError: If socket creation or configuration fails
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> sock = create_multicast_socket('239.251.200.193')
|
|
128
|
+
>>> sock.sendto(data, ('239.251.200.193', 5006))
|
|
129
|
+
|
|
130
|
+
>>> # Multi-homed system
|
|
131
|
+
>>> sock = create_multicast_socket('239.251.200.193', interface='192.168.1.100')
|
|
132
|
+
"""
|
|
133
|
+
import struct
|
|
134
|
+
|
|
135
|
+
# Create UDP socket
|
|
136
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
137
|
+
|
|
138
|
+
# Allow multiple sockets to bind to the same port
|
|
139
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
140
|
+
|
|
141
|
+
# Set SO_REUSEPORT if available (allows multiple processes)
|
|
142
|
+
if hasattr(socket, 'SO_REUSEPORT'):
|
|
143
|
+
try:
|
|
144
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
145
|
+
logger.debug("SO_REUSEPORT enabled")
|
|
146
|
+
except OSError as e:
|
|
147
|
+
logger.debug(f"Could not set SO_REUSEPORT: {e}")
|
|
148
|
+
|
|
149
|
+
# Bind to specified port
|
|
150
|
+
try:
|
|
151
|
+
sock.bind((bind_addr, port))
|
|
152
|
+
logger.debug(f"Bound to {bind_addr}:{port}")
|
|
153
|
+
except OSError as e:
|
|
154
|
+
logger.error(f"Failed to bind socket to {bind_addr}:{port}: {e}")
|
|
155
|
+
sock.close()
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
# Join multicast group on specified interface
|
|
159
|
+
interface_addr = interface if interface else '0.0.0.0'
|
|
160
|
+
mreq = struct.pack('=4s4s',
|
|
161
|
+
socket.inet_aton(multicast_addr), # multicast group
|
|
162
|
+
socket.inet_aton(interface_addr)) # interface to use
|
|
163
|
+
try:
|
|
164
|
+
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
165
|
+
logger.debug(f"Joined multicast group {multicast_addr} on interface {interface_addr}")
|
|
166
|
+
except OSError as e:
|
|
167
|
+
# EADDRINUSE is not fatal - group already joined
|
|
168
|
+
if e.errno != 48: # EADDRINUSE on macOS
|
|
169
|
+
logger.warning(f"Failed to join multicast group: {e}")
|
|
170
|
+
|
|
171
|
+
return sock
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def validate_multicast_address(address: str) -> bool:
|
|
175
|
+
"""
|
|
176
|
+
Validate that an address is a valid multicast address
|
|
177
|
+
|
|
178
|
+
Multicast addresses are in the range 224.0.0.0 to 239.255.255.255
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
address: IP address string to validate
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if valid multicast address, False otherwise
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
>>> validate_multicast_address('239.251.200.193')
|
|
188
|
+
True
|
|
189
|
+
|
|
190
|
+
>>> validate_multicast_address('192.168.1.1')
|
|
191
|
+
False
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
parts = address.split('.')
|
|
195
|
+
if len(parts) != 4:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
first_octet = int(parts[0])
|
|
199
|
+
# Multicast range: 224.0.0.0 to 239.255.255.255
|
|
200
|
+
return 224 <= first_octet <= 239
|
|
201
|
+
except (ValueError, AttributeError):
|
|
202
|
+
return False
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ka9q-python
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: Python interface for ka9q-radio control and monitoring
|
|
5
|
+
Home-page: https://github.com/mijahauan/ka9q-python
|
|
6
|
+
Author: Michael Hauan AC0G
|
|
7
|
+
Author-email: Michael Hauan AC0G <ac0g@hauan.org>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/mijahauan/ka9q-python
|
|
10
|
+
Project-URL: Documentation, https://github.com/mijahauan/ka9q-python/blob/main/README.md
|
|
11
|
+
Project-URL: Repository, https://github.com/mijahauan/ka9q-python
|
|
12
|
+
Project-URL: Issues, https://github.com/mijahauan/ka9q-python/issues
|
|
13
|
+
Keywords: ka9q-radio,sdr,ham-radio,radio-control
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Intended Audience :: Telecommunications Industry
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.24.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
Dynamic: requires-python
|
|
35
|
+
|
|
36
|
+
# ka9q-python
|
|
37
|
+
|
|
38
|
+
[](https://badge.fury.io/py/ka9q-python)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
|
|
41
|
+
**General-purpose Python library for controlling [ka9q-radio](https://github.com/ka9q/ka9q-radio)**
|
|
42
|
+
|
|
43
|
+
Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, SuperDARN radar, CODAR oceanography, HF fax, satellite downlinks, and more.
|
|
44
|
+
|
|
45
|
+
**Note:** Package name is `ka9q-python` out of respect for KA9Q (Phil Karn's callsign). Import as `import ka9q`.
|
|
46
|
+
|
|
47
|
+
## Table of Contents
|
|
48
|
+
|
|
49
|
+
- [Features](#features)
|
|
50
|
+
- [Installation](#installation)
|
|
51
|
+
- [Quick Start](#quick-start)
|
|
52
|
+
- [Documentation](#documentation)
|
|
53
|
+
- [Examples](#examples)
|
|
54
|
+
- [Use Cases](#use-cases)
|
|
55
|
+
- [License](#license)
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
✅ **Zero assumptions** - Works for any SDR application
|
|
60
|
+
✅ **Complete API** - All 85+ radiod parameters exposed
|
|
61
|
+
✅ **Channel control** - Create, configure, discover channels
|
|
62
|
+
✅ **RTP recording** - Generic recorder with timing support and state machine
|
|
63
|
+
✅ **Precise timing** - GPS_TIME/RTP_TIMESNAP for accurate timestamps
|
|
64
|
+
✅ **Multi-homed support** - Works on systems with multiple network interfaces
|
|
65
|
+
✅ **Pure Python** - No compiled dependencies
|
|
66
|
+
✅ **Well tested** - Comprehensive test coverage
|
|
67
|
+
✅ **Documented** - Comprehensive examples and API reference included
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install ka9q-python
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or install from source:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/mijahauan/ka9q-python.git
|
|
79
|
+
cd ka9q-python
|
|
80
|
+
pip install -e .
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
### Listen to AM Broadcast
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from ka9q import RadiodControl
|
|
89
|
+
|
|
90
|
+
# Connect to radiod
|
|
91
|
+
control = RadiodControl("radiod.local")
|
|
92
|
+
|
|
93
|
+
# Create AM channel on 10 MHz WWV
|
|
94
|
+
control.create_channel(
|
|
95
|
+
ssrc=10000000,
|
|
96
|
+
frequency_hz=10.0e6,
|
|
97
|
+
preset="am",
|
|
98
|
+
sample_rate=12000
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# RTP stream now available with SSRC 10000000
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Monitor WSPR Bands
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from ka9q import RadiodControl
|
|
108
|
+
|
|
109
|
+
control = RadiodControl("radiod.local")
|
|
110
|
+
|
|
111
|
+
wspr_bands = [
|
|
112
|
+
(1.8366e6, "160m"),
|
|
113
|
+
(3.5686e6, "80m"),
|
|
114
|
+
(7.0386e6, "40m"),
|
|
115
|
+
(10.1387e6, "30m"),
|
|
116
|
+
(14.0956e6, "20m"),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
for freq, band in wspr_bands:
|
|
120
|
+
control.create_channel(
|
|
121
|
+
ssrc=int(freq),
|
|
122
|
+
frequency_hz=freq,
|
|
123
|
+
preset="usb",
|
|
124
|
+
sample_rate=12000
|
|
125
|
+
)
|
|
126
|
+
print(f"{band} WSPR channel created")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Discover Existing Channels
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from ka9q import discover_channels
|
|
133
|
+
|
|
134
|
+
channels = discover_channels("radiod.local")
|
|
135
|
+
for ssrc, info in channels.items():
|
|
136
|
+
print(f"{ssrc}: {info.frequency/1e6:.3f} MHz, {info.preset}, {info.sample_rate} Hz")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Record RTP Stream with Precise Timing
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from ka9q import discover_channels, RTPRecorder
|
|
143
|
+
import time
|
|
144
|
+
|
|
145
|
+
# Get channel with timing info
|
|
146
|
+
channels = discover_channels("radiod.local")
|
|
147
|
+
channel = channels[14074000]
|
|
148
|
+
|
|
149
|
+
# Define packet handler
|
|
150
|
+
def handle_packet(header, payload, wallclock):
|
|
151
|
+
print(f"Packet at {wallclock}: {len(payload)} bytes")
|
|
152
|
+
|
|
153
|
+
# Create and start recorder
|
|
154
|
+
recorder = RTPRecorder(channel=channel, on_packet=handle_packet)
|
|
155
|
+
recorder.start()
|
|
156
|
+
recorder.start_recording()
|
|
157
|
+
time.sleep(60) # Record for 60 seconds
|
|
158
|
+
recorder.stop_recording()
|
|
159
|
+
recorder.stop()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Multi-Homed Systems
|
|
163
|
+
|
|
164
|
+
For systems with multiple network interfaces, specify which interface to use:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from ka9q import RadiodControl, discover_channels
|
|
168
|
+
|
|
169
|
+
# Specify your interface IP address
|
|
170
|
+
my_interface = "192.168.1.100"
|
|
171
|
+
|
|
172
|
+
# Create control with specific interface
|
|
173
|
+
control = RadiodControl("radiod.local", interface=my_interface)
|
|
174
|
+
|
|
175
|
+
# Discovery on specific interface
|
|
176
|
+
channels = discover_channels("radiod.local", interface=my_interface)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Documentation
|
|
180
|
+
|
|
181
|
+
For detailed information, please refer to the documentation in the `docs/` directory:
|
|
182
|
+
|
|
183
|
+
- **[API Reference](docs/API_REFERENCE.md)**: Full details on all classes, methods, and functions.
|
|
184
|
+
- **[RTP Timing Support](docs/RTP_TIMING_SUPPORT.md)**: Guide to RTP timing and synchronization.
|
|
185
|
+
- **[Architecture](docs/ARCHITECTURE.md)**: Overview of the library's design and structure.
|
|
186
|
+
- **[Installation Guide](docs/INSTALLATION.md)**: Detailed installation instructions.
|
|
187
|
+
- **[Testing Guide](docs/TESTING_GUIDE.md)**: Information on how to run the test suite.
|
|
188
|
+
- **[Security Considerations](docs/SECURITY.md)**: Important security information regarding the ka9q-radio protocol.
|
|
189
|
+
- **[Changelog](docs/CHANGELOG.md)**: A log of all changes for each version.
|
|
190
|
+
- **[Release Notes](docs/releases/)**: Release-specific notes and instructions.
|
|
191
|
+
|
|
192
|
+
## Examples
|
|
193
|
+
|
|
194
|
+
See the `examples/` directory for complete applications:
|
|
195
|
+
|
|
196
|
+
- **`discover_example.py`** - Channel discovery methods (native Python and control utility)
|
|
197
|
+
- **`tune.py`** - Interactive channel tuning utility (Python implementation of ka9q-radio's tune)
|
|
198
|
+
- **`tune_example.py`** - Programmatic examples of using the tune() method
|
|
199
|
+
- **`rtp_recorder_example.py`** - Complete RTP recorder with timing and state machine
|
|
200
|
+
- **`test_timing_fields.py`** - Verify GPS_TIME/RTP_TIMESNAP timing fields
|
|
201
|
+
- **`simple_am_radio.py`** - Minimal AM broadcast listener
|
|
202
|
+
- **`superdarn_recorder.py`** - Ionospheric radar monitoring
|
|
203
|
+
- **`codar_oceanography.py`** - Ocean current radar
|
|
204
|
+
- **`hf_band_scanner.py`** - Dynamic frequency scanner
|
|
205
|
+
- **`wspr_monitor.py`** - Weak signal propagation reporter
|
|
206
|
+
|
|
207
|
+
## Use Cases
|
|
208
|
+
|
|
209
|
+
### AM/FM/SSB Radio
|
|
210
|
+
- Broadcast monitoring
|
|
211
|
+
- Ham radio operation
|
|
212
|
+
- Shortwave listening
|
|
213
|
+
|
|
214
|
+
### Scientific Research
|
|
215
|
+
- WSPR propagation studies
|
|
216
|
+
- SuperDARN ionospheric radar
|
|
217
|
+
- CODAR ocean current mapping
|
|
218
|
+
- Meteor scatter
|
|
219
|
+
- EME (moonbounce)
|
|
220
|
+
|
|
221
|
+
### Digital Modes
|
|
222
|
+
- FT8/FT4 monitoring
|
|
223
|
+
- RTTY/PSK decoding
|
|
224
|
+
- DRM digital radio
|
|
225
|
+
- HF fax reception
|
|
226
|
+
|
|
227
|
+
### Satellite Operations
|
|
228
|
+
- Downlink reception
|
|
229
|
+
- Doppler tracking
|
|
230
|
+
- Multi-frequency monitoring
|
|
231
|
+
|
|
232
|
+
### Custom Applications
|
|
233
|
+
**No assumptions!** Use for anything SDR-related.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ka9q/__init__.py,sha256=Hj9-RXpUlYCKCaS8s_mCI2E_rT1Tb9j04fT94PSpqg8,2272
|
|
2
|
+
ka9q/control.py,sha256=gte-8p8W_l76OJbaUu3pi4gqwztS1Kc1NzdMvgkoAlw,88910
|
|
3
|
+
ka9q/discovery.py,sha256=gURq2GlV49vArYCcExarsKtyD1x6Is2LuDfLWveYM5c,18707
|
|
4
|
+
ka9q/exceptions.py,sha256=_P8rok_tTmRMu-lLRhS95kv8FRK5PEY80_6jIux2mWM,474
|
|
5
|
+
ka9q/resequencer.py,sha256=o31Hrt1m12jFyfIOtpHlICK0EQm5wm2Bo3yJNgr8FgQ,14084
|
|
6
|
+
ka9q/rtp_recorder.py,sha256=J9cWygxe7oKG5nQu4tIO6Ba5gaY5iK5PahvfdpcZbo4,16171
|
|
7
|
+
ka9q/stream.py,sha256=fc2yPQanVEA3FO7-jWfhEuN9UHpE9sFyybaIOOfSJHY,13822
|
|
8
|
+
ka9q/stream_quality.py,sha256=yklim5rx2U_y1ElgYrLtftSxYxPItoOwIHJsw17xujA,7589
|
|
9
|
+
ka9q/types.py,sha256=LtqDQIG6N89iAKuqwdwJEtupcJeLqpqRCjpRtdK0pW4,3524
|
|
10
|
+
ka9q/utils.py,sha256=fFR_sYIpeDyPaFlXzN6jCiGhYlssvrMMVx4z6vyhqcc,7107
|
|
11
|
+
ka9q_python-3.2.0.dist-info/licenses/LICENSE,sha256=mjz-l5TVsO9j7Jqt1gfLvTWtZK_HL9sZRRpsHaq4Qv8,1074
|
|
12
|
+
ka9q_python-3.2.0.dist-info/METADATA,sha256=Rx3b6-JleiGIYvLQc7MBTE3vgIyogrdLCggj0OPA-t0,7334
|
|
13
|
+
ka9q_python-3.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
ka9q_python-3.2.0.dist-info/top_level.txt,sha256=FUTLZDtQpJGenGPPlN34E5QpQi_oaiLB6LuzgHblBNE,5
|
|
15
|
+
ka9q_python-3.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michael James Hauan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION OF THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ka9q
|