Skip to content

Releases: HamSCI/ka9q-python

v3.15.1 — derive __version__ from package metadata

Choose a tag to compare

@mijahauan mijahauan released this 21 May 11:43

Fixed

  • ka9q.__version__ was stale on v3.15.0. The wheel's dist-info correctly announced 3.15.0, but the __version__ string baked into ka9q/__init__.py still read '3.14.2' — a hardcoded literal I forgot to bump alongside pyproject.toml. In-code introspection (import ka9q; ka9q.__version__) returned the wrong string for every consumer.

    The fix replaces the hardcoded literal with importlib.metadata.version("ka9q-python") so the in-package version always tracks dist-info. Drift between the released version and the code-reported version is no longer possible. Falls back to "0.0.0+unknown" when the package isn't installed (e.g. running from a source tree without an editable install).

Not changed

Every functional addition from v3.15.0 — F16LE/F16BE/MULAW/ALAW decoders in parse_rtp_samples, the new OpusDecoder class behind the [opus] extra, the set_agc / OPUS_BITRATE bug fixes, the de-duplicated RadiodControl setters, the pin to ka9q-radio d555f1853422 — remains in place. This is purely a metadata-reporting fix.


🤖 Generated with Claude Code

v3.15.0 — full RTP encoding coverage + control.py bug fixes

Choose a tag to compare

@mijahauan mijahauan released this 21 May 01:25

Added

  • F16LE / F16BE RTP payload decoding in parse_rtp_samples for radiod's float16 output mode (encodings 6 and 9). Both audio and IQ paths.
  • G.711 µ-law / A-law decoders for encodings 10 and 11 — pure-numpy table-based, no audioop dependency (which was removed in Python 3.13).
  • OpusDecoder class in ka9q.stream for OPUS / OPUS_VOIP payloads (encodings 3 and 7). Lazy-imports opuslib; install via pip install ka9q-python[opus]. Maintains codec state across calls so packet-loss concealment works end-to-end — one instance per stream SSRC.
  • New opus optional dependency in pyproject.toml (opuslib>=3.0).

Fixed

  • RadiodControl.set_agc(attack_rate=...) was unreachable. It encoded StatusType.AGC_ATTACK_RATE, which does not exist in ka9q-radio's status.h or in ka9q/types.py — any call passing attack_rate= raised AttributeError. Replaced with a working threshold: kwarg backed by AGC_THRESHOLD (which radiod actually decodes in decode_radio_commands()).
  • Removed 6 stale duplicate set_* methods. In a single class, second defs silently shadow firsts — so callers were already using the correct versions further down the file. The dead first defs of set_squelch, set_pll, set_output_channels, set_independent_sideband, set_envelope_detection, and set_opus_bitrate are gone. One of them (set_opus_bitrate) referenced a non-existent StatusType.OPUS_BITRATE — the working second def uses the correct OPUS_BIT_RATE.

Tooling / pinning

  • Pin advanced from ka9q-radio f78cff9c (1.0.0-22) to d555f1853422. Drift report confirms zero TLV-tag, encoding-enum, demod-type, or window-type changes across the 68 intervening upstream commits — only packaging, hydrasdr-driver, and internal C-struct refactors. Existing ka9q-python state (types.py, decode_status_packet, SpectrumStream, every set_* method) was already covering the full HEAD protocol surface; this release brings the recorded compatibility tag in line.

Tests

  • Fixed pre-existing tests/test_filter_edges.py::_bare_control helper that did not initialise the client_id attribute added with v3.14.0 per-(client,radiod) multicast destinations.

🤖 Generated with Claude Code

v3.14.1 — RadiodStream multi-interface multicast join

Choose a tag to compare

@mijahauan mijahauan released this 14 May 01:02

Fixed

Extends Rob Robinett's e3acb6a fix from multi_stream.py to stream.py so RadiodStream also joins the multicast group on every local IPv4 interface instead of letting the kernel pick one via INADDR_ANY.

Same root cause, same symptom, different consumer. A co-located radiod with ttl=0, or any multi-homed station where radiod emits on a non-default-route interface, would silently deliver zero packets to a RadiodStream consumer. Most visible to clients that use the per-stream API directly:

  • codar-sounder (wraps RadiodStream in its RadiodIQSource)
  • hf-timestd's legacy T6 path
  • Any future client consuming a single RTP stream

MultiStream-based clients (psk-recorder, wspr-recorder, hfdl-recorder, hf-timestd shared-mode) were already fixed in 3.14.x by Rob's commit.

Refactor

