v3.14.0 — per-(client, radiod) multicast destinations
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-derivesgenerate_multicast_ip(client_id, radiod_host=self.status_address).
Destination precedence:
- Explicit
destination=toensure_channel(operator override, unchanged). - Derived from
(client_id, status_address)whenclient_idwas set on theRadiodControl. 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)withoutclient_idpreserves pre-3.14 behavior:destination=Noneflows through, radiod uses its config-file default. Clients opt in by passingclient_id="<name>".- Bumping the floor for §7 conformance: clients should pin
ka9q-python>=3.14.0in theirpyproject.tomlwhen they adoptclient_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