Skip to main content

ka9q-python

PyPI version License: MIT

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.

Table of Contents

Features

  • Complete radiod API — all 117 TLV status/command parameters exposed, generated from ka9q-radio's C headers
  • Every radiod RTP encoding decoded — S16LE/BE, F32LE/BE, F16LE/BE, MULAW, ALAW via pure-NumPy parse_rtp_samples(); OPUS / OPUS_VOIP via the optional OpusDecoder (install with [opus] extra)
  • Four stream abstractions — RTPRecorder (raw packets), RadiodStream (samples + gap handling), ManagedStream (self-healing single channel), MultiStream (shared socket, many SSRCs)
  • Typed status decoder — ChannelStatus, FrontendStatus, PllStatus, etc. with dotted-path field access
  • Precise RTP timing — GPS_TIME / RTP_TIMESNAP for sample-accurate wallclock timestamps
  • LAN discovery — enumerate radiod instances and their active channels via mDNS
  • CLI + TUI — ka9q list / query / set / tui for interactive and scripted control
  • Multi-homed — explicit interface selection for hosts with multiple NICs
  • Protocol drift detection — pinned to a specific ka9q-radio commit, with a sync script
  • Pure Python — NumPy is the only runtime dependency

Installation

pip install ka9q-python

Optional extras:

Extra Adds Needed for
tui textual ka9q tui interactive terminal UI
opus opuslib decoding OPUS / OPUS_VOIP RTP payloads via OpusDecoder
dev pytest, pytest-cov running the test suite
pip install "ka9q-python[opus]"          # one extra
pip install "ka9q-python[tui,opus]"      # multiple

Or install from source:

git clone https://github.com/HamSCI/ka9q-python.git
cd ka9q-python
pip install -e .

Quick Start

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 set RADIOD_HOST, RADIOD_ADDRESS, or the --radiod-host pytest option when running in other environments.

Listen to AM Broadcast

from ka9q import RadiodControl

# Connect to radiod (default test host: bee1-hf-status.local)
control = RadiodControl("bee1-hf-status.local", client_id="am-demo")

# 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

from ka9q import RadiodControl, Encoding

control = RadiodControl("bee1-hf-status.local", client_id="hq-encoding-demo")

# 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

from ka9q import RadiodControl

control = RadiodControl("bee1-hf-status.local", client_id="wspr-demo")

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")

Discover Existing Channels

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")

Record RTP Stream with Precise Timing

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()

Multi-Homed Systems

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)

Automatic Channel Recovery

ensure your channels survive radiod restarts:

from ka9q import RadiodControl, ChannelMonitor

control = RadiodControl("bee1-hf-status.local", client_id="monitor-demo")
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:

with RadiodControl("bee1-hf-status.local", client_id="cleanup-demo") 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.

ka9q-radio Compatibility

ka9q-python tracks a specific git commit of ka9q-radio to ensure its protocol definitions (StatusType, Encoding) match the C headers exactly. This prevents subtle bugs from protocol drift between the two projects.

How It Works

File Role
ka9q_radio_compat Plain-text pin recording the validated ka9q-radio commit hash
ka9q/compat.py Importable KA9Q_RADIO_COMMIT constant for deployment tooling
ka9q/types.py Auto-generated from ka9q-radio's status.h and rtp.h
scripts/sync_types.py The tool that parses C headers and regenerates types.py
tests/test_protocol_compat.py Drift test (runs automatically if ../ka9q-radio exists)

Checking for Drift

If you have the ka9q-radio source tree at ../ka9q-radio:

python scripts/sync_types.py --check    # CI mode: exits non-zero on drift
python scripts/sync_types.py --diff     # Preview changes without modifying anything

Syncing After ka9q-radio Updates

python scripts/sync_types.py --apply    # Regenerates types.py, updates pins
git diff ka9q/types.py                  # Review the changes
python -m pytest tests/                 # Verify nothing broke

The --apply mode updates three files atomically:

  1. ka9q/types.py — regenerated from the C headers
  2. ka9q_radio_compat — updated with the new commit hash
  3. ka9q/compat.py — updated with the new commit hash (importable)

For Deployment Tooling

ka9q-update (or any deployment tool) can read the pinned commit to ensure the correct radiod version is running:

from ka9q.compat import KA9Q_RADIO_COMMIT

print(f"This ka9q-python requires ka9q-radio at {KA9Q_RADIO_COMMIT[:12]}")

Running the Drift Test