The helpers (iter_local_ipv4_interfaces, join_multicast_all_interfaces) move into a package-private ka9q/_multicast.py. Both multi_stream.py and stream.py import from there — one implementation, identical behaviour. A future change to the join semantics applies uniformly.

Not Fixed

rtp_recorder.RtpRecorder still has the single-interface INADDR_ANY join. Left as a separate change in case anyone depends on the exact wire behaviour of the legacy recorder.

Tests

8 new in tests/test_multicast_helpers.py:

  • enumerator real-host smoke + if_nameindex OSError handling + nonexistent-iface filtering
  • join helper: per-iface setsockopt call sequence + correct mreq bytes
  • per-iface failure doesn't abort the loop
  • empty enumeration → empty join list (no crash)
  • both stream classes pull the helper from the shared module

142 offline unit tests pass.

Install

pip install ka9q-python>=3.14.1

v3.14.0 — per-(client, radiod) multicast destinations

Choose a tag to compare

@mijahauan mijahauan released this 13 May 11:03

Closes the CONTRACT v0.3 §7 gap: RadiodControl now derives a deterministic per-(client, radiod) multicast destination when constructed with client_id=. Peer clients on the same host land on distinct multicast groups automatically — no per-client derivation code required.

New

  • RadiodControl(status_address, client_id="<name>") — when set, ensure_channel(destination=None) auto-derives generate_multicast_ip(client_id, radiod_host=self.status_address).

Destination precedence:

  1. Explicit destination= to ensure_channel (operator override, unchanged).
  2. Derived from (client_id, status_address) when client_id was set on the RadiodControl.
  3. None — radiod uses its config-file default (pre-3.14 behavior, preserved for rollback).

