
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
ka9q-python
Advanced tools
General-purpose Python library for controlling ka9q-radio
Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, SuperDARN radar, CODAR oceanography, HF fax, satellite downlinks, and more.
Note: Package name is ka9q-python out of respect for KA9Q (Phil Karn's callsign). Import as import ka9q.
✅ Zero assumptions - Works for any SDR application
✅ Complete API - All 85+ radiod parameters exposed
✅ Channel control - Create, configure, discover channels
✅ RTP recording - Generic recorder with timing support and state machine
✅ Precise timing - GPS_TIME/RTP_TIMESNAP for accurate timestamps
✅ Multi-homed support - Works on systems with multiple network interfaces
✅ Pure Python - No compiled dependencies
✅ Well tested - Comprehensive test coverage
✅ Documented - Comprehensive examples and API reference included
pip install ka9q-python
Or install from source:
git clone https://github.com/mijahauan/ka9q-python.git
cd ka9q-python
pip install -e .
Host selection: All examples reference
bee1-hf-status.local, which is the default integration test radiod in this repo. Replace it with your own radiod host or setRADIOD_HOST,RADIOD_ADDRESS, or the--radiod-hostpytest option when running in other environments.
from ka9q import RadiodControl
# Connect to radiod (default test host: bee1-hf-status.local)
control = RadiodControl("bee1-hf-status.local")
# Create AM channel on 10 MHz WWV
control.create_channel(
ssrc=10000000,
frequency_hz=10.0e6,
preset="am",
sample_rate=12000
)
# RTP stream now available with SSRC 10000000
### Request Specific Output Encoding
```python
from ka9q import RadiodControl, Encoding
control = RadiodControl("bee1-hf-status.local")
# Create a channel with 32-bit float output (highest quality)
control.ensure_channel(
frequency_hz=14.074e6,
preset="usb",
sample_rate=12000,
encoding=Encoding.F32
)
### Monitor WSPR Bands
```python
from ka9q import RadiodControl
control = RadiodControl("bee1-hf-status.local")
wspr_bands = [
(1.8366e6, "160m"),
(3.5686e6, "80m"),
(7.0386e6, "40m"),
(10.1387e6, "30m"),
(14.0956e6, "20m"),
]
for freq, band in wspr_bands:
control.create_channel(
ssrc=int(freq),
frequency_hz=freq,
preset="usb",
sample_rate=12000
)
print(f"{band} WSPR channel created")
from ka9q import discover_channels
channels = discover_channels("bee1-hf-status.local")
for ssrc, info in channels.items():
print(f"{ssrc}: {info.frequency/1e6:.3f} MHz, {info.preset}, {info.sample_rate} Hz")
from ka9q import discover_channels, RTPRecorder
import time
# Get channel with timing info
channels = discover_channels("bee1-hf-status.local")
channel = channels[14074000]
# Define packet handler
def handle_packet(header, payload, wallclock):
print(f"Packet at {wallclock}: {len(payload)} bytes")
# Create and start recorder
recorder = RTPRecorder(channel=channel, on_packet=handle_packet)
recorder.start()
recorder.start_recording()
time.sleep(60) # Record for 60 seconds
recorder.stop_recording()
recorder.stop()
For systems with multiple network interfaces, specify which interface to use:
from ka9q import RadiodControl, discover_channels
# Specify your interface IP address
my_interface = "192.168.1.100"
# Create control with specific interface
control = RadiodControl("bee1-hf-status.local", interface=my_interface)
# Discovery on specific interface
channels = discover_channels("bee1-hf-status.local", interface=my_interface)
ensure your channels survive radiod restarts:
from ka9q import RadiodControl, ChannelMonitor
control = RadiodControl("bee1-hf-status.local")
monitor = ChannelMonitor(control)
monitor.start()
# This channel will be automatically re-created if it disappears
monitor.monitor_channel(
frequency_hz=14.074e6,
preset="usb",
sample_rate=12000
)
### Channel Cleanup (frequency = 0)
`radiod` removes channels by polling for streams whose frequency is set to `0 Hz`. Always call `remove_channel(ssrc)` (or explicitly set `set_frequency(ssrc, 0.0)` if you build TLVs yourself) when tearing down a stream so the background poller can reclaim it:
```python
with RadiodControl("bee1-hf-status.local") as control:
info = control.ensure_channel(
frequency_hz=10e6,
preset="iq",
sample_rate=16000
)
# ... use channel ...
control.remove_channel(info.ssrc) # marks frequency=0
Note:
remove_channel()finishes instantly on the client; radiod’s poller typically purges the channel within the next second.
For detailed information, please refer to the documentation in the docs/ directory:
See the examples/ directory for complete applications:
ensure_channel() handles the complexity of checking existing channels, creating new ones only when necessary, and verifying configurations.radiod streams efficiently.discover_example.py - Channel discovery methods (native Python and control utility)tune.py - Interactive channel tuning utility (Python implementation of ka9q-radio's tune)tune_example.py - Programmatic examples of using the tune() methodrtp_recorder_example.py - Complete RTP recorder with timing and state machinetest_timing_fields.py - Verify GPS_TIME/RTP_TIMESNAP timing fieldssimple_am_radio.py - Minimal AM broadcast listenersuperdarn_recorder.py - Ionospheric radar monitoringcodar_oceanography.py - Ocean current radarhf_band_scanner.py - Dynamic frequency scannerwspr_monitor.py - Weak signal propagation reporterNo assumptions! Use for anything SDR-related.
This project is licensed under the MIT License - see the LICENSE file for details.
FAQs
Python interface for ka9q-radio control and monitoring
We found that ka9q-python demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.