The pytest drift test runs automatically as part of the test suite:

python -m pytest tests/test_protocol_compat.py -v

It auto-skips if ../ka9q-radio is not present, so CI environments without the C source tree are unaffected.

Documentation

Examples

See examples/ for runnable scripts:

Use Cases

See docs/RECIPES.md for worked examples of:

  • LAN probing — enumerate radiod instances and their active channels
  • Fixed-channel pipelines — WSPR, PSK/FT8, HF timing (bundled band plans, MultiStream); see companion projects wspr-recorder, psk-recorder, hf-timestd
  • Nimble channel switching — single-channel SWL-style retuning driven from the CLI or an app
  • SDR portability — ka9q-python talks to radiod, which talks to the SDR; reporting frontend capabilities via FrontendStatus. Primary tested frontend is the RX888; AirspyR2 and Airspy HF+ support is in development.

Troubleshooting

Created channels never appear (silent write-path failure)

Symptom: discover_channels() works — you can see the radiod's existing channels — but every create_channel() / ensure_channel() you issue "succeeds" (no exception from the send), and then the channel never shows up. The only visible error is a later TimeoutError: Channel SSRC ... not verified within ...s or a poll_channel() that returns None — neither of which names the cause.

Cause: some hosts carry a kernel route that sends all locally-originated multicast to loopback (e.g. a 239.0.0.0/8 dev lo entry). Inbound multicast still arrives on the real NIC — so discovery (the read path) looks healthy — while every outbound control command is routed to lo and never reaches radiod. There is no socket error: the packet is delivered, just to the wrong interface. This was tcpdump-confirmed during the 2026-08-12 audit: tcpdump -i <nic> saw zero command packets while tcpdump -i lo captured all of them (sourced from 127.0.0.1).

Check:

# Substitute the group your radiod status DNS name resolves to:
ip route get 239.205.73.40
# BAD:  ... dev lo   src ...   <- commands never leave this host
# GOOD: ... dev eth0 src ...

Remedy: pass your NIC's IP as interface= so ka9q-python sets IP_MULTICAST_IF on the send socket, overriding the kernel route:

control = RadiodControl("bee1-status.local", interface="192.168.1.176")
channels = discover_channels("bee1-status.local", interface="192.168.1.176")

Rule of thumb: a working discover_channels() proves only the read path. If reads work but writes vanish, check ip route get first — send-side ACKs don't exist in this protocol, so routing misconfiguration is otherwise invisible.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Metadata

Release files for ka9q-python 3.26.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ka9q-python 3.26.0
File Size Uploaded
ka9q_python-3.26.0.tar.gz 500.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ka9q-python 3.26.0
File Interpreter ABI Platform
ka9q_python-3.26.0-py3-none-any.whl Python 3 none any Details

Total release size: 646.7 kB

Release files / ka9q_python-3.26.0.tar.gz

Download URL ka9q_python-3.26.0.tar.gz
Size 500.0 kB
Tags Source
SHA-256 checksum
How to use checksums
3efff488e5228a6372077a7ac552ea44fef2bef33080be72828bfa55e8aa9333
BLAKE2b-256 checksum
How to use checksums
8f2e94cbf6262d3c7772682a134c94344181e4c2ad5173d169ce76842c2a534f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / ka9q_python-3.26.0-py3-none-any.whl

Download URL ka9q_python-3.26.0-py3-none-any.whl
Size 146.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0a1d30bcc46d90cd067a30e4012d6e7be93de93bc36de80f1df11265aa69eefb
BLAKE2b-256 checksum
How to use checksums
30c6c724c480f1406d4260f39109a7a88ecc2ec65452ae380e2ca4597812272a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

3.26.0 This release

2 release files

3.22.0

2 release files

3.20.0

2 release files

3.19.0

2 release files

3.18.0

2 release files

3.17.0

2 release files

3.16.1

2 release files

3.15.1

2 release files

3.15.0

2 release files

3.14.2

2 release files

3.14.1

2 release files

3.14.0

2 release files

3.10.0

2 release files

3.9.0

2 release files

3.8.0

2 release files

3.7.1

2 release files

3.7.0

2 release files

3.5.0

2 release files

3.4.2

2 release files

3.4.1

2 release files

3.4.0

2 release files

3.3.0

2 release files

3.2.7

2 release files

3.2.6

2 release files

3.2.5

2 release files

3.2.4

2 release files

3.2.3

2 release files

3.2.2

2 release files

3.2.1

2 release files

3.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page