The (client_id, radiod_host) pairing gives the right behavior for every multi-instance shape:

  • Same client on two radiods → two distinct destinations (radiod's status_address feeds the hash).
  • Two clients on one radiod → two distinct destinations (different client_id feeds the hash).
  • Same client + same radiod → repeatable across restarts (deterministic hash).

MultiStream._attempt_restore inherits the behavior because it reuses the same RadiodControl. A radiod restart that drops a channel restores it on the same per-client multicast group it was on before.

Because allocate_ssrc already hashes destination into the SSRC, per-client destinations produce per-client SSRCs — radiod's channel table cleanly separates concurrent clients that ask for the same (freq, preset, sample_rate, encoding) tuple instead of silently collapsing them into one shared channel.

Why now

CONTRACT v0.3 §7 (sigmond's client contract, 2026-04-12) said "ka9q-python derives the multicast destination deterministically." It didn't — for ~13 months ensure_channel(destination=None) was effectively a no-op that left the field unspecified, so radiod fell back to its config-file default group and every client on a given radiod shared one multicast address. v3.14.0 makes the spec true.

Backwards compatibility

  • RadiodControl(status) without client_id preserves pre-3.14 behavior: destination=None flows through, radiod uses its config-file default. Clients opt in by passing client_id="<name>".
  • Bumping the floor for §7 conformance: clients should pin ka9q-python>=3.14.0 in their pyproject.toml when they adopt client_id=.

Companion client adoptions

All five radiod-consuming sigmond clients were updated in the same release wave (2026-05-13) and deployed live on bee1:

Client Repo + commit Live multicast group
psk-recorder mijahauan/psk-recorder@47cf92c 239.95.99.52
wspr-recorder mijahauan/wspr-recorder@0620bf6 239.169.255.247
hfdl-recorder mijahauan/hfdl-recorder@af84271 239.150.155.78
codar-sounder mijahauan/codar-sounder@b0ddcb0 239.178.79.119
hf-timestd mijahauan/hf-timestd@5a8767c 239.78.93.2

Contract prose updated at mijahauan/sigmond@39b97db.

Tests

9 new unit tests in tests/test_client_id_destination.py cover the precedence rules, the uniqueness invariants, and SSRC divergence. 134 offline unit tests green.

Install

pip install ka9q-python>=3.14.0

v3.12.0 — Spectrum bin decoding and SpectrumStream

Choose a tag to compare

@mijahauan mijahauan released this 07 May 03:07

What's New

Spectrum bin vector decoding

decode_status_packet() now decodes BIN_DATA (float32, SPECT_DEMOD) and BIN_BYTE_DATA (uint8, SPECT2_DEMOD) TLV vectors from radiod status packets. New fields on SpectrumStatus:

  • bin_data — float32 power values per FFT bin
  • bin_byte_data — uint8 quantised log-power values
  • bin_power_db property — dB values regardless of source format

SpectrumStream

New class for receiving real-time FFT spectrum data from radiod. Spectrum data flows over the status multicast channel (port 5006) as TLV vectors, not over RTP. SpectrumStream handles:

  • Channel creation (SPECT2_DEMOD by default)
  • Periodic polling to keep the channel alive
  • SSRC filtering and bin vector decoding
  • Callback delivery with ready-to-use numpy arrays
from ka9q import RadiodControl, SpectrumStream

def on_spectrum(status):
    db = status.spectrum.bin_power_db
    print(f"{len(db)} bins, peak {db.max():.1f} dB")

with RadiodControl("radiod.local") as ctl:
    with SpectrumStream(
        control=ctl,
        frequency_hz=14.1e6,
        bin_count=2048,
        resolution_bw=50.0,
        on_spectrum=on_spectrum,
    ) as stream:
        import time; time.sleep(30)

Documentation

  • API Reference, Architecture, and Recipes docs updated
  • New examples/spectrum_example.py
  • 13 new unit tests

Install

pip install ka9q-python==3.12.0

v3.10.0 — channel-lifetime keep-alive

Choose a tag to compare

@mijahauan mijahauan released this 30 Apr 03:33

Highlights

Adds Python support for ka9q-radio's new channel-lifetime self-destruct timer (radiod 0f8b622+). Channels can now declare a lifetime in radiod main-loop frames; if the client dies, the channel expires on its own instead of lingering on radiod.

New API on RadiodControl

  • set_channel_lifetime(ssrc, lifetime) — explicit poll, suitable as a periodic keep-alive
  • create_channel(..., lifetime=None) — sends LIFETIME on creation when set
  • ensure_channel(..., lifetime=None) — passes through to create_channel; on the reuse path, calls set_channel_lifetime so the value is enforced regardless of prior state
  • tune(..., lifetime=None)LIFETIME is included in the multi-parameter command buffer
  • _decode_status_response() now surfaces incoming LIFETIME as status['lifetime'] and incoming DESCRIPTION as status['description']

Default behavior is unchanged: omit lifetime and the wire packet has no LIFETIME tag, so pre-0f8b622 radiod stays compatible.

Units are radiod main-loop frames (~50 Hz at the default 20 ms blocktime). 0 = infinite.

Other

  • Backfilled CHANGELOG entry for v3.9.0 (CLI + Textual TUI + typed ChannelStatus)
  • Refreshed stale tests/test_encode_socket.py to match the current 6-byte wire format
  • Hardened tests/test_native_discovery.py to mock RadiodControl._connect (no DNS dependency)

Full unit suite: 232 passed, 12 skipped.

Install

pip install --upgrade ka9q-python
# or, with TUI extras:
pip install --upgrade 'ka9q-python[tui]'

PyPI: https://pypi.org/project/ka9q-python/3.10.0/

v3.7.0 — Radiod-aware multicast addressing

Choose a tag to compare

@mijahauan mijahauan released this 12 Apr 11:21

Summary

  • Radiod-aware multicast addressing: generate_multicast_ip() and allocate_ssrc() now accept a radiod_host parameter so the same client talking to different radiod instances gets distinct multicast destinations and SSRCs.
  • Automatic propagation: RadiodControl.create_channel() and ensure_channel() automatically include the radiod identity — no caller changes needed.

Backward Compatibility

  • generate_multicast_ip(unique_id) without radiod_host produces identical results to v3.6.0.
  • allocate_ssrc() SSRC values will differ from v3.6.0 (hash format changed to include radiod host). Channels will be re-created with new SSRCs on upgrade. This is intentional — the old SSRCs could collide when a client talked to multiple radiod instances.

Install

pip install ka9q-python==3.7.0

🤖 Generated with Claude Code

v3.5.0: Protocol Drift Detection and Compatibility Pinning

Choose a tag to compare

@mijahauan mijahauan released this 01 Apr 01:56

What's New

Protocol Drift Detection Tooling

  • scripts/sync_types.py: Code-generates ka9q/types.py from ka9q-radio C headers (status.h, rtp.h)
    • --check: exits non-zero if drift detected (for CI/tests)
    • --apply: regenerates types.py and updates compatibility pin
    • --diff: dry-run showing what would change
  • ka9q_radio_compat: Plain-text pin recording the ka9q-radio commit hash that types.py was validated against
  • ka9q/compat.py: Importable KA9Q_RADIO_COMMIT constant for ka9q-update deployment tooling
  • tests/test_protocol_compat.py: Pytest drift test — auto-skips when ka9q-radio source is absent, fails on mismatch when present

Protocol Resync (ka9q-radio 6b0fec7)

  • StatusType: MINPACKET→MAXDELAY, GAINSTEP→UNUSED4, CONVERTER_OFFSET→UNUSED3, COHERENT_BIN_SPACING→UNUSED2, BLOCKS_SINCE_POLL→UNUSED; added SPECTRUM_OVERLAP (116)
  • Encoding: added MULAW (10), ALAW (11); UNUSED_ENCODING shifted to 12
  • control.py: set_max_delay() replaces set_packet_buffering() (old name kept as backward-compat alias)

Cleanup

  • Removed ad-hoc compare_encodings.py and compare_status_types.py (replaced by unified sync script)

Backward Compatibility

All changes are backward compatible. Existing code continues to work without modification.

Install / Upgrade

pip install --upgrade ka9q-python

v3.4.2: Bug Fixes and Enhanced Documentation

Choose a tag to compare

@mijahauan mijahauan released this 05 Feb 12:40

Release v3.4.2: Bug Fixes and Enhanced Documentation

This release focuses on improving the user experience through bug fixes, comprehensive documentation, and better organization of examples.

🐛 Bug Fixes

Fixed stream_example.py Bug

Fixed a critical bug in examples/stream_example.py where the code incorrectly iterated over the dictionary returned by discover_channels(). The example now correctly accesses ChannelInfo objects, preventing an AttributeError and allowing the example to run as intended.

This was a high-impact bug that would have prevented new users from successfully running one of the key examples.

📚 Documentation Enhancements

New Getting Started Guide

Added a comprehensive docs/GETTING_STARTED.md file that serves as the primary entry point for new users. This guide includes:

  • Step-by-step installation instructions
  • A simple, annotated first program (creating an AM radio channel)
  • Detailed explanations of core concepts (RadiodControl, ChannelInfo, stream abstraction layers)
  • A comparison table of the three stream APIs (RTPRecorder, RadiodStream, ManagedStream)
  • Complete working examples with ManagedStream
  • Common troubleshooting tips

Examples Organization

Added a new examples/README.md that transforms the examples directory into a structured learning resource:

  • Categorizes examples by complexity (basic, intermediate, advanced)
  • Provides detailed descriptions for each example (what it does, concepts demonstrated, how to run it)
  • Recommends a learning path for new users
  • Includes usage instructions and expected output for each example

Updated API Documentation

The documentation for create_channel() in docs/API_REFERENCE.md has been updated to match the actual implementation:

  • Added missing destination parameter (for RTP destination multicast address)
  • Added missing encoding parameter (for output encoding like F32, S16LE, OPUS)
  • Corrected parameter order to match the actual function signature
  • Added return type documentation (returns int SSRC)
  • Enhanced examples with both basic and advanced usage patterns

Updated Main README

The main README.md now links prominently to the new Getting Started guide, making it easier for new users to find the beginner-friendly tutorial.

📊 Impact

These changes significantly improve the user onboarding experience:

  • For new users: Clear, guided path from installation to first working application
  • For existing users: No breaking changes, just better documentation and a bug fix
  • For contributors: Better structure makes it easier to understand and contribute

📦 Installation

pip install ka9q-python==3.4.2

Or upgrade from a previous version:

pip install --upgrade ka9q-python

🔗 Links

📝 Full Changelog

Added

  • Comprehensive Getting Started Guide: Added docs/GETTING_STARTED.md with step-by-step tutorial for new users
  • Examples README: Added examples/README.md to organize examples by complexity and provide learning path

Fixed

  • stream_example.py Bug: Fixed incorrect dictionary iteration that caused AttributeError

Changed

  • Updated API Documentation: Complete documentation for create_channel() including all parameters
  • Updated README: Added link to Getting Started guide

Full Changelog: v3.4.1...v3.4.2

v3.3.0: ManagedStream for self-healing RTP streams

Choose a tag to compare

@mijahauan mijahauan released this 18 Dec 19:02

New Features

  • ManagedStream class: Wraps RadiodStream with automatic drop detection and channel restoration when radiod restarts
  • StreamState enum: Track stream health (STOPPED, STARTING, HEALTHY, DROPPED, RESTORING)
  • ManagedStreamStats: Monitor drops, restorations, and healthy/dropped durations
  • Callbacks: Get notified on stream drop and restoration events

Usage

from ka9q import RadiodControl, ManagedStream

stream = ManagedStream(
    control=control,
    frequency_hz=14.074e6,
    preset='usb',
    sample_rate=12000,
    on_samples=callback,
    on_stream_dropped=on_drop,
    on_stream_restored=on_restore,
    drop_timeout_sec=3.0,
    restore_interval_sec=1.0,
)
channel = stream.start()

This enables robust streaming applications that automatically recover from radiod restarts without manual intervention.

Installation

pip install ka9q-python==3.3.0