# trx-rs Architecture

## 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 |
| FTx decode | trx-ftx (pure Rust) |

---

## High-Level Architecture

```mermaid
graph TD
    subgraph server["trx-server"]
        HW["Radio Hardware"] <-->|"CAT protocol<br/>serial / TCP"| Backend["Rig Backend<br/>(ft817 / ft450d / sdr)"]
        Backend --> RigTask["rig_task.rs<br/>(state machine)"]
        RigTask --> Listener["listener.rs<br/>(JSON TCP :4530)"]
        RigTask --> Audio["audio.rs<br/>(Opus :4531)"]
        Audio --> Decoders["Decoders<br/>(APRS, CW, FT8, WSPR, RDS)"]
        Decoders --> Uplinks["PSKReporter / APRS-IS"]
    end

    subgraph client["trx-client"]
        Remote["remote_client.rs<br/>(polls state, routes commands)"]
        Remote <-->|"mpsc / watch channels"| HTTP["trx-frontend-http<br/>(Web UI :8080)"]
        Remote <-->|"mpsc / watch channels"| Rigctl["trx-frontend-rigctl<br/>(rigctl :4532)"]
        Remote <-->|"mpsc / watch channels"| JSON["trx-frontend-http-json<br/>(JSON/TCP)"]
    end

    Listener <-->|"JSON TCP :4530"| Remote
    Audio -->|"Opus TCP :4531"| Remote

    HTTP & Rigctl & JSON <--> Users["End Users<br/>(Browser / Hamlib / Custom tools)"]
```

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)
│
└── 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-ftx/             # Pure Rust FTx decoder (FT8/FT4/FT2)
        ├── 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<RigInfo>,
    pub status: RigStatus,
    pub initialized: bool,
    pub control: RigControl,
    pub server_callsign: Option<String>,
    pub spectrum: Option<SpectrumData>,   // FFT frame from SDR
    pub filter: Option<RigFilterState>,   // 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<RigVfo>,
    pub tx: Option<RigTxStatus>,   // power, SWR, ALC
    pub rx: Option<RigRxStatus>,   // signal strength
    pub lock: Option<bool>,
}

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<Box<dyn Future<Output = DynResult<CommandResult>> + 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<dyn RigListener>` and calls them on state changes.

```rust
pub trait RigListener: Send + Sync {
    fn on_frequency_change(&self, old: Option<Freq>, 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 {
    max_attempts: u32,
    base_delay: Duration,
    max_delay: Duration,
    // Delays include ±25% randomized jitter to prevent thundering herd
}

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<T>  = Result<T, RigError>;
pub type DynResult<T>  = Result<T, Box<dyn std::error::Error + Send + Sync>>;
```

---

## 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<String>,    // Auth token
    pub rig_id: Option<String>,   // 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<String>,
    pub state: Option<RigSnapshot>,   // Updated rig state
    pub rigs: Option<Vec<RigEntry>>,  // Response to GetRigs
    pub error: Option<String>,
}

pub struct RigEntry {
    pub rig_id: String,
    pub display_name: Option<String>,
    pub state: RigSnapshot,
    pub audio_port: Option<u16>,
}
```

### 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<String> }
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<HashMap<String, RigHandle>>`. Each `RigHandle` contains:
- `mpsc::Sender<RigRequest>` — send commands to the rig task
- `watch::Receiver<RigState>` — 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<Box<dyn RigCat>>;

pub struct RegistrationContext {
    factories: HashMap<String, BackendFactory>,
}

impl RegistrationContext {
    pub fn register_backend(&mut self, name: &str, factory: BackendFactory);
    pub fn build_rig(&self, name: &str, access: RigAccess) -> DynResult<Box<dyn RigCat>>;
}
```

Built-in registrations (via `register_builtin_backends_on`):
- `"ft817"` → `Ft817::new`
- `"ft450d"` → `Ft450d::new`
- `"soapysdr"` → `SoapySdrRig::new_from_config(SoapySdrConfig { ... })` (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<RigStatus>;
    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<Mutex<Option<Vec<f32>>>>,
    center_offset_hz: i64,
    wfm_deemphasis_us: u32,
    wfm_stereo: bool,
    wfm_denoise: bool,
    gain_db: f64,
}
```

---

## 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<RigState>`
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<String>,
    pub selected_rig_id: Arc<Mutex<Option<String>>>,
    pub known_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
    pub poll_interval: Duration,
    pub spectrum: Arc<Mutex<SharedSpectrum>>,
}
```

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<RigRequest>`) 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<RigState>,
        rig_tx: mpsc::Sender<RigRequest>,
        callsign: Option<String>,
        listen_addr: SocketAddr,
        context: Arc<FrontendRuntimeContext>,
    ) -> JoinHandle<()>;
}

