Summary
set_channel_lifetime() — the keepalive every client must run to stop channels self-destructing — makes radiod silently revert a channel's output encoding to the preset default. A channel created with encoding=Encoding.F32 runs as F32 for one keepalive interval and is S16BE from then on. Nothing notices, because verify_channel() checks frequency but never encoding.
Net effect: every long-lived channel in the fleet is S16BE regardless of what was requested.
Reproduction (AC0G-B4, radiod on the HamSCI fork, ka9q-python current)
Scratch channel, preset="iq", sample_rate=12000, encoding=Encoding.F32 (=4), lifetime=200:
scratch ssrc=1605393904, requested F32(4)
after creation : 4 (F32) <- request IS honored
-> sending set_channel_lifetime (the keepalive the recorder runs)
after 4 keepalives : 2 (S16BE) <- silently reverted
The request is not being denied at creation — it is honored and then lost. Confirmed against live production channels on the same host, all long-running, all reverted:
freq= 2500000.0 sr=24000 encoding=2
freq= 10000000.0 sr=24000 encoding=2
freq= 45375000.0 sr=96000 encoding=2 (hf-timestd T6, requests F32)
Why the request can't simply be re-asserted at creation
RadiodControl.create_channel() already can't send encoding in the creation packet — that path is disabled in-tree:
# Encoding setting - NOT sending in main buffer anymore
# Radiod requires OUTPUT_ENCODING to be sent in a separate command after creation
# if encoding > 0:
# encode_int(cmdbuffer, StatusType.OUTPUT_ENCODING, encoding)
so encoding is fired as a separate, unacknowledged follow-up command. That works — until the first keepalive undoes it.
Why this matters beyond precision
This is not only a resolution nit. F32 was chosen deliberately for the metrology chain: signal and noise vary substantially over a 24-hour period, and the requirement is to capture that without AGC, whose unpredictable scaling is unacceptable in scientific measurements. Silently dropping to fixed-point 16-bit with agc_enable=False, gain=0.0 reintroduces exactly the dynamic-range problem F32 was chosen to avoid — with no headroom strategy at either end.
Measured on B4's T6 channel: the signal sits at ~34 counts RMS out of ±32767, i.e. about 5 of 16 bits, for a quantization SNR of ~38–41 dB against a channel SNR of 51 dB. Quantization, not the RF, is currently the noise floor on that channel.
(For the T6 timing work in HamSCI/hf-timestd#7 this does not explain the displaced-peak bug — the 0.5 s integration averages quantization away entirely — but it does bound a proposed short-integration fine stage, which integrates ~5 samples rather than 48 000.)
Suggested fixes
- Re-assert encoding on every keepalive — have
set_channel_lifetime() carry OUTPUT_ENCODING (or re-send it immediately after), so the keepalive stops being lossy. Smallest change, fixes the fleet.
- Verify encoding in
verify_channel() / ensure_channel() so a lost grant is loud rather than silent. The current contract says "verifies the channel meets the requested specifications" while checking frequency only.
- Consider whether radiod should reset output encoding on a LIFETIME-only command at all — arguably the upstream bug, but (1) makes clients correct regardless of radiod version.
A station-side workaround for anyone who needs F32 now: set encoding in radiod's preset/config so the default is F32, rather than relying on the per-channel grant surviving.
Summary
set_channel_lifetime()— the keepalive every client must run to stop channels self-destructing — makes radiod silently revert a channel's output encoding to the preset default. A channel created withencoding=Encoding.F32runs as F32 for one keepalive interval and is S16BE from then on. Nothing notices, becauseverify_channel()checks frequency but never encoding.Net effect: every long-lived channel in the fleet is S16BE regardless of what was requested.
Reproduction (AC0G-B4, radiod on the HamSCI fork, ka9q-python current)
Scratch channel,
preset="iq",sample_rate=12000,encoding=Encoding.F32(=4),lifetime=200:The request is not being denied at creation — it is honored and then lost. Confirmed against live production channels on the same host, all long-running, all reverted:
Why the request can't simply be re-asserted at creation
RadiodControl.create_channel()already can't send encoding in the creation packet — that path is disabled in-tree:so encoding is fired as a separate, unacknowledged follow-up command. That works — until the first keepalive undoes it.
Why this matters beyond precision
This is not only a resolution nit. F32 was chosen deliberately for the metrology chain: signal and noise vary substantially over a 24-hour period, and the requirement is to capture that without AGC, whose unpredictable scaling is unacceptable in scientific measurements. Silently dropping to fixed-point 16-bit with
agc_enable=False, gain=0.0reintroduces exactly the dynamic-range problem F32 was chosen to avoid — with no headroom strategy at either end.Measured on B4's T6 channel: the signal sits at ~34 counts RMS out of ±32767, i.e. about 5 of 16 bits, for a quantization SNR of ~38–41 dB against a channel SNR of 51 dB. Quantization, not the RF, is currently the noise floor on that channel.
(For the T6 timing work in HamSCI/hf-timestd#7 this does not explain the displaced-peak bug — the 0.5 s integration averages quantization away entirely — but it does bound a proposed short-integration fine stage, which integrates ~5 samples rather than 48 000.)
Suggested fixes
set_channel_lifetime()carryOUTPUT_ENCODING(or re-send it immediately after), so the keepalive stops being lossy. Smallest change, fixes the fleet.verify_channel()/ensure_channel()so a lost grant is loud rather than silent. The current contract says "verifies the channel meets the requested specifications" while checking frequency only.A station-side workaround for anyone who needs F32 now: set
encodingin radiod's preset/config so the default is F32, rather than relying on the per-channel grant surviving.