A Python client-server system for controlling a Software Defined Radio (SDR) transceiver. cat_gui.py is a full-featured Tkinter desktop front-end; cat_server.py is a protocol-compliant back-end that ships with a built-in signal simulator so the GUI works immediately out of the box — and can be replaced (or extended) with a real SDR hardware driver.
- Overview
- System Architecture
- Feature Highlights
- Requirements & Installation
- Windows Installation & Usage
- Quick Start
- Configuration Files
- Command-Line Reference
- TCP Protocol Specification
- GUI Layout & Controls
- Audio System
- IQ & Audio WAV Playback (Server)
- Frequency Memories
- Device Profiles & Switching
- HiDPI / Scaling
- Theming & Fonts
- Generated Files Reference
- Extending the Server
CAT GUI implements a complete radio-control interface modeled on the look and feel of high-end SDR transceivers. The GUI connects to the server over a local (or remote) TCP socket and communicates via a simple newline-delimited JSON protocol. A separate UDP channel carries real-time bidirectional audio (receive audio from the server to the GUI speaker; microphone audio from the GUI to the server when PTT is active).
The reference server included here is a simulator: it generates synthetic RF carrier signals on a 192 kHz-wide spectrum, produces a 440 Hz receive audio tone, accepts every GUI command, and echoes all state changes back. You can play real IQ recordings and real receive audio through it with two flags (--iq_wav and --audio_wav). The server architecture is intentionally minimal so that it is straightforward to replace the signal-generation stub with a real SDR hardware driver (SoapySDR, RTL-SDR, SDRplay, etc.).
┌────────────────────────────────────────────────────┐
│ cat_gui.py (client) │
│ │
│ ┌───────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │ WFCanvas │ │SpecCanvas│ │ FreqDisp (×3) │ │
│ │(waterfall)│ │(spectrum)│ │ LO A / LO B / │ │
│ └───────────┘ └──────────┘ │ Tune │ │
│ └─────────────────┘ │
│ ┌──────────────┐ ┌────────────────────────────┐ │
│ │RTPAudioClient│ │ NetClient (TCP) │ │
│ │ UDP/G.711μ │ │ JSON newline-delimited │ │
│ └──────────────┘ └────────────────────────────┘ │
└──────────────┬──────────────────────┬──────────────┘
│ TCP (control) │ UDP (audio)
▼ ▼
┌────────────────────────────────────────────────────┐
│ cat_server.py (server) │
│ │
│ ┌────────────┐ ┌───────────────┐ ┌───────────┐ │
│ │RadioState │ │ClientHandler │ │UDPAudio │ │
│ │ (all SDR │ │ (per-client │ │Channel │ │
│ │ state) │ │ TCP thread) │ │(TX+RX RTP)│ │
│ └────────────┘ └───────────────┘ └───────────┘ │
│ ┌────────────┐ ┌───────────────┐ │
│ │IQWavSource │ │AudioWavSource │ │
│ │(--iq_wav) │ │(--audio_wav) │ │
│ └────────────┘ └───────────────┘ │
└────────────────────────────────────────────────────┘
TCP control channel — newline-delimited UTF-8 JSON objects. The GUI sends one command object per line; the server replies with {"resp": "ok"} for every command. For hello and select_device only, the reply also includes a full state dict: {"resp": "ok", "state": {...}}. While running, the server additionally pushes {"type": "data", ...} frames at ~10 Hz with fresh spectrum, S-meter, and squelch data.
UDP audio channel — RTP datagrams with a 12-byte header and a G.711 μ-law (PCMU) payload at 8 kHz / 8-bit / mono / 20 ms frames (160 bytes of μ-law per packet). Bidirectional: server → GUI when PTT is off (receive audio); GUI → server when PTT is on (microphone audio for TX).
- RF waterfall — O(width) per-frame incremental
PhotoImage.put()scroll, adjustable speed (1–10), freezes with "● TX" badge during transmit - RF spectrum — retained-item canvas (no
delete("all")per frame), draggable IF passband overlay, peak-hold display with configurable decay, VFO cursor line - AF waterfall — same engine as RF waterfall; driven from locally-decoded RTP audio (not a server-computed value), so what is drawn always matches what you hear
- AF spectrum — local FFT of received PCM; a Hamming window is applied in all cases; uses
numpy.fft.rfftwhen available, falls back to a pure-Python Cooley-Tukey radix-2 FFT otherwise - Reference level (SCALE) adjustable ±5 dB steps; FFT averaging (AVE) 1–10; per-box Waterfall / Spectrum toggle
- Frequency-axis grid lines and labels auto-scaled to a "nice" step size for any span or zoom level
- Dual LO (VFO A / B) plus a Tune display — three independent 9-digit amber frequency readouts with comma separators
- Per-digit mouse-wheel (or left/right-click) increment/decrement; double-click opens a direct Hz entry dialog
- SPLIT mode — LO A as RX, LO B as TX; TX/RX labels shown beside each LO display when active
- Band buttons — 160 m through 6 m (ITU Region 2 ranges); pressing one QSYs directly to the band's center frequency
- Per-device and per-antenna band restrictions — disabled band buttons are visually grayed out
- M (Memory) buttons — beside each frequency row; opens a 20-slot memory dialog per device
- Volume, AGC Threshold, RF Gain, Squelch — horizontal sliders, instantly reflected to server
- Mode buttons — LSB, USB, AM, FM, CW, and up to 10 user-defined modulation modes
- AGC — off / slow / medium / fast, plus a configurable AGC threshold slider
- Filter — passband dragged directly on the IF spectrum canvas; low and high edge independently adjustable
- Zoom — in/out buttons or mouse-wheel on the IF spectrum; zoom narrows the RF span displayed
- Toggle buttons: NB (noise blanker), NR (noise reduction), NB RF, NB IF, AFC, ANF, Notch, Mute
- S-meter — arc-style analog meter with peak-hold bar; numeric dBm and S-unit text readout; squelch open/closed LED. During transmit the arc automatically switches to an SWR gauge (scale 1.0–5.0) with colour-coded zones (green → red)
- Start / Stop — arms or disarms the SDR (server begins or stops streaming data)
- PTT — circular button, instantaneous TX/RX switching; waterfall/spectrum frozen with badge during TX
- SWAP — swaps the LO A and LO B frequencies in one click
- LOCK — locks the active LO (or both LOs when SPLIT is on) to prevent accidental frequency changes; the frequency displays and M buttons for locked LOs are disabled
- Transport bar — Record ●, Play ▶, Pause ⏸, Stop ■, Rewind ◀◀, Fast-forward ▶▶, Loop ∞
- Device selector — up to 20 named device profiles; switching saves current state and restores the target device's persisted state and memories
- Bandwidth selector — combobox populated from the server's
bandwidth_mapfor the current mode;◄/►step buttons shift the active LO by the selected bandwidth - TX Power selector — shown when the device profile defines
power_levels; opens a modal dialog that sendsset_powerto the server - Antenna selector — up to 10 labeled ports per device, each with its own optional band restriction
- Sample Rate selector — per-device list of selectable SDR sample rates
- Soundcard selector — PyAudio device enumeration; independently pick mic and speaker device
- User-defined buttons — 14 programmable buttons (7 + 7 rows), each independently
normal(momentary) orpush(toggle/latch) - RF user buttons — 11 programmable buttons left of the band array, same normal/push types; a long-press (≥ 3 s) opens an in-app config dialog whose widgets are defined per-button in the device profile's
config_Nkey - Text/RTTY pane — user-defined modulation modes can split the AF box to reveal a read-only text panel or a live RTTY-style bidirectional chat panel
- Auto-detects screen DPI and picks the best scale level; manual
+/−scale buttons always available - Scale factor is 1.25ˢᶜᵃˡᵉ (e.g., level 2 = 1.5625×); range −5 to +5
- Bottom control panel always stays fully visible at any window size — RF waterfall/spectrum shrink first
- Debounced
<Configure>resize handler prevents layout thrashing during live window drag --full-screenflag; triple-Esc toggle while running--resolution WxHand--aspect-ratio W:Hflags; aspect ratio is enforced after layout settles
Python 3.9 or later. Python 3.11+ includes tomllib in the standard library; older versions need tomli.
tkinter is included in the standard library but may require a separate OS package on some Linux distributions:
# Debian / Ubuntu
sudo apt install python3-tk
# Fedora / RHEL
sudo dnf install python3-tkinter| Package | Purpose | Install |
|---|---|---|
numpy |
Accelerated FFT for spectrum (both sides); required for --iq_wav on the server |
pip install numpy |
tomli |
TOML config support on Python < 3.11 | pip install tomli |
pyaudio |
Microphone and speaker audio (GUI only) | pip install pyaudio |
fonttools |
Authoritative PostScript family name lookup for custom fonts | pip install fonttools |
Without pyaudio the GUI runs normally but audio input/output is silently disabled. Without numpy spectrum rendering falls back to a pure-Python FFT (correct but slower). Without tomli/tomllib a built-in minimal TOML parser is used (covers all keys the bundled config templates produce).
# Clone or download the repository, then:
pip install numpy tomli pyaudio fonttools # all optional, install as neededNo setup.py or pyproject.toml is required — both scripts run directly.
Everything in this project runs on Windows without any UNIX-specific dependencies. The steps below cover a clean machine from scratch.
Download the Python 3.11 (or later) installer from python.org/downloads.
During installation:
- Check "Add python.exe to PATH" on the first screen — this is unchecked by default.
- Click "Customize installation" and confirm that "tcl/tk and IDLE" is checked. This installs
tkinter, which is the GUI toolkit used bycat_gui.py. If you skip this, the GUI will fail to importtkinterand will not start.
Verify after installation by opening Command Prompt (Win + R → cmd) and running:
python --version
python -c "import tkinter; print('tkinter OK')"Both commands should complete without error.
Open Command Prompt or PowerShell and run:
pip install numpy tomli pyaudio fonttoolspip install pyaudio frequently fails on Windows because it tries to compile a C extension without a compiler present. The cleanest solution is to install a pre-built wheel instead:
pip install pipwin
pipwin install pyaudioAlternatively, download the matching .whl file for your Python version from the Unofficial Windows Binaries for Python Extension Packages page (look for PyAudio‑0.2.x‑cpXXX‑cpXXX‑win_amd64.whl where XXX matches your Python version), then install it directly:
pip install PyAudio-0.2.14-cp311-cp311-win_amd64.whlIf PyAudio cannot be installed, the GUI still runs fully — audio input/output is silently disabled and a notice is printed to the console.
Download cat_gui.py and cat_server.py (and morgenta_regular.ttf if you want the bundled frequency font) into the same folder, for example C:\CAT.
All config files, state files, and memory files are created in the current working directory when the scripts are first run. It is best to cd into your project folder before launching anything:
cd C:\CATpython cat_server.pyOn first run (when no cat_server.toml or cat_device.toml exist), the server creates annotated .example template files next to where the real configs would live, then runs on built-in defaults. The console will show:
[config] cat_server.toml not found — using built-in defaults (copy cat_server.toml.example to cat_server.toml to customise)
[config] cat_device.toml not found — using built-in defaults (copy cat_device.toml.example to cat_device.toml to customise)
[cat_server] listening on 0.0.0.0:50101
To persist your settings, copy (or rename) the .example files:
copy cat_server.toml.example cat_server.toml
copy cat_device.toml.example cat_device.tomlWindows Firewall prompt — Windows may show a security alert the first time the server opens a socket. Click "Allow access" (at minimum for Private networks) so the GUI can reach it, even when both processes are on the same machine.
Open a second Command Prompt window, cd to the same folder, and run:
python cat_gui.pyOn first run this creates cat_gui.toml. The GUI window opens. Type 127.0.0.1 in the Host field and 50101 in the Port field (these are the defaults already shown), then click Connect followed by Start.
Tip — autoconnect: To skip the Host/Port/Connect row entirely on subsequent launches, edit
cat_gui.tomland set:[connection] host = "127.0.0.1" port = 50101 autoconnect = trueThe GUI will connect automatically on startup and the connection row will be hidden.
Because the server and GUI are separate processes, it is convenient to run each in its own window. A simple batch file for this:
start_all.bat (save in C:\CAT):
@echo off
cd /d %~dp0
start "CAT Server" cmd /k python cat_server.py
timeout /t 1 >nul
start "CAT GUI" cmd /k python cat_gui.pyDouble-click start_all.bat to launch both in separate titled windows. Closing either window shuts down that process cleanly.
Windows often has multiple audio endpoints (e.g., speakers, headphones, virtual cable). To list all devices and their indices:
python cat_gui.py --audio-listThen launch the GUI targeting specific devices:
python cat_gui.py --audio-mic 1 --audio-speaker 2Or set them persistently in cat_gui.toml:
[audio]
mic = 1
speaker = 2Custom TTF/OTF fonts work on Windows without administrator rights. The GUI calls AddFontResourceExW with FR_PRIVATE | FR_NOT_ENUM flags, which registers the font in-process only — no system-wide installation and no UAC prompt. Simply point --freq-font at any .ttf or .otf file:
python cat_gui.py --freq-font "C:\Fonts\MyFont.ttf"Or in cat_gui.toml:
[display]
freq_font = "C:\\Fonts\\MyFont.ttf"Note the double backslashes in TOML strings, or use forward slashes (both work on Windows):
freq_font = "C:/Fonts/MyFont.ttf"On high-DPI monitors Windows applies display scaling. If the GUI appears blurry or oversized, Python may be receiving pre-scaled coordinates from Windows. The auto-scale logic already compensates by reading the real screen resolution and selecting the best scale level, but you can override it:
python cat_gui.py --scale 2Or set it in cat_gui.toml:
[display]
scale = 2
disable_scale = falseYou can also right-click python.exe → Properties → Compatibility → Change high DPI settings → "Override high DPI scaling behavior: Application" to let Python handle DPI itself rather than Windows.
python cat_gui.py --full-screenOnce running, press Esc three times within one second to toggle full-screen on or off.
If the server and GUI run on different machines (e.g., server on a shack PC, GUI on a laptop over LAN), you must allow inbound connections on both the TCP control port and the UDP audio port through Windows Defender Firewall:
- Open Windows Defender Firewall with Advanced Security (
wf.msc). - Add an Inbound Rule → Rule Type: Port → TCP → port
50101→ Allow. - Add a second Inbound Rule → Rule Type: Port → UDP → port
5004→ Allow.
Then launch the server normally and point the GUI at the server's LAN IP:
python cat_gui.py --host 192.168.1.10 --port 50101If python is not found after installation, use the full path (C:\Users\YourName\AppData\Local\Programs\Python\Python311\python.exe) or re-run the Python installer and check "Add Python to environment variables" in the Customize step.
If pip is not found, run:
python -m pip install numpy tomli pyaudio fonttoolsIf the console prints garbled characters (rare on modern Windows 10/11), set the code page to UTF-8 before running:
chcp 65001
python cat_server.py1. Start the server (default port 50101, simulated RF signals):
python cat_server.pyOn first run, if cat_server.toml or cat_device.toml are absent, the server creates .example template files and runs on built-in defaults. Copy the examples to activate custom configuration:
cp cat_server.toml.example cat_server.toml
cp cat_device.toml.example cat_device.toml2. Start the GUI (connects to 127.0.0.1:50101 by default):
python cat_gui.py3. In the GUI, click Connect, then click Start.
The RF waterfall and spectrum will begin scrolling, the S-meter will animate, and a 440 Hz receive tone will play through the system speaker (if PyAudio is installed).
Both sides self-correct their TOML configuration files on every run: if a key is missing (e.g. after an upgrade adds a new option), it is added at its default value and the file is rewritten in place.
Created in the working directory as cat_gui.toml (override with --config PATH).
# CAT GUI configuration
# CLI flags override these values at runtime without modifying this file.
[display]
bg = "dark" # "light" or "dark"
full_screen = false # start in full-screen mode
scale = 0 # HiDPI scale level, -5 to 5
disable_scale = false # hide the +/- scale controls
freq_font = "" # path to TTF/OTF font for frequency digit displays
gui_font = "" # path to TTF/OTF font for all other GUI text
[connection]
# Both host and port must be set, and autoconnect = true, to connect on startup.
# With autoconnect = true the host/port/connect row is hidden from the GUI entirely.
host = ""
port = 0
autoconnect = false
[audio]
# Device indices from --audio-list; -1 = system default.
# Both mic and speaker must be set together (or both left at -1).
mic = -1
speaker = -1
disable_soundcard_select = falseCreated as cat_server.toml (override with --config PATH). Contains server transport settings and the list of up to 20 named device profiles.
[server]
host = "0.0.0.0"
port = 50101
[audio]
audio_port = 5004
no_audio = false
[devices]
# Up to 20 device profiles. Empty label = slot unused.
label_1 = "Main SDR"
config_1 = "devcfg_main.toml"
label_2 = ""
config_2 = ""
# ... label_3 / config_3 ... label_20 / config_20Created as cat_device.toml (override with --device-config PATH). Defines the GUI layout for one device: its programmable buttons, modulation modes, SDR sample rates, and antenna ports.
[user_buttons]
# Up to 14 user-defined buttons. Slots must be filled in order (no gaps).
label_1 = "CW Spot"
type_1 = "push" # "normal" (momentary), "push" (toggle/latch), or "list" (selection dropdown)
list_1 = "" # comma-separated dropdown items when type is "list" (max 20 chars each)
label_2 = ""
# ... label_3 / type_3 / list_3 ... label_14 / type_14 / list_14
[user_mods]
# Up to 10 user-defined modulation buttons. Slots must be filled in order.
label_1 = "RTTY"
type_1 = "text_input" # "normal", "text", or "text_input"
# ... label_2 / type_2 ... label_10 / type_10
[rf_usr_btns]
# Up to 11 buttons shown left of the band array in the RF panel.
# Buttons with empty labels are hidden in the GUI.
label_1 = "ATU"
mode_1 = "push" # "normal" (momentary) or "push" (push-push toggle)
# config_1: JSON-encoded dict defining the long-press (≥ 3 s) configure dialog.
# Empty string or "{}" = no dialog for this button.
# Each key is the widget label; the value is a widget spec object.
# Four widget types are supported:
# slide → {"type": "slide", "range": [min, max]}
# list → {"type": "list", "values": [{"key": "Label", "val": "value"}, …]}
# check → {"type": "check"}
# radio → {"type": "radio", "options": ["val1", "val2", …]}
# Example (slider 0–100, a two-option radio group, and a checkbox):
config_1 = '{"Power": {"type":"slide","range":[0,100]}, "Band": {"type":"radio","options":["HF","VHF"]}, "Bypass": {"type":"check"}}'
# ... label_2 / mode_2 / config_2 ... label_11 / mode_11 / config_11
[bandwidth]
# Available filter bandwidths (Hz) for each modulation mode.
# A matching entry is required for every label defined in [user_mods];
# the server will refuse to start (sys.exit) if any label is missing here.
AM = "3000,6000,9000,10000"
FM = "12500,25000"
LSB = "2700,3600"
USB = "2700,3600"
CW = "250,500,1000,2000"
# RTTY = "250,500" ← add one line per [user_mods] label you define
[sdr]
sample_rate = 192000
# Comma-separated list of selectable rates for this device.
sample_rates = "192000,250000,500000,1000000,2000000"
# Comma-separated list of bands this device may tune to (empty = all).
allowed_bands = "160m,80m,60m,40m,30m,20m,17m,15m,12m,10m,6m"
# Comma-separated TX power levels in watts; empty = power selector hidden.
power_levels = "5.0,10.0,25.0,50.0,100.0"
[antenna]
# Up to 10 antenna ports. Empty label = slot unused/hidden.
label_1 = "Dipole"
allowed_bands_1 = "" # empty = inherit device-level allowed_bands
label_2 = "HF Vertical"
allowed_bands_2 = "40m,20m,15m,10m"
# ... label_3 / allowed_bands_3 ... label_10 / allowed_bands_10Each entry in cat_server.toml's [devices] section points to a separate cat_device.toml-compatible file for that profile. Switching devices in the GUI loads that profile's buttons, sample rates, memories, and last-saved GUI state.
These are generated automatically next to each device's config file:
| File | Content |
|---|---|
<device>.gui_state.json |
Persisted operator settings: frequencies (LO A/B/Tune), mode, filter, AGC, gains, squelch, toggles, zoom, sample rate, button states, antenna selection, spectrum display settings, selected bandwidth (selected_bw), RF user button configure-dialog values (rf_usr_btn_config_vals) |
<device>.memories.json |
3 × 20 frequency memories (LO A, LO B, Tune) with labels and frequencies |
State is saved when the operator switches away from a device and restored when they switch back. Memory slots are written immediately whenever a slot is saved from the GUI.
cat_server.py supports translating device-defined button/menu labels via the --lang LOCALE flag (e.g. --lang es, --lang de, --lang pt_BR). Each device config file needs its own, separate translations file, named <device_base>_labels_<lang>.toml, placed alongside it:
devices/g90.toml ← device config
devices/g90_labels_es.toml ← Spanish label overrides for g90.toml
Translation files are never shared across multiple device configs — one file per device. Lookup falls back from an exact locale match (e.g. pt_BR) to the base language (e.g. pt) if no exact-match file is found. If no matching file exists for the requested locale, the server runs with the original (untranslated) labels.
python cat_gui.py [OPTIONS]
Connection:
--host HOST Server hostname or IP (must pair with --port)
--port PORT Server TCP port (must pair with --host)
--autoconnect Connect automatically on startup; hides the
host/port/connect row in the GUI
Display:
--bg {light,dark} Background theme ("dark" is the default)
--full-screen Start in full-screen mode (triple-Esc to toggle)
--resolution WxH Initial window size in pixels, e.g. 1280x720
--aspect-ratio W:H Lock window to an aspect ratio, e.g. 16:9 or 4:3
(ignored when --full-screen is set)
--scale INT Initial HiDPI scale level, -5 to 5 (0 = auto-detect)
--disable-scale Hide the +/- scale buttons (pair with --scale)
--freq-font PATH TTF/OTF font file for the LO/Tune frequency displays
--gui-font PATH TTF/OTF font file for all other GUI text
Audio:
--audio-list Print all audio device indices and exit
--audio-mic INDEX Select microphone device by index (pair with --audio-speaker)
--audio-speaker INDEX Select speaker device by index (pair with --audio-mic)
--disable-soundcard-select
Hide the Soundcard button in the GUI
Misc:
--config PATH Load GUI TOML config from PATH instead of ./cat_gui.toml
--debug Enable verbose debug output on the console
Band Restriction:
--restrict-band Hard-block any LO change that falls outside the active
device's allowed_bands (standard amateur bands outside
that list are always rejected). Without this flag,
allowed_bands only grays out band buttons; keyboard and
mouse-wheel frequency entry are unrestricted.
Layout:
--disable-upper-box Hide the upper-left toggle button that opens the
upper spec/waterfall panel.
python cat_server.py [HOST] [PORT] [OPTIONS]
Transport:
HOST TCP listen address, positional (default: 0.0.0.0)
PORT TCP listen port, positional (default: 50101)
--audio-port PORT UDP RTP audio port (default: 5004)
--no-audio Disable the UDP audio channel entirely
Config files:
--config PATH Load cat_server.toml from PATH instead of ./cat_server.toml
--device-config PATH Load cat_device.toml from PATH instead of ./cat_device.toml
Localization:
--lang LOCALE Locale tag for device label overrides (e.g. es, de, pt_BR).
Each device config file must have its own translations file
named <device_base>_labels_<lang>.toml alongside it, e.g.
devices/g90_labels_es.toml for devices/g90.toml. One
translations file per device; never share one file across
multiple device configs. Falls back from an exact locale
match (e.g. pt_BR) to the base language (e.g. pt) if no
exact match is found.
IQ & Audio:
--iq_wav PATH WAV file of IQ samples to use for the RF spectrum/waterfall
(stereo PCM/float, I=left, Q=right; optional auxi chunk for
center frequency). Looped forever. Requires numpy.
--audio_wav PATH WAV file to transmit as simulated receive audio (looped).
Resampled to 8 kHz mono. Replaces the built-in 440 Hz tone.
User-defined buttons (also settable via cat_device.toml):
--user-button-label-N TEXT Label for user button N (1–14, max 7 chars)
--user-button-type-N TYPE "normal" or "push" for user button N
User-defined modulation modes:
--user_mod_N LABEL Label for user-mod button N (1–10, max 4 chars)
--user_mod_type_N TYPE "normal", "text", or "text_input" for slot N
RF user buttons:
--rf_usr_btn_N LABEL Label for RF user button N (1–11, max 7 chars)
--rf_usr_btn_mode_N M "normal" or "push" for RF user button N
Note: HOST and PORT are positional arguments, not flags — e.g. python cat_server.py 192.168.1.50 50101 --no-audio, not --host 192.168.1.50 --port 50101.
Priority: CLI flags always beat the TOML config file, which beats built-in defaults. Button/mod slot flags must be specified sequentially (1, 2, 3 …) with no gaps; the server will error if a slot is skipped.
All messages are UTF-8, newline-terminated JSON objects (\n). One object per line in both directions. The server accepts multiple simultaneous TCP clients (each in its own thread).
Every command receives an immediate {"resp": "ok"} reply. Commands marked ★ also return a full state dict in the same reply: {"resp": "ok", "state": {...full radio state...}}.
Note: the server implements every command in this section. The bundled
cat_gui.pyclient, however, currently only sends the commands marked with a ✦. Commands without a ✦ (set_freq,set_agc,set_nb,set_nbrf,set_nbif,set_nr,set_afc,set_anf,set_notch,set_mute) are accepted by the server and update real state, but have no corresponding control in the bundled GUI yet — they're available for alternative/custom clients.
| Command | Fields | Notes |
|---|---|---|
hello ★✦ |
— | Sent on connect; triggers a reload_state push and returns full state |
| Command | Fields | Notes |
|---|---|---|
set_freq |
hz: int |
Set LO A (main receive) frequency |
set_lo_a_freq ✦ |
hz: int |
Set LO A (main receive) frequency — this is what the bundled GUI actually sends; set_freq and set_lo_a_freq are handled identically by the server |
set_lo_b_freq ✦ |
hz: int |
Set LO B (split TX) frequency |
set_tune_freq ✦ |
hz: int |
Set Tune (BFO/IF offset) frequency |
set_lo ✦ |
lo: "A"|"B" |
Select active LO |
| Command | Fields | Notes |
|---|---|---|
set_mode ✦ |
mode: str |
e.g. "USB", "LSB", "AM", "FM", "CW" |
set_agc |
mode: str |
"off", "slow", "medium", "fast" — server-only, no GUI control yet |
set_agc_thresh ✦ |
value: float |
AGC threshold in dBm (−140 to −20) |
set_filter ✦ |
lo: int, hi: int |
IF passband edges in Hz (e.g. lo=100, hi=2800) |
set_zoom ✦ |
value: int |
Zoom factor (≥ 1) |
set_rf_gain ✦ |
value: float |
RF gain in dB (0–60) |
set_volume ✦ |
value: float |
Audio volume (0–100) |
set_squelch ✦ |
value: float |
Squelch level in dBm (−140 to 0) |
set_nb |
enabled: bool |
Noise blanker (audio/IF) — server-only, no GUI control yet |
set_nbrf |
enabled: bool |
Noise blanker (RF) — server-only, no GUI control yet |
set_nbif |
enabled: bool |
Noise blanker (IF) — server-only, no GUI control yet |
set_nr |
enabled: bool |
Noise reduction — server-only, no GUI control yet |
set_afc |
enabled: bool |
Automatic frequency control — server-only, no GUI control yet |
set_anf |
enabled: bool |
Automatic notch filter — server-only, no GUI control yet |
set_notch |
enabled: bool |
Manual notch filter — server-only, no GUI control yet |
set_mute |
enabled: bool |
Audio mute — server-only, no GUI control yet |
set_selected_bw ✦ |
value: int |
Set the active bandwidth from the current mode's bandwidth_map list (Hz) |
| Command | Fields | Notes |
|---|---|---|
set_spec_ref |
box: "rf"|"af", value: float |
Reference level (top of display), snapped to nearest 5 dB, range −50 to +10 |
set_spec_ave |
box: "rf"|"af", value: int |
FFT averaging count, 1–10 |
| Command | Fields | Notes |
|---|---|---|
set_ptt |
enabled: bool, udp_port: int |
Activate/deactivate PTT; udp_port tells the server where to send TX audio |
set_split |
enabled: bool |
Enable/disable SPLIT (LO A RX, LO B TX) |
start |
— | Begin SDR streaming |
stop |
— | Stop SDR streaming |
transport |
action: str |
"rec", "play", "pause", "stop", "rw", "ff", "infinite" |
| Command | Fields | Notes |
|---|---|---|
get_devices |
— | Returns {"type": "device_list", "devices": [...]} |
select_device ★ |
index: int |
1-based device index; saves current state, loads new device |
get_sample_rates |
— | Returns {"type": "sample_rate_list", "rates": [...], "current": N} |
set_sample_rate |
value: int |
Set sample rate (must be in this device's configured list) |
get_antennas |
— | Returns {"type": "antenna_list", "antennas": [...], "current": N, "device_allowed_bands": [...]} |
select_antenna |
index: int |
1-based antenna port index (0 = deselect) |
get_power_levels |
— | Returns the list of TX power levels for the current device |
set_power |
index: int |
Select TX power level by 0-based index from the device's power_levels list; silently ignored (with a console warning) if index is out of range |
| Command | Fields | Notes |
|---|---|---|
user_button |
index: int |
Momentary press of user button N (1-based) |
user_button |
index: int, enabled: bool |
Push-push (toggle) state of user button N |
user_button |
index: int, choice: int |
Selection index for a "list"-type user button |
rf_usr_button |
index: int |
Momentary press or push-push toggle of RF user button N (1–11, left of band buttons) |
rf_usr_button |
index: int, enabled: bool |
Explicit push state for a "push"-type RF user button |
rf_usr_btn_config_set |
index: int, values: {name: value, …} |
Store configure-dialog values for RF user button N; persisted to .gui_state.json |
user_text |
index: int, text: str |
Text submitted by the operator in a text_input mode panel |
ui_display |
box: str, view: str |
Waterfall / Spectrum view toggle |
ui_toolbar |
box: str, action: str |
Toolbar button click (Waterfall / Spectrum toolbar) |
ui_smeter_btn |
action: str |
S-meter button click (Peak / S-units / Squelch) |
ui_button |
action: str |
GUI control button (Full Screen, SDR-Device, FreqMgr, Minimize, Exit) |
memory |
— | Legacy momentary "M" button press (no-op; kept for backward compatibility with older GUI builds) |
| Command | Fields | Notes |
|---|---|---|
get_memories |
position: "LO A"|"LO B"|"Tune" |
Returns the 20-slot list for that row |
save_memory |
position: str, index: int, label: str, freq: float |
Saves a slot and persists immediately to disk |
| Command | Fields | Notes |
|---|---|---|
audio_hello |
udp_port: int |
Registers the GUI's UDP endpoint with the server's audio channel |
{
"type": "data",
"f_start": 28390000,
"f_stop": 28590000,
"spectrum": [-120.5, -118.3, ...], // NUM_BINS dBm values
"af_range": 3000,
"af_spectrum": [-95.1, -93.0, ...], // AF_BINS dBm values (0..3000 Hz)
"smeter_dbm": -73.0,
"smeter_text": "S9",
"squelch_open": true,
"swr": null, // float (e.g. 1.35) while PTT on; null otherwise
"state": { ... } // incremental state fields
}The GUI uses f_start/f_stop to position the RF spectrum/waterfall frequency axis; af_range (always 3000 Hz) for the AF display axis.
| Type | Fields | Meaning |
|---|---|---|
audio_port |
port, sample_rate, frame_ms, codec |
Emitted on client connect; tells the GUI which UDP port to open for audio |
reload_state |
— | GUI should resync all widgets from the preceding resp:ok state |
device_list |
devices: [{index, label}] |
Response to get_devices |
sample_rate_list |
rates: [int], current: int |
Response to get_sample_rates |
antenna_list |
antennas: [...], current: int, device_allowed_bands: [...] |
Response to get_antennas |
memory_list |
position: str, memories: [{label, freq}×20] |
Response to get_memories or save_memory |
power_level_list |
levels: [str], current: int |
Response to get_power_levels; drives the TX Power selector dialog |
bandwidth_map |
map: {mode: [int, ...]} |
Sent on connect and on device switch; populates the Bandwidth selector combobox per mode |
user_text |
index: int, text: str |
Server-pushed text to a text/text_input panel slot |
disconnected |
(optional reason) |
Emitted by GUI internally when the TCP connection drops |
The full state dict (sent in resp:ok for hello / select_device, and incrementally in data frames) reflects every attribute tracked by RadioState on the server:
center_freq, tune_freq, lo_b_freq, lo_active
mode, agc, agc_thresh
filter_lo, filter_hi
rf_gain, volume, squelch
nb, nr, nbrf, nbif, afc, anf, notch, mute
ptt, split, running
zoom, sample_rate
user_buttons, user_btn_state, user_btn_list_sel
rf_usr_btns, rf_usr_btn_state, rf_usr_btn_config_vals
user_mod_labels, user_mod_types
spec_ref_rf, spec_ave_rf, spec_ref_af, spec_ave_af
allowed_bands, antenna_labels, antenna_index, antenna_allowed_bands
bandwidth_map, selected_bw
power_levels, power_index
active_device_index
Note — server state vs. GUI-exposed state: the dict above is the full server-side state (everything RadioState.apply() accepts and tracks). The bundled cat_gui.py only reads/displays a subset of it; in particular it stores the main receive frequency under the key lo_freq rather than center_freq (the two refer to the same value — the server's center_freq is what the GUI calls lo_freq), and it does not currently expose UI controls for agc, nb, nr, nbrf, nbif, afc, anf, notch, or mute — those fields arrive in every state push but nothing in the GUI reads or displays them. They are part of the protocol and can be driven by a custom/alternative client (or a future GUI update) using the corresponding set_agc, set_nb, set_nr, set_nbrf, set_nbif, set_afc, set_anf, set_notch, and set_mute commands documented above, all of which the server already implements. See State Variables → Hamlib Mapping for the smaller set of state keys the GUI itself actually tracks.
Key notes:
rf_usr_btn_config_vals— dict keyed by 1-based button index (as string); value is a{name: value}dict of the last values submitted viarf_usr_btn_config_set. Persisted to.gui_state.json.selected_bw— currently selected bandwidth as an Hz string (e.g."2700"). Persisted per-device.active_device_index— 1-based index of the active device profile (0 = none). Used by the GUI to restore the device label on startup and mark the active device in the Device dialog.antenna_allowed_bands— list of 10 sorted-band-name lists (one per antenna slot). Empty inner list = inherit device-levelallowed_bands.swrin data frames only — float SWR reading (e.g.1.35) while PTT is on;nullwhen PTT is off. Drives the SWR gauge on the S-meter. (Not in the state dict; only intype: "data"frames.)
The RF panel fills the top portion of the window and is divided into two vertically stacked subpanels:
RF Waterfall (WFCanvas) — the larger, expanding panel at the top. New spectrum rows are prepended at the top so the newest data is always at the top and history scrolls down. The scroll rate is controlled by the Speed knob in the toolbar. During PTT the waterfall is frozen and a "● TX" badge is shown.
RF Spectrum (SpecCanvas) — fixed-height strip below the waterfall. Drawn with a retained-object technique (all canvas items created once at startup; each frame only updates coordinates). Shows: spectrum trace with green fill, a semi-transparent IF passband overlay (blue rectangle with draggable edges), a VFO cursor line (red), and a peak-hold line (white-blue). The IF filter edges can be dragged directly with the mouse. Mouse-wheel on the spectrum zooms in/out.
A narrow strip between the RF panel and the bottom row contains per-box controls for both the RF panel (above) and the AF panel (below):
- Waterfall / Spectrum — mutually exclusive toggle buttons to switch the display mode for this box
- SCALE — reference level (top of display) in dB; +/− buttons step by 5 dB (range −50 to +10)
- AVE — FFT averaging count; +/− buttons step by 1 (range 1–10)
- Speed — waterfall scroll speed; +/− buttons step by 1 (range 1–10)
- RBW / Span labels (informational)
The left-hand panel is fixed-width and hosts all transceiver controls, top to bottom:
S-meter row — arc-style analog S-meter canvas with animated needle, peak-hold indicator, squelch open/closed LED, and a PTT circular button pinned to the right. When PTT is active the arc gauge automatically switches to an SWR meter (scale 1.0–5.0) with colour-coded zones (green for low SWR, stepping through amber to red at high SWR); the dBm / S-unit text area is replaced with a numeric SWR readout. The S-meter resumes when PTT is released.
Frequency displays — three FreqDisp widgets (LO A, LO B, Tune). Each shows 9 amber digits with thousands separators. A row of LO A/B selector buttons sits between the displays; SPLIT state shows TX/RX labels beside the active LOs. An M button beside each row opens the frequency memory dialog for that row.
SWAP / LOCK / BW row — immediately below the LO selector buttons:
- SWAP — exchanges the LO A and LO B frequencies in one click.
- LOCK — toggles a frequency lock on the active LO. When locked the frequency display and M button for that LO are disabled to prevent accidental QSY. With SPLIT active, LOCK applies to both LO A and LO B simultaneously.
- ◄ / ► — shift the active LO down or up by the currently selected bandwidth.
- Bandwidth combobox — dropdown populated from
bandwidth_map[current_mode]; selecting a value sendsset_selected_bwto the server.
Band buttons — 11 ITU Region 2 bands (160 m – 6 m). Clicking QSYs LO to the band center. Buttons outside the device's allowed_bands (or the selected antenna's restriction) are grayed out automatically.
Volume / AGC Threshold / RF Gain / Squelch — four horizontal sliders with labels.
Device / Sample Rate / Soundcard — buttons that open modal selection dialogs.
Power — TX power level button; shown only when the server reports power_levels for the current device. Opens a level-selection dialog; the chosen level is sent as set_power.
Mode buttons — standard modulation modes (LSB, USB, AM, FM, CW, …) plus up to 10 server-defined user modulation modes.
DSP toggles — NB, NR, AGC, Filter, AFC, ANF, Notch, Mute — each a two-state push button with green highlight when active.
Transport bar — Record ●, Play ▶, Pause ⏸, Stop ■, Rewind ◀◀, Fast-forward ▶▶, Loop ∞.
Start button — arms/disarms the SDR. Text changes to "Stop" while running.
User-defined button rows — 14 buttons in two rows of 7. Labels and types come from the server; unlabeled buttons are hidden.
RF user buttons — 11 buttons shown above the band buttons in the RF panel, to the left of the band array. A long-press (≥ 3 seconds) on any button opens a runtime configure dialog. The dialog's widgets are defined in the device profile's config_N key for that button (see cat_device.toml above); values submitted in the dialog are sent as rf_usr_btn_config_set and persisted to .gui_state.json.
Date/time + connection controls — UTC clock (green); host/port fields and a Connect button with a status LED. In autoconnect mode the entire row is hidden.
The right half of the bottom row is the Audio Frequency panel, driven entirely from locally-decoded RTP audio (not server-computed):
- AF Waterfall — same
WFCanvasengine; shows 0–3000 Hz audio frequency content scrolling in real time - AF Spectrum — same
SpecCanvasengine; 0–3000 Hz range, no filter overlay (AF has no draggable passband) - AF Toolbar — same controls as RF toolbar (Scale, Ave, Speed, Waterfall/Spectrum toggle)
- Text/RTTY Pane — when a
textortext_inputuser-mod mode is selected, the AF box splits horizontally. The right side shows a read-only text display (server-pushed messages) and, fortext_inputmodes, an editable 3-line input box that sends its contents asuser_textwhen the operator presses Enter. Each user-mod slot has its own independent text history.
The GUI's RTPAudioClient manages the UDP audio channel:
- Receive (PTT OFF): The server sends one RTP/PCMU packet every 20 ms. The GUI decodes μ-law to 16-bit PCM and writes to a PyAudio output (speaker) stream via a deque ring buffer and a PyAudio callback.
- Transmit (PTT ON): The GUI opens a PyAudio input (mic) stream, reads 160-sample PCM frames, encodes to μ-law, packs into RTP, and sends UDP datagrams to the server.
- AF spectrum feed: Decoded PCM samples are accumulated in a rolling ring buffer; a background worker thread fires an FFT every ~50 ms and posts the result to the GUI's Tk event queue for display in the AF waterfall/spectrum.
- The μ-law codec uses precomputed 256-entry decode and 65536-entry encode lookup tables (built once at import time) for a zero-branch, zero-struct hot path on every audio frame.
- Soundcard selection (microphone and speaker independently) via the Soundcard dialog or
--audio-mic/--audio-speakerflags;--audio-listprints all available device indices.
Drives the RF spectrum and waterfall from a real IQ recording instead of the synthetic signal generator. Accepts SDRplay-style stereo WAV files where the left channel is I and the right channel is Q, at any integer or float PCM depth (8/16/32-bit). If the file contains an auxi chunk the recorded centre frequency is extracted and used to seed the initial tuned frequency.
The file is looped forever (tape-loop playback). An IQ FFT (IQ_FFT_SIZE = 4096 bins, Hanning window, fftshift) converts each block to a dBm-approximate power spectrum; the zoom level then crops and resamples the full-bandwidth result to the GUI's display width.
Requires numpy.
Replaces the built-in 440 Hz demo tone with a real WAV file (mono or stereo PCM/float at any sample rate). Stereo is downmixed to mono; the audio is resampled to 8 kHz if its native rate differs. The file loops forever as the simulated receive audio stream delivered to the GUI.
Each device profile has its own independent set of memories: 20 slots for each of three frequency positions (LO A, LO B, Tune) = 60 slots per device.
Opening the memory dialog (clicking an M button beside a frequency display) sends get_memories to the server. The server returns all 20 slots for that position from the active device's memory file. The operator can:
- Recall a memory slot — sends
set_freq(or equivalent) and closes the dialog - Save the current frequency into a slot — opens a label-entry dialog, then sends
save_memory - Edit a slot's label in-place
Memories are written to disk immediately every time a slot is saved; they survive server restarts and device switches.
Up to 20 device profiles can be defined in cat_server.toml's [devices] section. Each entry pairs a display label with a path to a cat_device.toml-compatible config file.
When the operator clicks the Device button, the GUI sends get_devices; the server replies with a list dialog. Selecting a device sends select_device:
- The server saves the current device's GUI state (frequencies, mode, filter, gains, toggles, etc.) to its
.gui_state.jsonfile. - The new device's
cat_device.toml-like file is loaded; user buttons, modulation buttons, RF buttons, sample rates, band restrictions, and antenna ports are replaced. - The new device's
.gui_state.jsonis restored (frequencies, mode, toggles, etc.). - The new device's
.memories.jsonis loaded. - A
reload_statemessage is sent; the GUI resyncs all widgets.
On startup, if a [devices] list is configured and --device-config was not explicitly passed, the server auto-selects device 1 so that its persisted state file is used from the very first connection (avoiding a "phantom" identity mismatch on first connect).
All geometry constants are defined in a BASE dictionary at scale 1.0. The effective scale factor is 1.25 ^ scale_level. Level 0 targets a 1280×720 display; the auto-detection logic picks the largest level whose default window size (1520×870 at level 0) fits in 90% of the screen:
| Scale Level | Factor | Target Resolution |
|---|---|---|
| −5 | 0.33× | very small displays |
| −4 | 0.41× | — |
| −3 | 0.51× | — |
| −2 | 0.64× | — |
| −1 | 0.80× | < 1280×720 |
| 0 | 1.00× | 1280×720 |
| 1 | 1.25× | 1920×1080 |
| 2 | 1.56× | 2560×1440 |
| 3 | 1.95× | — |
| 4 | 2.44× | 3840×2160 |
| 5 | 3.05× | — |
The + / − scale buttons in the top-right corner of the window increment/decrement the level at runtime; all fonts, widget sizes, padding, and canvas geometry recalculate immediately. The scale overlay shows the current level and fades after a few seconds.
The layout engine ensures the bottom control panel (S-meter row through the date/time row) is always fully visible at any window height: only the RF waterfall/spectrum above shrinks to accommodate the bottom panel.
The default dark theme uses a deep navy/teal colour scheme:
| Role | Hex | Description |
|---|---|---|
| Window / waterfall background | #020814 |
Deep dark blue |
| Control panel | #0c1525 |
Dark navy |
| Spectrum background | #010610 |
Near-black navy |
| Spectrum trace | #22cc44 |
Green |
| VFO cursor | #ff2828 |
Red |
| Frequency digits | #ffb800 |
Amber |
| Active button | #1a3c6a / #50c0ff |
Blue |
| S-meter bar | #28ee50 → #ff3830 |
Green → red |
| Peak hold | #e0e8ff |
White-blue |
The --bg light flag or bg = "light" in cat_gui.toml replaces all background surfaces with #FFECD6 (warm cream), converting the frequency digit amber to dark orange for readability.
Two independent font paths can be set:
--freq-font PATH— used exclusively for the LO A, LO B, and Tune frequency digit displays--gui-font PATH— propagated to all of Tk's named system fonts (TkDefaultFont,TkTextFont, etc.) so every widget picks it up automatically
Font loading (TTF/OTF) works without admin rights on Linux, macOS, and Windows:
- Linux: copies the font to
~/.local/share/fonts/, runsfc-cache, then callsFcConfigAppFontAddFile()on the live in-process fontconfig handle so Tk sees the family immediately - macOS: copies to
~/Library/Fonts/, then callsCTFontManagerRegisterFontsForURLscoped to the current process - Windows: calls
AddFontResourceExWwithFR_PRIVATE | FR_NOT_ENUM(no admin rights required)
The PostScript family name is resolved by fonttools (preferred), then fc-query, then a filename-stem heuristic.
| File | Created By | Content |
|---|---|---|
cat_gui.toml |
GUI on first run | GUI display, connection, audio settings |
cat_server.toml.example |
Server on first run (if cat_server.toml is absent) |
Annotated template — copy to cat_server.toml to customise |
cat_device.toml.example |
Server on first run (if cat_device.toml is absent) |
Annotated template — copy to cat_device.toml to customise |
<device>.gui_state.json |
Operator (must create manually) | Persisted per-device operator settings; server saves into it but never creates it |
<device>.memories.json |
Server on first memory save | Per-device 3×20 frequency memories |
<device>.gui_state.json.example |
Server on first run | Example gui_state file for reference / starting point |
<device>.memories.json.example |
Server on first run | Example memories file for reference / starting point |
Config file creation: When
cat_server.tomlorcat_device.tomlis absent the server writes a<name>.toml.examplecompanion file and runs on built-in defaults for that session. The actual.tomlfile is never auto-created; the operator must copy or rename the.examplefile to activate custom configuration.
gui_state files: The server saves into
<device>.gui_state.jsonbut will never create it if it does not exist. An.exampleversion is written on first run for reference. The operator must create the real file (e.g. by copying the example) before per-device state will be persisted across restarts.
All .toml files are self-healing: missing keys are added at their default value and the file is rewritten in place.
The reference server is structured so that the signal-generation layer is easy to replace:
RadioState.apply(cmd)— processes every GUI command. Add new commands here.ClientHandler._stream_loop()— callsRadioState.as_dict()and builds the outgoingdataframe at 10 Hz. Replace the syntheticSignallist with real SDR samples to get a live spectrum.UDPAudioChannel._tx_loop()— sends μ-law RTP frames from eitherAudioWavSource.read_frame()or_gen_sine_frame(). Connect a real SDR demodulator here to deliver actual receive audio.UDPAudioChannel._rx_loop()— receives μ-law RTP from the GUI during PTT. The decoded PCM is currently discarded; route it to your SDR transmit path here.IQWavSource— a complete, standalone IQ WAV reader with looping and FFT output. Wrap a real SDR API (SoapySDR, RTL-SDR Python bindings, etc.) in the same interface (read_block(n)→ complex numpy array) to feed live IQ samples into_iq_fft_spectrum_db().
The JSON protocol is intentionally simple: any language or framework that can open a TCP socket and write newline-terminated JSON can drive the GUI.
cat_gui.py uses a custom JSON-over-TCP protocol (newline-delimited JSON objects) that communicates with a proprietary Python backend (cat_server.py). Hamlib's rigctld uses a text command protocol (single-char or \long_name value\n over TCP port 4532). The two protocols are architecturally different in several key ways:
| Dimension | cat_gui.py Custom Protocol | Hamlib rigctld |
|---|---|---|
| Wire format | JSON objects, newline-delimited | Plain-text tokens, space-separated, \n-terminated |
| Transport | TCP (configurable port) + UDP RTP audio | TCP (default 4532) only |
| Push model | Server pushes spectrum/state continuously | Client polls for each get_* |
| Audio | Integrated RTP/G.711 µ-law audio stream | No audio — radio-only |
| State sync | resp:ok + reload_state push full state |
Client reads each parameter individually |
| Mode tokens | Server-defined arbitrary labels (up to 10 slots) | Fixed set: USB, LSB, CW, FM, AM, RTTY… |
| Memory | JSON object with freq/mode/label | rigmem utility; CAT-command channel |
Commands sent from the GUI to the server.
| # | cat_gui.py cmd |
Parameters | Hamlib Equivalent | Hamlib Short/Long | Status |
|---|---|---|---|---|---|
| 1 | hello |
— | (handshake — no direct equivalent) | \dump_caps (nearest: capability query) |
dump_caps is closest |
| 2 | start |
— | (no equivalent — SDR-specific start/stop streaming) | — | ❌ Missing in Hamlib |
| 3 | stop |
— | (no equivalent) | — | ❌ Missing in Hamlib |
| 4 | set_lo_a_freq |
hz (int) |
set_freq on VFOA |
F / \set_freq |
|
| 5 | set_lo_b_freq |
hz (int) |
set_freq on VFOB |
F / \set_freq (with VFO=VFOB) |
|
| 6 | set_lo |
lo ("A"|"B") |
set_vfo |
V / \set_vfo |
✅ Maps — VFO A/B selection |
| 7 | set_tune_freq |
hz (int) |
(no direct equivalent — IF/tune offset concept) | RIT/XIT J/Z (partial) |
|
| 8 | set_mode |
mode (string) |
set_mode |
M / \set_mode |
|
| 9 | set_filter |
lo, hi (Hz) |
set_mode passband |
M / \set_mode (2nd arg) |
|
| 10 | set_ptt |
enabled (bool), udp_port (int) |
set_ptt |
T / \set_ptt |
|
| 11 | set_split |
enabled (bool) |
set_split_vfo |
S / \set_split_vfo |
|
| 12 | set_volume |
value (float 0–100) |
set_level AF |
L AF / \set_level AF |
✅ Maps — AF level |
| 13 | set_rf_gain |
value (float dB) |
set_level RF |
L RF / \set_level RF |
✅ Maps — RF gain level |
| 14 | set_squelch |
value (float dBm) |
set_level SQL |
L SQL / \set_level SQL |
✅ Maps — Squelch level |
| 15 | set_agc_thresh |
value (float dB) |
set_level AGC |
L AGC / \set_level AGC |
|
| 16 | set_zoom |
value (int) |
(no equivalent — SDR spectrum zoom) | — | ❌ Missing in Hamlib |
| 17 | set_selected_bw |
value (int Hz) |
set_mode passband |
M / \set_mode |
|
| 18 | set_spec_ref |
box, value (dB) |
(no equivalent — display parameter) | — | ❌ Missing in Hamlib |
| 19 | set_spec_ave |
box, value (int) |
(no equivalent — display parameter) | — | ❌ Missing in Hamlib |
| 20 | ui_display |
box, view |
(no equivalent — GUI-layer command) | — | ❌ Missing in Hamlib |
| 21 | transport |
action (rec/play/pause/stop/rw/ff/infinite) |
(no equivalent — media transport) | — | ❌ Missing in Hamlib |
| 22 | get_devices |
— | (no equivalent — device enumeration) | \dump_caps (partial) |
❌ Missing in Hamlib |
| 23 | select_device |
index (int) |
(no equivalent — device switching) | -m model CLI only |
❌ Missing in Hamlib (CLI flag, not runtime command) |
| 24 | get_sample_rates |
— | (no equivalent — SDR-specific) | — | ❌ Missing in Hamlib |
| 25 | set_sample_rate |
value (int Hz) |
(no equivalent — SDR-specific) | — | ❌ Missing in Hamlib |
| 26 | get_antennas |
— | get_ant |
y / \get_ant |
|
| 27 | select_antenna |
index (int, 1-based) |
set_ant |
Y / \set_ant |
✅ Maps — antenna selection by index |
| 28 | get_power_levels |
— | get_level RFPOWER |
l RFPOWER / \get_level RFPOWER |
|
| 29 | set_power |
index (int) |
set_level RFPOWER |
L RFPOWER / \set_level RFPOWER |
|
| 30 | user_button |
index, optional enabled / choice |
set_func |
U / \set_func |
|
| 31 | user_text |
index, text (string) |
send_morse (b) — closest analog |
b / \send_morse |
|
| 32 | rf_usr_button |
index, optional enabled |
set_func |
U / \set_func |
user_button — Hamlib has no RF-domain custom button concept |
| 33 | rf_usr_btn_config_set |
index, values (dict) |
(no equivalent) | — | ❌ Missing in Hamlib |
| 34 | get_memories |
position (int) |
get_mem (channel memory read) |
\get_mem |
|
| 35 | save_memory |
position, freq, mode, label |
set_mem / set_channel |
\set_mem / \set_channel |
|
| 36 | audio_hello |
udp_port (int) |
(no equivalent — RTP audio channel setup) | — | ❌ Missing in Hamlib |
Messages received by the GUI from the server and processed in _handle().
| # | cat_gui.py type |
Key Payload Fields | Hamlib Equivalent | Status |
|---|---|---|---|---|
| 1 | data |
f_start, f_stop, spectrum[], smeter_dbm, smeter_text, swr |
(no equivalent — continuous SDR push) | ❌ No Hamlib analog — Hamlib uses get_level STRENGTH polling; no spectrum push |
| 2 | af_local |
af_spectrum[], af_range |
(no equivalent — locally computed AF FFT) | ❌ No Hamlib analog |
| 3 | reload_state |
Full state dict (all parameters) | (no equivalent — full state push) | ❌ No Hamlib analog — Hamlib requires individual get_* polls |
| 4 | resp:ok (via "state" key) |
Merged state dict | RPRT 0 (success acknowledgment only) |
RPRT 0; cat_gui piggybacks full state on acknowledgment |
| 5 | audio_port |
port (UDP int), sample_rate, frame_ms |
(no equivalent) | ❌ Missing in Hamlib |
| 6 | disconnected |
optional reason |
RPRT -1 (error response) |
|
| 7 | device_list |
devices[] |
(no equivalent) | ❌ Missing in Hamlib |
| 8 | sample_rate_list |
rates[], current |
(no equivalent) | ❌ Missing in Hamlib |
| 9 | antenna_list |
antennas[], current, device_allowed_bands |
get_ant response (scalar only) |
|
| 10 | power_level_list |
levels[], current |
get_level RFPOWER (scalar) |
|
| 11 | memory_list |
memories[] |
get_mem (one channel) |
|
| 12 | user_text |
index, text |
(no equivalent) | ❌ Missing in Hamlib |
Key-value pairs stored in self.state (populated from server-pushed JSON).
| # | cat_gui.py State Key | Type | Description | Hamlib Equivalent | Hamlib Command | Status |
|---|---|---|---|---|---|---|
| 1 | lo_freq |
int Hz | LO A (main VFO) frequency | Frequency VFOA | f / \get_freq |
✅ Maps |
| 2 | lo_b_freq |
int Hz | LO B (second VFO) frequency | Frequency VFOB | f (with VFO=VFOB) |
✅ Maps |
| 3 | tune_freq |
int Hz | IF/tune frequency (absolute) | RIT (j) or XIT (z) — relative only |
||
| 4 | lo_active |
"A"|"B" | Which LO/VFO is active | Current VFO | v / \get_vfo |
✅ Maps |
| 5 | mode |
string | Current modulation mode label | Mode token | m / \get_mode |
|
| 6 | filter_lo / filter_hi |
int Hz | IF passband lower/upper edges | Passband (single width int) | m / \get_mode (2nd value) |
|
| 7 | ptt |
bool | Push-to-talk state | PTT state | t / \get_ptt |
✅ Maps |
| 8 | split |
bool | Split TX/RX enabled | Split VFO | s / \get_split_vfo |
✅ Maps |
| 9 | volume |
float 0–100 | Audio output volume | AF level | l AF / \get_level AF |
✅ Maps |
| 10 | rf_gain |
float dB | RF gain | RF level | l RF / \get_level RF |
✅ Maps |
| 11 | squelch |
float dBm | Squelch threshold | SQL level | l SQL / \get_level SQL |
✅ Maps |
| 12 | agc_thresh |
float dB | AGC threshold | AGC level | l AGC / \get_level AGC |
|
| 13 | sample_rate |
int Hz | SDR sample rate | (no equivalent) | — | ❌ Missing in Hamlib |
| 14 | zoom |
int | Spectrum zoom level | (no equivalent) | — | ❌ Missing in Hamlib |
| 15 | running |
bool | SDR streaming active | (no equivalent) | — | ❌ Missing in Hamlib |
| 16 | user_mod_labels |
list[str] | Server-defined mode button labels | Mode tokens | m / \get_mode |
|
| 17 | user_mod_types |
list[str] | Mode type (normal/text/text_input) | (no equivalent) | — | ❌ Missing in Hamlib |
| 18 | user_buttons |
list[dict] | Server-defined auxiliary buttons | set_func / get_func tokens |
U / u |
|
| 19 | user_btn_state |
list[bool] | Toggle state per user button | get_func |
u / \get_func |
|
| 20 | user_btn_list_sel |
list[int] | List-type button selections | (no equivalent) | — | ❌ Missing in Hamlib |
| 21 | rf_usr_btns |
list[dict] | RF domain user buttons | set_func / get_func |
U / u |
|
| 22 | rf_usr_btn_state |
list[bool] | RF user button toggle states | get_func |
u |
|
| 23 | rf_usr_btn_config_vals |
dict | Per-button config parameter values | (no equivalent) | — | ❌ Missing in Hamlib |
| 24 | antenna_index |
int (1-based) | Currently selected antenna | Antenna number | y / \get_ant |
✅ Maps |
| 25 | antenna_labels |
list[str] | Named antenna port labels | (not in Hamlib protocol) | — | ❌ Missing in Hamlib |
| 26 | antenna_allowed_bands |
list[list[str]] | Per-antenna band restrictions | (no equivalent) | — | ❌ Missing in Hamlib |
| 27 | allowed_bands |
list[str] | Active allowed amateur bands | (no equivalent) | — | ❌ Missing in Hamlib |
| 28 | power_index |
int | Selected TX power preset index | RFPOWER level (float 0–1) | l RFPOWER |
|
| 29 | power_levels |
list | Named TX power presets | (no equivalent as list) | — | ❌ Missing in Hamlib |
| 30 | selected_bw |
int Hz | BW selector (step size) | Passband | m / \get_mode |
|
| 31 | bandwidth_map |
dict | BW preset map | (no equivalent) | — | ❌ Missing in Hamlib |
| 32 | active_device_index |
int | Currently active SDR device | (no equivalent — SDR-specific) | — | ❌ Missing in Hamlib |
| 33 | spec_ref_rf / spec_ref_af |
float dB | Spectrum reference level | (no equivalent) | — | ❌ Missing in Hamlib |
| 34 | spec_ave_rf / spec_ave_af |
int | Spectrum averager count | (no equivalent) | — | ❌ Missing in Hamlib |
| 35 | toolbar_view_rf / toolbar_view_af |
string | Display mode (Waterfall/Spectrum) | (no equivalent) | — | ❌ Missing in Hamlib |
Standard Hamlib/rigctld commands that have no counterpart in the cat_gui.py protocol.
| # | Hamlib Command | Short | Description | Notes |
|---|---|---|---|---|
| 1 | set_rit / get_rit |
J / j |
Receiver Incremental Tuning (Hz offset) | cat_gui uses absolute tune_freq; RIT would require conversion |
| 2 | set_xit / get_xit |
Z / z |
Transmitter Incremental Tuning | Same as RIT note |
| 3 | set_ts / get_ts |
N / n |
Tuning step size in Hz | No step concept in cat_gui (uses custom BW step) |
| 4 | set_rptr_shift / get_rptr_shift |
R / r |
Repeater shift (FM operations) | Not relevant for SDR-focused app |
| 5 | set_rptr_offs / get_rptr_offs |
O / o |
Repeater offset in Hz | Same as above |
| 6 | set_ctcss_tone / get_ctcss_tone |
C / c |
CTCSS tone (tenths of Hz) | FM sub-tone — not supported |
| 7 | set_dcs_code / get_dcs_code |
D / d |
DCS code | Not supported |
| 8 | set_ctcss_sql / get_ctcss_sql |
0x90 / 0x91 |
CTCSS squelch tone | Not supported |
| 9 | set_func NB / get_func NB |
U NB / u NB |
Noise Blanker on/off | cat_gui has no NB toggle in visible protocol (may be in user_button) |
| 10 | set_func COMP |
U COMP |
Speech compressor | Not supported |
| 11 | set_func VOX |
U VOX |
Voice-operated TX | Not supported |
| 12 | set_func TONE / TSQL |
U TONE |
CTCSS tone squelch | Not supported |
| 13 | set_func LOCK |
U LOCK |
VFO lock | cat_gui implements frequency lock client-side only (no server cmd) |
| 14 | set_func AFC |
U AFC |
Automatic Frequency Control | May be in user_button; not in explicit protocol |
| 15 | set_func ANF |
U ANF |
Automatic Notch Filter | May be in user_button |
| 16 | set_func NR |
U NR |
Noise Reduction | May be in user_button |
| 17 | set_level PREAMP |
L PREAMP |
Preamplifier level | Not in explicit protocol |
| 18 | set_level ATT |
L ATT |
Attenuator setting | Not in explicit protocol |
| 19 | set_level MICGAIN |
L MICGAIN |
Microphone gain | Not in explicit protocol |
| 20 | set_level KEYSPD |
L KEYSPD |
CW keyer speed (WPM) | Not supported |
| 21 | set_level NOTCHF |
L NOTCHF |
Manual notch frequency | May be in user_button/config |
| 22 | send_morse |
b |
Send Morse code string | Not supported |
| 23 | get_dcd |
0x8b |
Data Carrier Detect / squelch open | Not in explicit protocol |
| 24 | dump_caps |
\dump_caps |
Dump all rig capabilities | Partially covered by get_devices + reload_state |
| 25 | set_lock_mode |
(rigctld-only) | Prevent mode changes from other clients | Not needed (single-client design) |
cat_gui.py custom protocol Hamlib rigctld
───────────────────────────────── ──────────────────────────────────
JSON {cmd, ...} → server \command value\n → rigctld
server pushes spectrum/state client polls each parameter
Integrated RTP UDP audio No audio channel
Session handshake (hello) Stateless command/response
Dynamic mode/button labels Fixed mode/func token set
SDR concepts (LO, zoom, sample_rate) Rig concepts (VFO, RIT, CTCSS)
Device switching at runtime Model selected at startup (-m)
Band restriction (allowed_bands) No band restriction in protocol
Per-device persistent config Backend config only at start (--set-conf)
| Category | Total cat_gui items | ✅ Direct Map | ❌ No Hamlib Equivalent | |
|---|---|---|---|---|
| Outbound Commands | 36 | 4 (11%) | 16 (44%) | 16 (45%) |
| Inbound Messages | 12 | 0 (0%) | 5 (42%) | 7 (58%) |
| State Variables | 35 | 8 (23%) | 9 (26%) | 18 (51%) |
| Totals | 83 | 12 (14%) | 30 (36%) | 41 (49%) |
[bandwidth]validation: The server performs a fatal check (sys.exit(1)) at startup if any label defined in[user_mods]lacks a matching entry in the[bandwidth]section of the active device config. Always add a[bandwidth]entry for every custom modulation mode you define, or the server will refuse to start.