Skip to content

Lifetime keepalive silently reverts channel output encoding to the preset default (F32 grants lost fleet-wide) #3

Description

@mijahauan

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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions