A Textual-based terminal UI for monitoring and controlling a live
radiod channel. Modeled on ka9q-radio's ncurses control program:
eight panels of channel state and one-letter keybindings that drive the
same setters as ka9q set.
The TUI dependency (textual) ships as an optional extra:
pip install -e ".[tui]"
# or, for development:
pip install -e ".[dev,tui]"# Fully interactive: pick a radiod via mDNS, then pick an SSRC
ka9q tui
# Skip the radiod picker; still prompts for SSRC
ka9q tui bee1-hf-status.local
# Skip both pickers, jump straight to the live view
ka9q tui bee1-hf-status.local --ssrc 14095000
# Pin a network interface when the host is multi-homed
ka9q tui --interface eth0ka9q list HOST still works for non-interactive SSRC enumeration.
When host is omitted, the TUI opens a two-step picker before the live
view:
- Radiod picker — runs
discover_radiod_services()(avahi/mDNS browse of_ka9q-ctl._udp) and lists every responding instance. Use the arrows to highlight one and Enter to pick.rrescans,q/Esc quits. If only one service answers, it auto-selects. - SSRC picker — runs
discover_channels()against the chosen host and shows a sortable table ofSSRC │ Freq (MHz) │ Preset │ Rate │ SNR │ Dest. Enter picks the highlighted row,aselects "all SSRCs" (the listener latches onto whichever arrives first),rrescans, Esc returns to the radiod picker.
From the live view, M re-enters the picker chain to switch
radiod/SSRC without restarting the process.
radiod emits per-channel status packets in two situations:
- On change — when any setter modifies the channel.
- On poll — in response to a status-request command.
The TUI uses both:
- A background thread runs
RadiodControl.listen_status()and pushes any status packet for the focused SSRC onto an internal queue (tui.py:38-62). This captures change-driven updates and anything triggered by other clients (e.g. a runningcontrolinstance). - A 1 Hz timer calls
poll_status(ssrc, timeout=0.8)in a worker thread (tui.py:412-421), so values refresh every second even when the channel is idle. - The UI drains the queue and repaints every 200 ms (tui.py:399).
A one-shot poll_status(timeout=2.0) fires at mount to populate the
panels immediately.
Grid layout (3×3, last cell blank):
| Panel | Source fields |
|---|---|
| Tuning | Carrier, first/second LO, shift, channel filter edges, frontend filter edges, Doppler |
| Frontend / GPSDO | Description, input sample rate, AD bits, real/complex, calibrate_ppm, gpsdo_reference_hz (10 MHz reference), ad_over, samples_since_over, LNA/MIX/IF gains, RF gain/atten/AGC |
| Signal / Levels | IF power (dBFS), RF level cal, input dBm, baseband power, noise density, S/N₀, S/N + bandwidth, output level |
| Filter / FFT | Kaiser β, blocksize, FIR length, drops, noise BW, optional Filter2 block |
| Demod | Mode + demod name, plus mode-specific block (FM SNR / peak dev / PL tone / de-emphasis; linear AGC / ISB / envelope / PLL; spectrum RBW / bins / window / FFT N) |
| Options / Squelch | Lock, SNR squelch enable, open/close thresholds, ISB, envelope, AGC, FM threshold-extend |
| Input / Status | GPS time, cmd count, input sample rate, samples in, ADC overrange counters, status destination + interval |
| Output / RTP | SSRC, output rate, channels, encoding, TTL, destination, sample count, packet counters, errors, max delay, Opus parameters when present |
A "—" means the field is absent from the latest status packet — many fields depend on frontend/demod type (RX888 vs Airspy; linear vs FM vs spectrum), so not every cell is populated for every channel.
- GPSDO discipline:
Frontend / GPSDOpanel —Calibrate: ±x.xxx ppmis the frontend's fractional-frequency error vs. its 10 MHz reference, andRef (10M): …is the measured reference frequency. - ADC overranges: both
Frontend / GPSDOandInput / StatusshowAD over(total clip count) andsamples_since_over(how long ago the last clip occurred, in samples at the input sample rate).
Modeled after control. Parameter keys open a single-line modal
prompt; toggle keys act immediately.
| Key | Action | Setter |
|---|---|---|
f |
Carrier frequency (Hz) | set_frequency |
p |
Preset / mode | set_preset |
S |
Sample rate (Hz) | set_sample_rate |
s |
Squelch open (dB) | set_squelch |
G |
RF gain (dB) | set_rf_gain |
A |
RF attenuation (dB) | set_rf_attenuation |
g |
Linear gain (dB) | set_gain |
H |
Headroom (dB) | set_headroom |
L |
AGC threshold (dB) | set_agc_threshold |
R |
AGC recovery (dB/s) | set_agc_recovery_rate |
T |
AGC hang time (s) | set_agc_hangtime |
P |
PLL bandwidth (Hz) | set_pll |
K |
Kaiser β | set_kaiser_beta |
e |
Output encoding (S16LE, F32LE, OPUS…) | set_output_encoding |
b |
Opus bitrate (bps) | set_opus_bitrate |
t |
PL tone (Hz, 0 off) | set_pl_tone |
D |
Demod type (linear/fm/wfm/spect) | set_demod_type |
l |
Toggle channel lock | set_lock |
i |
Toggle independent sideband | set_independent_sideband |
v |
Toggle envelope detection | set_envelope_detection |
x |
Toggle FM threshold-extend | set_fm_threshold_extension |
M |
Re-pick radiod / SSRC | — |
? / h |
Show help overlay | — |
q / Ctrl-C |
Quit | — |
Prompt keys dispatch through SET_VERBS in
cli.py:163, so any value accepted by ka9q set
is also accepted in the modal — the TUI and CLI share one vocabulary.
If a setter raises, the header line shows ERR: … instead of dimming
the panels, so you can see the failure without losing context.
- tui.py — app, panels, keybindings, status worker.
- control.py
listen_status— passive receive on the status multicast. - control.py
poll_status— active poll (sends a command tagged with the target SSRC and waits for the matching status reply). - status.py
ChannelStatus— the typed dataclass every panel reads from.
Nothing updates / panels stay blank. Confirm the SSRC exists:
ka9q query HOST --ssrc N should print a status. If query works but
the TUI doesn't, the status multicast may be reaching the control
socket but not the passive listener — check --interface on multi-homed
hosts.
All values are "—". The SSRC is receiving packets but they lack
populated TLVs. Usually this means the channel is in an unusual demod
state; pressing p and re-setting the preset forces radiod to emit a
full status.
Key does nothing. Parameter keys require a focused SSRC. Pass
--ssrc explicitly or wait for the listener to latch onto one.