# trx-rs Code Design & Architecture Overview ## Table of Contents 1. [Project Purpose](#project-purpose) 2. [Technology Stack](#technology-stack) 3. [High-Level Architecture](#high-level-architecture) 4. [Crate Layout](#crate-layout) 5. [Core Library (trx-core)](#core-library-trx-core) 6. [Protocol Layer (trx-protocol)](#protocol-layer-trx-protocol) 7. [Server (trx-server)](#server-trx-server) 8. [Backend Abstraction (trx-backend)](#backend-abstraction-trx-backend) 9. [Client (trx-client)](#client-trx-client) 10. [Frontend System (trx-frontend)](#frontend-system-trx-frontend) 11. [Signal Decoders](#signal-decoders) 12. [DSP & Spectrum Pipeline](#dsp--spectrum-pipeline) 13. [Plugin System](#plugin-system) 14. [Configuration](#configuration) 15. [Concurrency Model](#concurrency-model) 16. [Authentication & Security](#authentication--security) 17. [Data Flow Diagrams](#data-flow-diagrams) --- ## Project Purpose **trx-rs** is a modular amateur radio transceiver control daemon written in Rust. It separates radio hardware access (server) from user-facing control interfaces (client), enabling: - **Remote control** of transceivers over TCP networks - **Multi-rig operation** with per-rig isolation and routing - **SDR integration** with real-time DSP (demodulation, spectrum, decode) - **Pluggable backends** for different radio hardware - **Multiple frontends** — web UI, Hamlib-compatible rigctl, JSON-over-TCP - **Signal decoding** — APRS, CW, FT8, WSPR, RDS — with live streaming and logging - **Uplinks** — PSKReporter, APRS-IS IGate Target users are amateur radio operators who want networked, automated, or multi-radio control from a single host or across a LAN. --- ## Technology Stack | Layer | Technology | |-------|-----------| | Language | Rust (2021 edition) | | Async runtime | Tokio | | Web framework | Actix-web (HTTP frontend) | | Serialization | Serde / JSON | | Config format | TOML | | Audio codec | Opus | | SDR interface | soapysdr crate (wraps SoapySDR C library) | | CAT serial | tokio-serial | | CLI | clap | | Logging | tracing / tracing-subscriber | | FT8 decode | ft8_lib (external C library via FFI) | --- ## High-Level Architecture ``` ┌──────────────────────────────────────────────────────────┐ │ trx-server │ │ │ │ Radio Hardware (serial/TCP) │ │ ↕ CAT protocol │ │ Rig Backend ──────── rig_task.rs ─── listener.rs │ │ (ft817/ft450d/sdr) (state machine) (JSON TCP :4530) │ │ │ │ │ audio.rs │ │ (Opus :4531) │ │ │ │ │ Decoders │ │ (APRS, CW, FT8, WSPR, RDS) │ │ PSKReporter / APRS-IS │ └──────────────────────────────────────────────────────────┘ ↕ JSON TCP (port 4530) ↕ Opus audio TCP (port 4531) ┌──────────────────────────────────────────────────────────┐ │ trx-client │ │ │ │ remote_client.rs (polls state, routes commands) │ │ ↕ internal mpsc/watch channels │ │ Frontends: │ │ trx-frontend-http (Web UI :8080) │ │ trx-frontend-rigctl (rigctl :4532) │ │ trx-frontend-http-json (JSON/TCP ephemeral) │ └──────────────────────────────────────────────────────────┘ ↕ Browser / Hamlib / Custom tools End Users ``` The server and client are separate binaries. They communicate over **JSON-over-TCP** (control) and **Opus-encoded TCP** (audio). Both binaries can load shared-library plugins at startup. --- ## Crate Layout ``` trx-rs/ # Workspace root ├── Cargo.toml # Workspace manifest (shared dependencies) ├── CLAUDE.md # Contributor notes │ └── src/ ├── trx-core/ # Core types, traits, state machine ├── trx-protocol/ # Client↔server message types, auth, codec ├── trx-app/ # Shared app helpers (config loading, plugins, logging) │ ├── trx-server/ # Server binary │ ├── src/ │ │ ├── main.rs │ │ ├── config.rs │ │ ├── rig_task.rs # Per-rig polling loop │ │ ├── listener.rs # JSON TCP server (:4530) │ │ ├── audio.rs # Opus audio server (:4531) │ │ ├── pskreporter.rs # PSKReporter uplink │ │ └── aprsfi.rs # APRS-IS IGate uplink │ │ │ └── trx-backend/ # Backend abstraction + factory │ ├── src/lib.rs # RegistrationContext, RigAccess enum │ ├── trx-backend-ft817/ # Yaesu FT-817 CAT │ ├── trx-backend-ft450d/ # Yaesu FT-450D CAT │ └── trx-backend-soapysdr/ # SoapySDR SDR (RX-only) │ ├── src/ │ │ ├── lib.rs # SoapySdrRig impl │ │ ├── real_iq_source.rs │ │ ├── dsp/ # DSP pipeline, FIR, oscillator, AGC │ │ ├── demod/ # AM, FM, WFM, SSB, CW demodulators │ │ └── spectrum.rs # FFT spectrum generation │ ├── trx-client/ # Client binary │ ├── src/ │ │ ├── main.rs │ │ ├── config.rs │ │ ├── remote_client.rs # TCP connection to server │ │ └── audio_client.rs # Audio stream handler │ │ │ └── trx-frontend/ # Frontend abstraction + registration │ ├── src/lib.rs # FrontendSpawner trait, FrontendRuntimeContext │ ├── trx-frontend-http/ # Actix-web: REST + SSE + WebSocket │ ├── trx-frontend-http-json/ # JSON-over-TCP thin control frontend │ └── trx-frontend-rigctl/ # Hamlib-compatible rigctl TCP (:4532) │ └── decoders/ ├── trx-aprs/ # APRS packet decoder ├── trx-cw/ # CW / Morse decoder ├── trx-ft8/ # FT8 decoder (wraps ft8_lib C library) ├── trx-wspr/ # WSPR beacon decoder ├── trx-rds/ # FM RDS decoder └── trx-decode-log/ # JSON Lines log rotation for decoded frames ``` --- ## Core Library (trx-core) **Path:** `src/trx-core/src/` The foundation of the system. All other crates depend on trx-core for shared types and traits. ### Key Re-exports (`lib.rs`) ```rust pub use rig::command::RigCommand; pub use rig::request::RigRequest; pub use rig::response::{RigError, RigResult}; pub use rig::state::{RigMode, RigSnapshot, RigState, RigFilterState, SpectrumData}; pub use rig::AudioSource; pub use decode::DecodedMessage; pub use audio::AudioStreamInfo; ``` ### Rig State (`rig/state.rs`) The `RigState` struct is the canonical snapshot of a rig at any point in time: ```rust pub struct RigState { pub rig_info: Option, pub status: RigStatus, pub initialized: bool, pub control: RigControl, pub server_callsign: Option, pub spectrum: Option, // FFT frame from SDR pub filter: Option, // Runtime DSP parameters // ... decoder enable flags, CW params, etc. } pub struct RigStatus { pub freq: Freq, pub mode: RigMode, pub tx_en: bool, pub vfo: Option, pub tx: Option, // power, SWR, ALC pub rx: Option, // signal strength pub lock: Option, } pub enum RigMode { LSB, USB, CW, CWR, AM, WFM, FM, DIG, PKT, Other(String) } ``` ### Rig Commands (`rig/command.rs`) All control actions are represented as enum variants: ```rust pub enum RigCommand { // Basic control GetSnapshot, SetFreq(Freq), SetMode(RigMode), SetPtt(bool), PowerOn, PowerOff, ToggleVfo, Lock, Unlock, // TX GetTxLimit, SetTxLimit(u8), // Decoders SetAprsDecodeEnabled(bool), SetCwDecodeEnabled(bool), SetFt8DecodeEnabled(bool), SetWsprDecodeEnabled(bool), ResetAprsDecoder, ResetCwDecoder, ResetFt8Decoder, ResetWsprDecoder, // CW keyer SetCwAuto(bool), SetCwWpm(u32), SetCwToneHz(u32), // SDR DSP SetBandwidth(u32), SetFirTaps(u32), SetSdrGain(f64), SetCenterFreq(Freq), GetSpectrum, // WFM SetWfmDeemphasis(u32), SetWfmStereo(bool), SetWfmDenoise(bool), } ``` ### State Machine (`rig/controller/machine.rs`) Manages the lifecycle of a rig connection: ``` Disconnected → Connecting → Initializing → PoweredOff ↘ Ready ⇄ Transmitting ↓ Error ↓ (recoverable) Connecting ``` ```rust pub enum RigMachineState { Disconnected, Connecting, Initializing, PoweredOff, Ready, Transmitting, Error(RigStateError), } ``` Transitions are triggered by `RigEvent` (Connected, PoweredOn, PttOn, Error, etc.) and processed by `process_event(&mut self, event: RigEvent)`. ### Command Handlers (`rig/controller/handlers.rs`) Each command implements `RigCommandHandler`: ```rust pub trait RigCommandHandler: Debug + Send + Sync { fn name(&self) -> &'static str; fn can_execute(&self, ctx: &dyn CommandContext) -> ValidationResult; fn execute<'a>( &'a self, executor: &'a mut dyn CommandExecutor ) -> Pin> + Send + 'a>>; } pub enum ValidationResult { Ok, InvalidState(String), // Wrong machine state InvalidParams(String), // Bad parameters Locked, // Rig is locked } ``` ### Event System (`rig/controller/events.rs`) Observers subscribe via the `RigListener` trait. `RigEventEmitter` maintains a list of `Arc` and calls them on state changes. ```rust pub trait RigListener: Send + Sync { fn on_frequency_change(&self, old: Option, new: Freq) {} fn on_mode_change(&self, old: Option<&RigMode>, new: &RigMode) {} fn on_ptt_change(&self, transmitting: bool) {} fn on_state_change(&self, old: &RigMachineState, new: &RigMachineState) {} fn on_meter_update(&self, rx: Option<&RigRxStatus>, tx: Option<&RigTxStatus>) {} fn on_lock_change(&self, locked: bool) {} fn on_power_change(&self, powered: bool) {} } ``` ### Operational Policies (`rig/controller/policies.rs`) Govern reconnection and polling behaviour: ```rust pub trait RetryPolicy: Send { fn next_delay(&mut self) -> Duration; } pub struct ExponentialBackoff { initial_delay: Duration, max_delay: Duration, multiplier: f64, current_delay: Duration, } pub trait PollingPolicy: Send { fn next_interval(&mut self) -> Duration; } pub struct AdaptivePolling { idle_interval: Duration, tx_interval: Duration, // faster polling during TX } ``` ### Audio Wire Format (`audio.rs`) ``` [ 1 byte type ][ 4 bytes BE length ][ N bytes payload ] Types: 0x00 AudioStreamInfo (sample rate, channels, frame duration) 0x01 RX audio frame (Opus-encoded PCM) 0x02 TX audio frame (Opus-encoded PCM) 0x03 APRS decode 0x04 CW decode 0x05 FT8 decode 0x06 WSPR decode ``` ### Error Types (`rig/response.rs`) ```rust pub struct RigError { pub message: String, pub kind: RigErrorKind, } pub enum RigErrorKind { Transient, // Retry-able (timeout, busy) Permanent, // Don't retry (unsupported operation) } pub type RigResult = Result; pub type DynResult = Result>; ``` --- ## Protocol Layer (trx-protocol) **Path:** `src/trx-protocol/src/` Bridges the internal `RigCommand`/`RigState` world to JSON messages exchanged over TCP. ### Message Types (`types.rs`) ```rust // Client → Server pub struct ClientEnvelope { pub token: Option, // Auth token pub rig_id: Option, // Multi-rig routing (None = default rig) pub cmd: ClientCommand, } pub enum ClientCommand { GetState, GetRigs, SetFreq { freq_hz: u64 }, SetCenterFreq { freq_hz: u64 }, SetMode { mode: String }, SetPtt { ptt: bool }, PowerOn, PowerOff, ToggleVfo, Lock, Unlock, GetTxLimit, SetTxLimit { limit: u8 }, SetBandwidth { bandwidth_hz: u32 }, SetFirTaps { taps: u32 }, SetSdrGain { gain_db: f64 }, SetWfmDeemphasis { deemphasis_us: u32 }, SetWfmStereo { enabled: bool }, SetWfmDenoise { enabled: bool }, SetAprsDecodeEnabled { enabled: bool }, /* ... other decoders ... */ GetSpectrum, // ... } // Server → Client pub struct ClientResponse { pub success: bool, pub rig_id: Option, pub state: Option, // Updated rig state pub rigs: Option>, // Response to GetRigs pub error: Option, } pub struct RigEntry { pub rig_id: String, pub display_name: Option, pub state: RigSnapshot, pub audio_port: Option, } ``` ### Type Mapping (`mapping.rs`) `client_command_to_rig(ClientCommand) → RigCommand` and the reverse conversion ensure the protocol types stay decoupled from the core domain model. ### Authentication (`auth.rs`) ```rust pub trait TokenValidator: Send + Sync { fn validate(&self, token: &str) -> bool; } pub struct SimpleTokenValidator { tokens: HashSet } pub struct NoAuthValidator; // Always returns true (debug/local use) ``` --- ## Server (trx-server) **Path:** `src/trx-server/src/` ### Startup Sequence 1. Parse CLI / TOML config (`config.rs`) 2. Register backends via `RegistrationContext` (built-ins + plugins) 3. For each configured rig: - Build or pre-configure the rig backend - Spawn `run_rig_task()` as a Tokio task 4. Spawn `run_listener()` (JSON TCP on `:4530`) 5. Spawn audio streaming server (`:4531`) 6. Wait for shutdown signal ### Multi-Rig Routing Rigs are stored in `Arc>`. Each `RigHandle` contains: - `mpsc::Sender` — send commands to the rig task - `watch::Receiver` — read latest state `listener.rs` routes incoming `ClientEnvelope.rig_id` to the correct handle. If `rig_id` is absent, the server's default rig is used. Auto-generated IDs follow the pattern `{model}_{index}` (e.g., `ft817_0`, `soapysdr_1`) when not explicitly set in config. ### Rig Task (`rig_task.rs`) Each rig runs an independent async loop: ``` connect → initialize → poll loop ↓ on error retry with ExponentialBackoff ↓ on persistent error Error state → wait for recovery ``` The task: - Drives the `RigStateMachine` through state transitions - Polls rig status at `AdaptivePolling` intervals (faster during TX) - Handles incoming `RigCommand`s from `mpsc::Receiver` - Broadcasts `RigState` snapshots via `watch::Sender` ### JSON TCP Listener (`listener.rs`) Accepts connections on port 4530. Per connection: 1. Read newline-delimited JSON (`ClientEnvelope`) 2. Validate token 3. Route to rig by `rig_id` 4. Convert `ClientCommand → RigCommand` and send to rig task 5. Await result and return `ClientResponse` ### Audio Server (`audio.rs`) Separate TCP listener on port 4531. Per connection: 1. Send `AudioStreamInfo` header 2. Send buffered decoder history (APRS, CW, FT8, WSPR, RDS frames) 3. Stream Opus-encoded RX audio frames as they arrive 4. Interleave decoder messages (`0x03`–`0x06` frame types) `DecoderHistories` maintains ring buffers of recent decoded events so late-connecting clients get context. ### Uplinks | Module | Purpose | |--------|---------| | `pskreporter.rs` | Posts FT8/WSPR spots to pskreporter.net | | `aprsfi.rs` | Forwards APRS packets to APRS-IS network (IGate) | Both are optional, configured per-rig. --- ## Backend Abstraction (trx-backend) **Path:** `src/trx-server/trx-backend/` ### Factory Pattern (`src/lib.rs`) ```rust pub enum RigAccess { Serial { path: String, baud: u32 }, Tcp { addr: String }, Sdr { args: String }, } type BackendFactory = fn(RigAccess) -> DynResult>; pub struct RegistrationContext { factories: HashMap, } impl RegistrationContext { pub fn register_backend(&mut self, name: &str, factory: BackendFactory); pub fn build_rig(&self, name: &str, access: RigAccess) -> DynResult>; } ``` Built-in registrations (via `register_builtin_backends_on`): - `"ft817"` → `Ft817::new` - `"ft450d"` → `Ft450d::new` - `"soapysdr"` → `SoapySdrRig::new_with_config` (if `soapysdr` feature enabled) ### RigCat Trait (from trx-core) All backends implement `RigCat`: ```rust pub trait RigCat: Rig { async fn get_status(&mut self) -> RigResult; async fn set_freq(&mut self, freq: Freq) -> RigResult<()>; async fn set_mode(&mut self, mode: RigMode) -> RigResult<()>; async fn set_ptt(&mut self, on: bool) -> RigResult<()>; async fn power_on(&mut self) -> RigResult<()>; async fn power_off(&mut self) -> RigResult<()>; async fn toggle_vfo(&mut self) -> RigResult<()>; // ... more operations } ``` ### FT-817 Backend (`trx-backend-ft817/`) - CAT protocol over serial (9600 baud default) - BCD-encoded frequency/mode commands - VFO A/B tracking - Meter reads: S-meter, TX power, SWR, ALC - Bands: 160m through 70cm + GHz receive ### FT-450D Backend (`trx-backend-ft450d/`) - Similar structure to FT-817 - Uses FT-450D-specific CAT command set ### SoapySDR Backend (`trx-backend-soapysdr/`) RX-only SDR backend with real-time DSP: ```rust pub struct SoapySdrRig { freq: Freq, mode: RigMode, pipeline: dsp::SdrPipeline, // Multi-channel DSP bandwidth_hz: u32, fir_taps: u32, spectrum_buf: Arc>>>, center_offset_hz: i64, wfm_deemphasis_us: u32, wfm_stereo: bool, wfm_denoise: bool, gain_db: f64, } ``` **Known limitation:** IQ sample streaming (`real_iq_source.rs:149–157`) is not yet implemented — the IQ source currently returns zero buffers. The soapysdr 0.3 crate lacks streaming APIs; direct `soapysdr-sys` FFI or a crate upgrade would be required. --- ## Client (trx-client) **Path:** `src/trx-client/src/` ### Startup Sequence 1. Parse CLI / TOML config 2. Register frontends via `FrontendRegistrationContext` (built-ins + plugins) 3. Spawn `run_remote_client()` — connects to server, drives `watch::Sender` 4. Spawn enabled frontends (HTTP, rigctl, http-json) 5. Wait for shutdown ### Remote Client (`remote_client.rs`) Maintains the server TCP connection: ```rust pub struct RemoteClientConfig { pub addr: String, pub token: Option, pub selected_rig_id: Arc>>, pub known_rigs: Arc>>, pub poll_interval: Duration, pub spectrum: Arc>, } ``` Workflow: 1. Connect to `addr` (host:4530) 2. Poll `GetState` at configured interval (default 750 ms) 3. Poll `GetSpectrum` at ~40 ms (25 fps) if backend supports it 4. Forward commands from frontends (`mpsc::Receiver`) to server 5. Broadcast received `RigState` to all frontends via `watch::Sender` Multi-rig: `selected_rig_id` can be changed at runtime to switch which rig the client targets. `known_rigs` is populated by periodic `GetRigs` calls. ### Audio Client (`audio_client.rs`) Connects to the audio port (`:4531`) and relays: - Opus-encoded audio frames → local PCM broadcast channel - Decoder messages → frontend display --- ## Frontend System (trx-frontend) **Path:** `src/trx-client/trx-frontend/` ### Abstraction (`src/lib.rs`) ```rust pub trait FrontendSpawner { fn spawn_frontend( state_rx: watch::Receiver, rig_tx: mpsc::Sender, callsign: Option, listen_addr: SocketAddr, context: Arc, ) -> JoinHandle<()>; } pub struct FrontendRuntimeContext { pub rigctl_clients: AtomicUsize, pub rigctl_addr: Option, pub http_clients: AtomicUsize, pub known_rigs: Arc>>, pub selected_rig_id: Arc>>, pub spectrum: Arc>, } ``` ### HTTP Frontend (`trx-frontend-http/`) Built on **Actix-web**, serves a browser-based control panel. **REST Endpoints:** | Method | Path | Description | |--------|------|-------------| | GET | `/status` | Current rig state + frontend metadata | | POST | `/cmd/{command}` | Execute a rig command | | GET | `/events` | SSE stream of state changes | | GET | `/audio` | WebSocket audio stream | | GET | `/favicon.png` | Static asset | `/status` response includes a `FrontendMeta` block: ```rust struct FrontendMeta { http_clients: usize, rigctl_clients: usize, rigctl_addr: Option, active_rig_id: Option, rig_ids: Vec, owner_callsign: Option, show_sdr_gain_control: bool, } ``` **Web UI features:** frequency display/entry, mode selector, PTT indicator, S-meter/TX-power/SWR meters, decoder toggles, decode history, spectrum waterfall (SDR), rig picker (multi-rig). **Modules:** | File | Responsibility | |------|---------------| | `server.rs` | Actix app builder, middleware, CORS | | `api.rs` | REST handler functions | | `audio.rs` | WebSocket ↔ PCM audio bridge | | `auth.rs` | Token or basic-auth middleware | | `status.rs` | State formatting for JSON responses | ### Rigctl Frontend (`trx-frontend-rigctl/`) Hamlib-compatible plaintext TCP interface on port 4532. Allows WSJT-X, JS8Call, and other Hamlib-aware applications to control the rig without modification. ### HTTP-JSON Frontend (`trx-frontend-http-json/`) JSON-over-TCP frontend on an ephemeral (or configured) port. Thin wrapper that passes `ClientCommand`/`ClientResponse` pairs — useful for scripting or automation tools. --- ## Signal Decoders **Path:** `src/decoders/` All decoders run as background Tokio tasks inside `trx-server`. They subscribe to the PCM audio broadcast channel from the active rig and publish decoded messages. | Crate | Decoder | Notes | |-------|---------|-------| | `trx-aprs` | APRS (AX.25) | Forwards to APRS-IS if enabled | | `trx-cw` | CW / Morse | Auto WPM detection | | `trx-ft8` | FT8 | Wraps `external/ft8_lib` C library via FFI; posts to PSKReporter | | `trx-wspr` | WSPR beacons | Posts to PSKReporter | | `trx-rds` | FM RDS | Station name, radiotext, time | | `trx-decode-log` | Logging infrastructure | JSON Lines, date-rotated files | Control commands (e.g., `SetAprsDecodeEnabled(bool)`, `ResetCwDecoder`) are routed through `rig_task.rs` to the active decoder tasks. Decoded events are multiplexed onto the audio stream wire protocol (`0x03–0x06` frame types) and also buffered in `DecoderHistories` for replay to newly connected clients. --- ## DSP & Spectrum Pipeline **Path:** `src/trx-server/trx-backend/trx-backend-soapysdr/src/` ### Architecture ``` IQ Samples (from SoapySDR device) ↓ SdrPipeline (per-channel) ├── Channel 0: Mixer → FIR Filter → Demod → AGC → PCM ├── Channel 1: Mixer → FIR Filter → Demod → AGC → PCM └── ... ↓ Audio broadcast channel (Vec) ↓ Decoders / Audio server ``` ### Demodulators (`demod/`) | Module | Mode | |--------|------| | `am.rs` | AM (envelope detection) | | `fm.rs` | Narrowband FM | | `wfm.rs` | Wideband FM (stereo + deemphasis + denoise) | | `ssb.rs` | LSB and USB | | `cw.rs` | CW (Morse, beat-frequency oscillator) | WFM demodulator supports: - Stereo pilot detection and L+R/L−R matrix decoding - Configurable de-emphasis time constant (50 µs EU / 75 µs US) - Optional noise reduction ### Spectrum (`spectrum.rs`) Real-time FFT of the mixer output is stored in `spectrum_buf` and snapshotted on demand: ```rust pub struct SpectrumData { pub magnitudes: Vec, // FFT magnitude bins (linear) pub low_hz: f64, pub high_hz: f64, pub center_hz: f64, } ``` Clients poll via `RigCommand::GetSpectrum` → `ClientCommand::GetSpectrum`. The remote client polls at ~25 fps and caches in `SharedSpectrum`. The HTTP frontend reads this cache to drive the waterfall display. --- ## Plugin System **Path:** `src/trx-app/src/plugins.rs` Both `trx-server` and `trx-client` support dynamic plugins loaded at startup. ### Search Paths (in order) 1. `./plugins/` 2. `~/.config/trx-rs/plugins/` 3. Directories in `TRX_PLUGIN_DIRS` environment variable (`:` on Unix, `;` on Windows) ### Backend Plugins Export symbol: `trx_register_backend(context: *mut RegistrationContext)` Plugins call `context.register_backend("my-rig", factory_fn)` to add new rig drivers without rebuilding the server binary. ### Frontend Plugins Export symbol: `trx_register_frontend(context: *mut FrontendRegistrationContext)` Plugins call `context.register_frontend("my-ui", spawner_fn)` to add new control interfaces. An example plugin is provided at `examples/trx-plugin-example/` (not a workspace member). --- ## Configuration **Format:** TOML. Generated with `--print-config` flag. **Search order:** 1. `--config ` CLI argument 2. `./trx-server.toml` / `./trx-client.toml` 3. `~/.config/trx-rs/trx-server.toml` 4. `/etc/trx-rs/trx-server.toml` ### Server Config Structure ```toml [general] callsign = "W5XYZ" log_level = "info" latitude = 35.5 longitude = -97.5 [listen] addr = "127.0.0.1" port = 4530 audio_port = 4531 [rig] # Legacy single-rig flat config model = "ft817" [rig.access] type = "serial" path = "/dev/ttyUSB0" baud = 9600 [behavior] max_retries = 3 retry_delay_secs = 1 polling_interval_ms = 250 [audio] sample_rate = 48000 frame_duration_ms = 20 dev = "" # CPAL device name (empty = default) [sdr] # SoapySDR global params args = "driver=rtlsdr" sample_rate = 2000000 bandwidth_hz = 2000000 gain_mode = "manual" gain_db = 25.0 center_offset_hz = 0 [[sdr.channels]] if_hz = 0 mode = "USB" audio_bandwidth_hz = 2800 fir_taps = 64 [pskreporter] enabled = true callsign = "W5XYZ" gridsquare = "EM13AH" [aprsfi] enabled = true callsign = "W5XYZ-11" [decode_logs] enabled = true dir = "~/.trx-rs/decode-logs" # Multi-rig (takes priority over flat [rig] section) [[rigs]] id = "ft817_0" name = "HF Transceiver" [rigs.rig] model = "ft817" [rigs.rig.access] type = "serial" path = "/dev/ttyUSB0" baud = 9600 [[rigs]] id = "sdr_0" name = "VHF/UHF SDR" [rigs.rig] model = "soapysdr" [rigs.rig.access] type = "sdr" args = "driver=rtlsdr" ``` ### Client Config Structure ```toml [remote] url = "localhost:4530" rig_id = "" # Empty = server default rig poll_interval_ms = 750 [remote.auth] token = "" [frontends.http] enabled = true listen = "127.0.0.1" port = 8080 [frontends.rigctl] enabled = true listen = "127.0.0.1" port = 4532 [frontends.http_json] enabled = false port = 0 ``` --- ## Concurrency Model The system is built on **Tokio** and uses channels for all cross-task communication: | Channel | Type | Purpose | |---------|------|---------| | `rig_tx` / `rig_rx` | `mpsc` | Frontend → rig task (commands) | | `state_tx` / `state_rx` | `watch` | Rig task → frontends (state updates) | | `audio_tx` / `audio_rx` | `broadcast` | Rig → decoders / audio server (PCM frames) | | `shutdown_tx` / `shutdown_rx` | `watch` | Main → all tasks (graceful shutdown signal) | ### Task Tree (server) ``` main ├── rig_task [per rig] — polls hardware, drives state machine ├── listener — accepts JSON TCP connections │ └── per-connection task — reads commands, sends responses ├── audio_server — accepts audio TCP connections │ └── per-connection task — streams Opus frames ├── decoder tasks — APRS, CW, FT8, WSPR, RDS ├── pskreporter — uplink task └── aprsfi — uplink task ``` ### Task Tree (client) ``` main ├── remote_client — polls server, maintains state_tx ├── audio_client — streams audio from server ├── http_frontend — Actix-web server ├── rigctl_frontend — Hamlib TCP server └── http_json_frontend — JSON-over-TCP server ``` --- ## Authentication & Security ### Token-Based Auth (JSON TCP) - Clients include `token` in every `ClientEnvelope` - Server validates via `TokenValidator` trait - `SimpleTokenValidator` — `HashSet` loaded from config - `NoAuthValidator` — always passes (debug / local-only mode) ### HTTP Frontend Auth - Optional token or HTTP Basic Auth middleware - Configured in `[frontends.http.auth]` - Rate limiting supported ### Transport Security No built-in TLS. For remote use, tunnel over SSH or place behind a TLS-terminating reverse proxy (nginx, Caddy, etc.). --- ## Data Flow Diagrams ### Command Flow (set frequency) ``` Browser → POST /cmd/set_freq?hz=14225000 ↓ trx-frontend-http RigRequest::Command(RigCommand::SetFreq(14225000)) ↓ mpsc channel (rig_tx) remote_client.rs ↓ TCP listener.rs (server) ↓ mpsc channel rig_task.rs → backend.set_freq(14225000) ↓ CAT serial / SoapySDR API Radio hardware ↑ ACK rig_task.rs updates RigState → watch::Sender ↑ TCP remote_client.rs receives ClientResponse ↑ watch::Sender trx-frontend-http sends SSE event to browser ``` ### State Update Flow (polling) ``` rig_task.rs polls rig_status() every ~250 ms → RigState updated → watch::Sender remote_client.rs receives via watch::Receiver → broadcasts to frontends via watch::Sender HTTP frontend reads watch::Receiver → pushes SSE "state" event to connected browsers ``` ### Spectrum Update Flow ``` SoapySdrRig::run_spectrum_snapshot() → FFT of IQ buffer → SpectrumData stored in Arc> remote_client.rs polls GetSpectrum every 40 ms → stores SpectrumData in SharedSpectrum (Arc>) HTTP frontend reads SharedSpectrum → renders waterfall in browser via WebSocket or polling ``` ### Audio Flow ``` SoapySDR IQ → DSP pipeline → PCM (Vec) → broadcast::Sender> ↙ (decoders subscribe) ↘ (audio server subscribes) APRS/CW/FT8/WSPR/RDS Opus encode decode tasks ↓ TCP ↓ audio client (trx-client) DecoderHistories buffer ↓ ↓ broadcast locally listener connections ↓ stream decoder messages HTTP WebSocket / local speakers ``` --- *Generated from source as of commit `56d6d12` (March 2026).*