pub struct FrontendRuntimeContext {
    pub rigctl_clients: AtomicUsize,
    pub rigctl_addr: Option<SocketAddr>,
    pub http_clients: AtomicUsize,
    pub known_rigs: Arc<Mutex<Vec<RemoteRigEntry>>>,
    pub selected_rig_id: Arc<Mutex<Option<String>>>,
    pub spectrum: Arc<Mutex<SharedSpectrum>>,
}
```

### 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 |

**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).

### 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-ftx` | FTx | Pure Rust FT8/FT4/FT2 decoder; 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<f32>)
    ↓
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 us EU / 75 us 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<f32>,   // 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 <path>` 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<String>` 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<RigState>
remote_client.rs receives via watch::Receiver
  → broadcasts to frontends via watch::Sender<RigState>
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<Mutex<>>
remote_client.rs polls GetSpectrum every 40 ms
  → stores SpectrumData in SharedSpectrum (Arc<Mutex<>>)
HTTP frontend reads SharedSpectrum
  → renders waterfall in browser via WebSocket or polling
```

### Audio Flow

```
SoapySDR IQ → DSP pipeline → PCM (Vec<f32>)
  → broadcast::Sender<Vec<f32>>
  ↙ (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
```

---

## Detailed Component Notes

### Rig Task Internals (`rig_task.rs` — 1,315 lines)

The rig task is the heart of the server. Key implementation details:

- **Command batching**: Accumulates pending requests before processing sequentially in FIFO order.
- **Spectrum deduplication**: Concurrent `GetSpectrum` requests are collapsed — one DSP computation broadcasts to all waiting responders.
- **Adaptive polling**: Poll interval adjusts based on TX state (100ms during TX, 500ms idle).
- **Grace period**: 800ms pause on polling after power-on/off operations to let hardware settle.
- **VFO priming**: Optional initialization sequence that toggles VFO A/B to populate the state cache.
- **Per-rig decoder histories**: Each rig maintains independent `Arc<DecoderHistories>` for all 11 decoder types.
- **Configurable timeouts**: `command_exec_timeout` (default 10s) and `poll_refresh_timeout` (default 8s) are configurable via `RigTaskConfig` and the TOML `[timeouts]` section.
- **Crash recovery**: Rig tasks are monitored; on crash, an `Error` state is broadcast to clients via the watch channel so they see the failure instead of silent timeout.

### Audio Pipeline (`audio.rs` — 3,977 lines)

The audio module handles decoder history storage and stream management:

- **`DecoderHistories`**: Per-rig mutable store for 11 decoder history queues (AIS, VDES, APRS, HF_APRS, CW, FT8, FT4, FT2, WSPR, WXSAT, LRPT).
- **Time-based retention**: 24h TTL on all history with periodic pruning.
- **Capacity bounds**: Per-decoder max of 10,000 entries (`MAX_HISTORY_ENTRIES`) prevents unbounded memory growth on busy channels.
- **Atomic total count**: `AtomicUsize` with CAS loop avoids acquiring 11 mutex locks in `snapshot_all()`.
- **Lock poisoning recovery with logging**: Uses `lock_or_recover()` helper that logs a warning when recovering from a poisoned mutex.
- **`StreamErrorLogger`**: Suppresses duplicate stream errors with 60s periodic summaries and error classification (alsa_poll_failure, input/output_stream_error).
- **Device enumeration helpers**: `find_input_device()` and `find_output_device()` extract the repeated device lookup logic from `run_capture()`/`run_playback()`.
- **CRC filtering**: APRS records filtered by `crc_ok` before storage.

### Remote Client Dual-Connection Model

`remote_client.rs` maintains two independent TCP connections to the server:

1. **Main connection** (port 4530): State polling, command forwarding, rig discovery.
2. **Spectrum connection** (dedicated): Polls `GetSpectrum` at 50ms intervals (20 fps) independently to avoid blocking the main connection during command processing.

Constants: `CONNECT_TIMEOUT: 5s`, `IO_TIMEOUT: 15s`, `SPECTRUM_IO_TIMEOUT: 3s`. Exponential backoff with jitter on reconnect.

### FrontendRuntimeContext Sub-Structs

The `FrontendRuntimeContext` struct in `trx-frontend/src/lib.rs` is decomposed into coherent sub-structs:

| Sub-struct | Purpose | Key fields |
|-----------|---------|------------|
| `AudioContext` | Audio streaming channels | `rx`, `tx`, `info`, `decode_rx`, `clients` |
| `DecodeHistoryContext` | Decode history for all types | `ais`, `vdes`, `aprs`, `hf_aprs`, `cw`, `ft8`, `ft4`, `ft2`, `wspr` |
| `HttpAuthConfig` | HTTP auth settings | `enabled`, `rx_passphrase`, `session_ttl_secs`, `tokens` |
| `HttpUiConfig` | HTTP UI display config | `show_sdr_gain_control`, `initial_map_zoom`, `spectrum_*` |
| `RigRoutingContext` | Remote rig state & routing | `active_rig_id`, `remote_rigs`, `rig_states`, `server_connected` |
| `OwnerInfo` | Station metadata | `callsign`, `website_url`, `ais_vessel_url_base` |
| `VChanContext` | Virtual channel audio | `audio`, `audio_cmd`, `destroyed`, `rig_audio_cmd` |
| `SpectrumContext` | Spectrum data | `sender`, `per_rig` |
| `PerRigAudioContext` | Per-rig audio channels | `rx`, `info` |

### Decoder Implementation Patterns

All real-time decoders follow a consistent pattern:

```rust
// 1. Stateful decoder struct with sample buffer
pub struct XxxDecoder { sample_buf: Vec<f32>, ... }

// 2. Block/sample processing
pub fn process_block(&mut self, samples: &[f32]) { ... }

// 3. Result extraction
pub fn decode_if_ready(&mut self) -> Vec<XxxResult> { ... }
```

| Decoder | Algorithm | Sample Rate | Key Constants |
|---------|-----------|-------------|---------------|
| FT8/FT4/FT2 | Waterfall + LDPC/OSD | Varies | MAX_LDPC_ITERATIONS=20, MAX_CANDIDATES=120 |
| CW | Goertzel tone detection | Varies | 10ms windows, tone range 300–1200 Hz |
| APRS | Bell 202 AFSK (1200/2200 Hz) | 9600 | HDLC framing, NRZI, CRC-16-CCITT |
| AIS | GMSK 9600 baud | 9600 | Narrowband FM input |
| WSPR | Fano decoder | 12000 | 162 symbols, 120s slot, 1.46 Hz spacing |
| RDS | RRC matched filter + Costas PLL | Native | 57 kHz subcarrier, 1187.5 bps, OSD FEC |
| VDES | pi/4-QPSK 76.8 ksps | 100k | Burst detection, partial Turbo FEC |

### Backend Reliability Workarounds (FT-817)

The FT-817 CAT backend (`trx-backend-ft817/`) includes empirical workarounds for hardware quirks:

- **Duplicate frame sends**: `set_mode()` and `set_ptt()` send CAT frames twice with 80ms delay (radio sometimes drops first frame).
- **Panel unlock before commands**: Clears stale bytes from the serial buffer.
- **Power-on dummy frame**: CPU wakes before CAT framing locks; dummy frame ensures readiness.
- **VFO state inference**: Infers VFO A/B by matching frequencies against cached values (fragile when frequencies collide).
- **Read timeout**: 800ms per CAT read operation (not configurable).
