[docs](trx-rs): refresh top-level docs and config example

Rewrite the README, remove AI-generated planning docs, and regenerate the combined example config.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-12 22:09:34 +01:00
parent fc24dc37ed
commit 2d014ac45b
4 changed files with 198 additions and 666 deletions
+143 -72
View File
@@ -4,114 +4,185 @@
# trx-rs # trx-rs
A modular transceiver control stack with configurable backends and frontends. The rig task is driven by controller components (state machine, handlers, and policies) with configurable polling and retry behavior via the `[behavior]` section in the config file. `trx-rs` is a modular amateur radio control stack written in Rust.
It splits radio hardware access from user-facing interfaces so you can run
rig control, SDR DSP, decoding, audio streaming, and web access as separate,
composable pieces.
**Note**: This is a live project with evolving APIs. Please report issues and feature requests. The project is built around two primary binaries:
Configuration reference: see `CONFIGURATION.md` for all server/client options and defaults. - `trx-server`: talks to radios and SDR backends
- `trx-client`: connects to the server and exposes frontends such as the web UI
## Configuration Files ## Web UI Demo
`trx-server` and `trx-client` read configuration from a shared `trx-rs.toml`. > GIF placeholder: add an animated walkthrough of the website here.
- Default search order for each app: ## What It Does
current directory, then `~/.config/trx-rs`, then `/etc/trx-rs`
- At each location, the loader checks:
`trx-rs.toml` and reads the `[trx-server]` or `[trx-client]` section
- Config file name:
`trx-rs.toml`
- `--config <FILE>` loads an explicit config file path and reads the matching `[trx-server]` or `[trx-client]` section from that file.
- `--print-config` prints an example combined config block suitable for `trx-rs.toml`.
See `trx-rs.toml.example` for a complete combined example. - Controls supported radios over networked client/server boundaries
- Exposes a browser UI, a rigctl-compatible frontend, and JSON-based control
- Supports SDR workflows with live spectrum, waterfall, demodulation, and decode
- Streams Opus audio between server, client, and browser
- Runs multiple decoders including AIS, APRS, CW, FT8, RDS, VDES, and WSPR
- Supports multi-rig deployments and SDR virtual channels
- Loads backends and frontends via plugins
## Supported backends ## Architecture
- Yaesu FT-817 (feature-gated crate `trx-backend-ft817`) At a high level:
- Planned: other rigs I own; contributions and reports are welcome.
## Frontends 1. `trx-server` owns the radio hardware and DSP pipeline.
2. `trx-client` connects to the server over TCP for control and audio.
3. Frontends hang off `trx-client`, including the HTTP web UI.
- HTTP status/control frontend (`trx-frontend-http`) This separation is intentional: it keeps hardware access local to one host while
- JSON TCP control frontend (`trx-frontend-http-json`) making control and monitoring available elsewhere on the network.
- rigctl-compatible TCP frontend (`trx-frontend-rigctl`, listens on 127.0.0.1:4532)
## HTTP Frontend Authentication ## Workspace Layout
The HTTP frontend supports optional passphrase-based authentication with two roles: - `src/trx-core`: shared types, rig state, controller logic
- `src/trx-protocol`: client/server protocol types and codecs
- `src/trx-app`: shared app bootstrapping, config, logging, plugins
- `src/trx-server`: server binary and backend integration
- `src/trx-client`: client binary and remote connection handling
- `src/trx-client/trx-frontend`: frontend abstraction
- `src/decoders`: protocol-specific decoder crates
- `examples/trx-plugin-example`: minimal plugin example
- **rx**: Read-only access to status, events, decode history, and audio streams ## Supported Pieces
- **control**: Full access including transmit control (TX/PTT) and power toggling
Authentication is disabled by default. When enabled, users must log in via a passphrase before accessing the web UI. Sessions are managed server-side with configurable time-to-live and cookie security settings. ### Backends
### Configuration - Yaesu FT-817
- Yaesu FT-450D
- SoapySDR-based SDR backend
Enable authentication under `[trx-client.frontends.http.auth]` in `trx-rs.toml`. ### Frontends
```toml - HTTP web frontend
[trx-client.frontends.http.auth] - rigctl-compatible TCP frontend
enabled = true - JSON-over-TCP frontend
rx_passphrase = "read-only-secret"
control_passphrase = "full-control-secret" ### Decoders
session_ttl_min = 480 # 8 hours
cookie_secure = false # Set to true for HTTPS - AIS
cookie_same_site = "Lax" - APRS
- CW
- FT8
- RDS
- VDES
- WSPR
## Build Requirements
You will need Rust plus a few system libraries.
### Common dependencies
- `libopus`
- `pkg-config` or `pkgconf`
- `cmake`
### SDR builds
- `libsoapysdr`
### Audio builds
- Core Audio on macOS, or ALSA development packages on Linux
## Configuration
Both `trx-server` and `trx-client` read from a shared `trx-rs.toml`.
- Default lookup order: current directory, `~/.config/trx-rs`, `/etc/trx-rs`
- Use `--config <FILE>` to point at an explicit config file
- Use `--print-config` to print an example combined config
Start from [`trx-rs.toml.example`](trx-rs.toml.example).
## Quick Start
### 1. Build
```bash
cargo build
``` ```
### Security Considerations ### 2. Create a config file
- **Local/LAN use**: Default settings are safe for 127.0.0.1 or trusted local networks. ```bash
- **Remote access**: For internet-exposed deployments: cp trx-rs.toml.example trx-rs.toml
- Deploy behind HTTPS (reverse proxy or TLS termination) ```
- Set `cookie_secure = true`
- Use strong passphrases (random, 16+ chars)
- Consider firewall rules and network segmentation
- **Passphrase storage**: Passphrases are stored in plaintext in the config file. Protect the config file with appropriate file permissions.
- **No rate limiting**: The current implementation does not include login rate limiting. For high-security scenarios, deploy behind a reverse proxy with rate limiting.
### Architecture Adjust backend, frontend, audio, and auth settings for your environment.
- **Sessions**: In-memory, expire after configured TTL (default 8 hours) ### 3. Run the server
- **Cookies**: HttpOnly, configurable Secure and SameSite attributes
- **Route protection**: Middleware validates session on protected endpoints; public routes (static assets, login) are always accessible
- **TX/PTT gating**: Control-only endpoints return 404 to rx-authenticated users (when `tx_access_control_enabled=true`)
## Audio streaming ```bash
cargo run -p trx-server
```
Bidirectional Opus audio streaming between server, client, and browser. ### 4. Run the client
- **Server** captures audio from a configured input device (cpal), encodes to Opus, and streams over a dedicated TCP connection (default port 4533). TX audio received from clients is decoded and played back. ```bash
- **Client** connects to the server's audio TCP port and relays Opus frames to/from the HTTP frontend via a WebSocket at `/audio`. cargo run -p trx-client
- **Browser** connects to the `/audio` WebSocket, decodes Opus via WebCodecs `AudioDecoder`, and plays RX audio. TX audio is captured via `getUserMedia` and encoded with WebCodecs `AudioEncoder`. ```
Enable with `[audio] enabled = true` in the server config and `[frontends.audio] enabled = true` in the client config. ### 5. Open the web UI
## Dependencies Open the configured HTTP frontend address in a browser.
### System libraries ## Web Frontend Highlights
The following system libraries are required at build time: - Real-time spectrum and waterfall
- Frequency, mode, and bandwidth control
- Decoder dashboards and history
- SDR virtual channels
- Browser RX/TX audio
- Optional authentication with read-only and control roles
| Library | Purpose | Install | ## Authentication
|---------|---------|---------|
| **libopus** | Opus audio codec encoding/decoding | `zb install opus` (or your system package manager) |
| **libsoapysdr** | Required for SoapySDR-based SDR backends | `zb install soapysdr` (or your system package manager) |
| **cmake** | Required by the `audiopus_sys` build script if libopus is not found via pkg-config | `zb install cmake` |
| **pkg-config** / **pkgconf** | Locates system libopus during build | `zb install pkgconf` |
| **Core Audio** (macOS) / **ALSA** (Linux) | Audio device access via cpal | Provided by the OS (macOS) or `alsa-lib-dev` (Linux) |
## Plugin discovery The HTTP frontend supports optional passphrase-based authentication.
`trx-server` and `trx-client` can load shared-library plugins that register backends/frontends - `rx`: read-only access
via a `trx_register` entrypoint. Search paths: - `control`: full control access
When exposing the web UI beyond a trusted LAN, run it behind HTTPS and enable
secure cookie settings in the config.
## Audio
Audio is transported as Opus between server, client, and browser.
- `trx-server` captures and encodes audio
- `trx-client` relays audio to the HTTP frontend
- Browsers connect over `/audio`
## Plugins
Both binaries can discover shared-library plugins through:
- `./plugins` - `./plugins`
- `~/.config/trx-rs/plugins` - `~/.config/trx-rs/plugins`
- `TRX_PLUGIN_DIRS` (path-separated) - `TRX_PLUGIN_DIRS`
Example plugin: `examples/trx-plugin-example` See [`examples/trx-plugin-example/README.md`](examples/trx-plugin-example/README.md).
## Documentation
- [`OVERVIEW.md`](OVERVIEW.md): architecture and design overview
- [`CONTRIBUTING.md`](CONTRIBUTING.md): contribution and commit rules
## Project Status
This is an active project with evolving APIs and frontend behavior. Expect some
rough edges and ongoing refactors.
## License ## License
This project is licensed under the BSD-2-Clause license. See `LICENSES/` for bundled third-party license files. Licensed under BSD-2-Clause.
See [`LICENSES`](LICENSES) for bundled third-party license files.
-247
View File
@@ -1,247 +0,0 @@
# Virtual Channels
## Overview
Virtual channels allow a single SDR rig to simultaneously receive multiple signals within
its capture bandwidth. Each virtual channel has its own frequency offset, mode, and
independent decoder pipeline. Traditional (non-SDR) rigs expose no virtual channels.
## Concepts
### Channel 0 (Primary)
The permanent default channel. Always exists. Controlled by normal rig commands
(`SetFreq`, `SetMode`, etc.). Cannot be deallocated.
### Virtual Channels (1+)
Dynamically allocated. Each has:
- An IF offset relative to the SDR center frequency
- Its own mode / demodulator
- Its own Opus audio stream
- Its own decoder subscriptions (FT8, APRS, CW, etc.)
- A ref-count of SSE sessions currently subscribed to it
A virtual channel is freed when its ref-count drops to zero (last subscriber
disconnects or switches away), except channel 0 which is permanent.
### Session Binding
Each SSE session (`/events`) is assigned a server-generated `session_id` (UUID),
returned in the initial `session` SSE event on connect. The client uses this ID
when allocating a virtual channel:
```
POST /rigs/{rig_id}/channels
{ "session_id": "<uuid>", "freq_hz": 14095600, "mode": "CW" }
```
The server tracks (session_id → channel_id) and decrements the channel ref-count
when the SSE stream drops. A session may only own one virtual channel at a time;
allocating a second implicitly releases the first.
### Center Frequency Constraint
The SDR center frequency is shared across all channels. When more than one channel
is active, attempting to set the center frequency (channel 0 freq) to a value that
would place any other channel outside the capture bandwidth returns **409 Conflict**.
Similarly, tuning a virtual channel outside the current capture bandwidth returns
**409 Conflict**.
### Renumbering
Channels are identified internally by UUID. The display index (0, 1, 2, …) is
derived from the server-returned ordered list. Indices are reassigned after
deallocation so the list stays compact.
## Capacity
Configured in the server config (`trx-server.toml`):
```toml
[rig.sdr_options]
max_virtual_channels = 4 # default: 4 (including channel 0)
```
Attempting to allocate beyond the cap returns **429 Too Many Requests**.
## HTTP API
All endpoints require at minimum the **Rx** role for reads; **Control** for writes.
| Method | Path | Description |
|--------|------|-------------|
| GET | `/rigs/{rig_id}/channels` | List all active channels |
| POST | `/rigs/{rig_id}/channels` | Allocate a new virtual channel |
| DELETE | `/rigs/{rig_id}/channels/{channel_id}` | Deallocate (not channel 0) |
| GET | `/rigs/{rig_id}/channels/{channel_id}` | Get channel state |
| PUT | `/rigs/{rig_id}/channels/{channel_id}` | Set freq/mode for a channel |
### GET /rigs/{rig_id}/channels
```json
[
{
"id": "00000000-0000-0000-0000-000000000000",
"index": 0,
"freq_hz": 14074000,
"mode": "USB",
"subscribers": 2,
"permanent": true
},
{
"id": "a1b2c3d4-...",
"index": 1,
"freq_hz": 14095600,
"mode": "CW",
"subscribers": 1,
"permanent": false
}
]
```
### POST /rigs/{rig_id}/channels
Request body (optional):
```json
{ "freq_hz": 14095600, "mode": "CW" }
```
Returns the new channel object. Errors:
- 429: cap reached
- 409: freq outside capture bandwidth
- 400: rig does not support virtual channels (traditional rig)
### PUT /rigs/{rig_id}/channels/{channel_id}
```json
{ "freq_hz": 14100000, "mode": "USB" }
```
Errors:
- 409: freq outside capture bandwidth (when other channels are active)
- 404: channel not found
### DELETE /rigs/{rig_id}/channels/{channel_id}
Deallocates the channel. All sessions subscribed to it are moved to channel 0
via an SSE event `channel-evicted`.
## SSE Events
New SSE event types:
| Event | Payload | Description |
|-------|---------|-------------|
| `channels` | `ChannelList` | Full channel list snapshot |
| `channel-updated` | `Channel` | One channel changed freq/mode/subscribers |
| `channel-evicted` | `{evicted_id, fallback_id}` | Client's channel was deallocated |
The `channels` snapshot is sent on connect (after the rig state snapshot) and
whenever the channel list changes.
## Audio WebSocket
Each channel exposes an independent Opus audio stream:
```
GET /rigs/{rig_id}/channels/{channel_id}/audio (WebSocket upgrade)
```
The legacy `/audio` endpoint continues to work and serves channel 0.
## Decode SSE
Each channel exposes its own independent decoder event stream:
```
GET /rigs/{rig_id}/channels/{channel_id}/decode (SSE)
```
The legacy `/decode` endpoint continues to work and serves channel 0's decoders.
Decoded frames are tagged with the channel's freq/mode at decode time.
## Data Model (Rust)
```rust
// In trx-core or trx-server
pub struct VirtualChannel {
pub id: Uuid,
pub freq_hz: u64,
pub mode: RigMode,
pub subscribers: usize, // ref-count
pub permanent: bool, // true for channel 0
}
pub struct ChannelManager {
channels: Vec<VirtualChannel>, // index 0 = primary
max_channels: usize,
// DSP handles indexed by position
dsp_handles: Vec<ChannelDspHandle>,
}
```
### ChannelDspHandle
Wraps a dynamically allocated `ChannelDsp` slot in the `SdrPipeline`. The
pipeline gains `add_channel()` / `remove_channel()` methods that operate on the
live IQ processing loop (via a command channel to the sdr-iq-read thread).
## DSP Integration
The existing `SdrPipeline` in `trx-backend-soapysdr` has a fixed set of
`ChannelDsp` instances (primary + AIS A + AIS B + optional VDES). Virtual
channels extend this with a dynamic slot list:
```
IQ Broadcast (broadcast::Sender<Vec<Complex<f32>>>)
├─ ChannelDsp[0] ← channel 0 (primary, permanent)
├─ ChannelDsp[1] ← AIS A (internal, not user-visible)
├─ ChannelDsp[2] ← AIS B (internal, not user-visible)
└─ ChannelDsp[3+] ← user virtual channels (dynamic)
```
The IQ broadcast already fans out to all receivers; adding a new virtual channel
simply spawns a new async task that subscribes to the IQ broadcast and runs a
`ChannelDsp` for that slot. Removing a channel aborts that task.
Each virtual channel task outputs PCM frames to a `broadcast::Sender<Vec<f32>>`
stored in `ChannelManager`, which the audio WebSocket handler subscribes to.
## Frontend UI
### Channel Picker (SDR rigs only)
A compact control bar visible only when the active rig supports virtual channels:
```
[ Ch 0 (14.074 USB) ▼ ] [ + New Channel ] [ ✕ Remove This ]
```
- **Picker**: dropdown of all active channels with freq+mode label
- **+ New Channel**: allocates a new channel, switches picker to it, focuses the
freq/mode controls
- **✕ Remove This**: deallocates the current channel (disabled for channel 0);
confirms before sending DELETE
### Channel State
When a channel is selected in the picker, the main VFO display, mode selector,
and decoder panels reflect that channel's state (not rig-global state). Tuning
and mode changes act on the selected channel via `PUT /rigs/{rig_id}/channels/{id}`.
### SSE Fallback
On receiving `channel-evicted`, the frontend automatically:
1. Switches the picker to channel 0
2. Shows a brief toast: "Virtual channel removed — switched to primary"
## Implementation Order
1. **`SdrPipeline` dynamic channels** — `add_channel()` / `remove_channel()` via
command channel to IQ read thread
2. **`ChannelManager`** in `trx-server` — tracks channels, ref-counts, DSP handles
3. **HTTP API** in `trx-frontend-http` — channel CRUD, audio WebSocket per channel
4. **SSE events**`channels` snapshot on connect, `channel-updated`, `channel-evicted`
5. **Frontend** — channel picker, +/✕ buttons, VFO/mode reflecting selected channel
-188
View File
@@ -1,188 +0,0 @@
# Virtual-Channel Audio — Implementation Plan
## Goal
Each virtual channel (SDR DSP slice) has its own Opus audio stream. When the
browser switches to a non-primary virtual channel the `/audio` WebSocket should
deliver audio demodulated at that channel's frequency and mode, not the primary
channel's audio.
---
## Current Architecture (baseline)
```
SoapySDR HW
└─ SdrPipeline (slot 0: primary, slot 1..N: virtual)
pcm_tx[0] pcm_tx[1] ... pcm_tx[N] (broadcast::Sender<Vec<f32>>)
trx-server/src/main.rs
subscribe_pcm(slot 0) → Opus encode → rx_audio_tx (broadcast::Sender<Bytes>)
trx-server/src/audio.rs handle_audio_client()
writes [0x00] StreamInfo
[0x0a] history blob
loop: [0x01] RX frame ← only primary channel
trx-client/src/audio_client.rs
reads all frames → rx_audio_tx.send(bytes) (single broadcast)
FrontendRuntimeContext.audio_rx (single broadcast::Sender<Bytes>)
audio.rs / audio_ws()
subscribes to audio_rx → WebSocket to browser
```
Only slot 0 (primary) is ever encoded/transmitted. All sessions hear the same
audio.
---
## Planned Architecture
```
SdrPipeline pcm_tx[0..N]
trx-server/src/audio.rs (extended handle_audio_client)
┌── per-rig VChanAudioMixer ──────────────────────────────────┐
│ tracks (server_uuid → OpusEncoder + broadcast::Sender<Bytes>) │
│ listens for VCHAN_SUB/VCHAN_UNSUB from client │
│ Opus-encodes each channel's PCM independently │
└─────────────────────────────────────────────────────────────┘
│ wire frames:
│ [0x01] RX_FRAME (primary channel, unchanged)
│ [0x0b] RX_FRAME_CH [16 B UUID][N B Opus] ← NEW
│ [0x0c] VCHAN_ALLOCATED [16 B UUID] ← NEW
│ client→server:
│ [0x0d] VCHAN_SUB [16 B UUID] subscribe to channel
│ [0x0e] VCHAN_UNSUB [16 B UUID] unsubscribe
trx-client/src/audio_client.rs
demux 0x0b frames by UUID → per-channel broadcast::Sender<Bytes>
on 0x0c (allocated): publish UUID to per-channel map
FrontendRuntimeContext
audio_rx: Option<broadcast::Sender<Bytes>> (primary, unchanged)
vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>> ← NEW
ClientChannelManager (trx-frontend-http/src/vchan.rs)
allocate(): after creating local entry, sends VCHAN_SUB via new
vchan_audio_tx: mpsc::Sender<VChanAudioCmd> ← NEW
delete_channel(): sends VCHAN_UNSUB
expose: subscribe_audio(channel_id) → Option<broadcast::Receiver<Bytes>>
audio_ws() (trx-frontend-http/src/audio.rs)
accepts ?channel_id=<uuid> query param
if present → lookup context.vchan_audio[uuid] → subscribe
else → context.audio_rx (primary, current behaviour)
```
---
## Wire Protocol Additions (trx-core/src/audio.rs)
```
AUDIO_MSG_RX_FRAME_CH = 0x0b
AUDIO_MSG_VCHAN_ALLOCATED = 0x0c
AUDIO_MSG_VCHAN_SUB = 0x0d
AUDIO_MSG_VCHAN_UNSUB = 0x0e
```
Frame layout for `RX_FRAME_CH`:
```
[0x0b] [4 B BE length = 16 + opus_len] [16 B UUID bytes] [opus_len B Opus]
```
Frame layout for `VCHAN_ALLOCATED`, `VCHAN_SUB`, `VCHAN_UNSUB`:
```
[type] [4 B BE length = 16] [16 B UUID bytes]
```
---
## Layer-by-Layer Changes
### 1. `trx-core/src/audio.rs`
- Add four new `AUDIO_MSG_*` constants.
- Add helper `read_vchan_frame(reader) -> (Uuid, Bytes)` and
`write_vchan_frame(writer, msg_type, uuid, payload)`.
### 2. `trx-server/src/audio.rs` (`handle_audio_client`)
- Accept `vchan_manager: Option<SharedVChanManager>` from `RigHandle`.
- Spawn a `VChanAudioMixer` task:
- Holds `HashMap<Uuid, (JoinHandle, broadcast::Sender<Bytes>)>`.
- On `VCHAN_SUB { uuid }`: call `vchan_manager.subscribe_pcm(uuid)`, spawn
Opus-encode task, write `VCHAN_ALLOCATED { uuid }` to client.
- On `VCHAN_UNSUB { uuid }`: abort encode task, remove from map.
- On PCM ready: Opus-encode, write `RX_FRAME_CH { uuid, opus }`.
- Add the `vchan_manager` parameter to `run_audio_listener()` and pass it
through from `main.rs`.
### 3. `trx-server/src/main.rs`
- Pass `rig_handle.vchan_manager.clone()` to `run_audio_listener()`.
### 4. `trx-client/src/audio_client.rs`
- Add `vchan_audio_tx: mpsc::Sender<VChanAudioEvent>` parameter
(where `VChanAudioEvent = Allocated(Uuid, broadcast::Sender<Bytes>) | Frame(Uuid, Bytes)`).
- On `RX_FRAME_CH { uuid, opus }`: forward to per-channel sender (create if
first frame for that uuid).
- On `VCHAN_ALLOCATED { uuid }`: signal that the channel is ready.
### 5. `trx-client/src/main.rs`
- Create `vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>>`
shared between audio_client task and FrontendRuntimeContext.
- Add an `mpsc::Sender<VChanAudioCmd>` that lets the HTTP frontend request
SUB/UNSUB over the audio TCP; pass it into `run_audio_client()`.
### 6. `trx-client/trx-frontend/src/lib.rs` (`FrontendRuntimeContext`)
- Add:
```rust
pub vchan_audio: Arc<RwLock<HashMap<Uuid, broadcast::Sender<Bytes>>>>,
pub vchan_audio_cmd: Option<mpsc::Sender<VChanAudioCmd>>,
```
- Initialise both to empty/None in `new()`.
### 7. `trx-client/trx-frontend/trx-frontend-http/src/vchan.rs` (`ClientChannelManager`)
- `allocate()`: after inserting the local record, if `vchan_audio_cmd` is
available, send `VChanAudioCmd::Subscribe(uuid)`.
- `delete_channel()`: send `VChanAudioCmd::Unsubscribe(uuid)`.
- `subscribe_audio(channel_id, context) -> Option<broadcast::Receiver<Bytes>>`:
look up `context.vchan_audio.read()[channel_id].subscribe()`.
### 8. `trx-client/trx-frontend/trx-frontend-http/src/audio.rs` (`audio_ws`)
- Parse optional `channel_id: Option<Uuid>` from query string.
- If `Some(uuid)`:
- Look up `context.vchan_audio.read()[uuid]` → `broadcast::Sender<Bytes>`.
- Subscribe, forward Opus frames exactly as today but from that sender.
- Else: current primary-channel path unchanged.
### 9. `assets/web/plugins/vchan.js`
- `vchanSubscribe()` and `vchanAllocate()` call `vchanReconnectAudio()`.
- `vchanReconnectAudio()`:
- If on virtual channel: `reconnectAudioWs(vchanActiveId)` (pass channel UUID).
- If on primary: `reconnectAudioWs(null)`.
- `reconnectAudioWs(channelId)` (new in `app.js` or `vchan.js`):
- Close existing `audioWs`.
- Reopen `new WebSocket('/audio' + (channelId ? '?channel_id=' + channelId : ''))`.
---
## Out of Scope (non-SDR rigs)
Non-SDR rigs (`vchan_manager === None`) are unaffected. The new message types
are only exchanged when the server-side vchan manager is present. Primary-
channel audio behaviour is 100% backwards-compatible.
---
## Implementation Order
1. `trx-core/src/audio.rs` — add constants and frame helpers *(no breakage)*
2. `trx-server/src/audio.rs` — `VChanAudioMixer` + new frame handling
3. `trx-server/src/main.rs` — plumb vchan_manager through
4. `trx-client/src/audio_client.rs` — demux RX_FRAME_CH
5. `trx-client/src/main.rs` — shared vchan_audio map + cmd channel
6. `trx-frontend/src/lib.rs` — new FrontendRuntimeContext fields
7. `trx-frontend-http/src/vchan.rs` — SUB/UNSUB on allocate/delete
8. `trx-frontend-http/src/audio.rs` — channel_id query param routing
9. `vchan.js` — reconnect WebSocket on channel switch
+55 -159
View File
@@ -1,245 +1,141 @@
# trx-rs Combined Configuration File [trx-server]
# rigs = []
# Copy this file to one of:
# ./trx-rs.toml (current directory)
# ~/.config/trx-rs/trx-rs.toml (user config)
# /etc/trx-rs/trx-rs.toml (system-wide)
#
# Or use per-binary files (trx-server.toml / trx-client.toml) without the
# section headers — both formats are supported and the section headers may
# also appear in any per-binary file passed via --config.
#
# CLI arguments override config file values.
# =============================================================================
# trx-server — connects to radio hardware, exposes a JSON/audio TCP server
# =============================================================================
[trx-server.general] [trx-server.general]
# Callsign or station identifier
callsign = "N0CALL" callsign = "N0CALL"
log_level = "info"
# Log level: trace, debug, info, warn, error latitude = 52.2297
# log_level = "info" longitude = 21.0122
[trx-server.rig] [trx-server.rig]
# Rig model: ft817 (more models coming)
model = "ft817" model = "ft817"
# Initial frequency (Hz) before first CAT read
initial_freq_hz = 144300000 initial_freq_hz = 144300000
# Initial mode before first CAT read (LSB, USB, CW, CWR, AM, WFM, FM, DIG, PKT)
initial_mode = "USB" initial_mode = "USB"
[trx-server.rig.access] [trx-server.rig.access]
# Access type: "serial" or "tcp"
type = "serial" type = "serial"
# Serial port settings (when type = "serial")
port = "/dev/ttyUSB0" port = "/dev/ttyUSB0"
baud = 9600 baud = 9600
# TCP settings (when type = "tcp")
# host = "192.168.1.100"
# tcp_port = 4532
[trx-server.behavior] [trx-server.behavior]
# Polling interval when idle (milliseconds)
poll_interval_ms = 500 poll_interval_ms = 500
# Polling interval when transmitting (milliseconds)
poll_interval_tx_ms = 100 poll_interval_tx_ms = 100
# Maximum retry attempts for transient errors
max_retries = 3 max_retries = 3
# Base delay for exponential backoff (milliseconds)
retry_base_delay_ms = 100 retry_base_delay_ms = 100
[trx-server.listen] [trx-server.listen]
# Enable the JSON TCP listener for client connections
enabled = true enabled = true
# IP address to listen on (use "0.0.0.0" for all interfaces)
listen = "127.0.0.1" listen = "127.0.0.1"
# TCP port to listen on
port = 4530 port = 4530
[trx-server.listen.auth] [trx-server.listen.auth]
# Authentication tokens (empty = no auth required)
tokens = [] tokens = []
[trx-server.pskreporter] [trx-server.audio]
# Enable uploads of decoded FT8/WSPR spots to PSK Reporter enabled = true
enabled = false listen = "127.0.0.1"
port = 4531
rx_enabled = true
tx_enabled = true
sample_rate = 48000
channels = 2
frame_duration_ms = 20
bitrate_bps = 256000
# PSK Reporter endpoint (UDP) [trx-server.pskreporter]
enabled = false
host = "report.pskreporter.info" host = "report.pskreporter.info"
port = 4739 port = 4739
# Optional receiver locator (4 or 6-char Maidenhead).
# If omitted, it is derived from [trx-server.general] latitude/longitude.
# receiver_locator = "JO93"
[trx-server.aprsfi] [trx-server.aprsfi]
# Enable APRS-IS IGate uplink (forwards received RF APRS packets to APRS-IS / aprs.fi)
enabled = false enabled = false
# APRS-IS server (rotate.aprs.net does DNS round-robin across all tier-2 servers)
host = "rotate.aprs.net" host = "rotate.aprs.net"
port = 14580 port = 14580
passcode = -1
# APRS-IS passcode. -1 = auto-computed from [trx-server.general] callsign.
# passcode = -1
[trx-server.decode_logs] [trx-server.decode_logs]
# Optional decoder message logs to files (APRS/CW/FT8/WSPR)
enabled = false enabled = false
dir = "/path/to/log/dir"
# Base directory for decoder logs.
# Default (if omitted): $XDG_DATA_HOME/trx-rs/decoders
# Fallback: logs/decoders
# dir = "/path/to/decoder-logs"
# Per-decoder log file names (supported tokens: %YYYY% %MM% %DD%)
aprs_file = "TRXRS-APRS-%YYYY%-%MM%-%DD%.log" aprs_file = "TRXRS-APRS-%YYYY%-%MM%-%DD%.log"
cw_file = "TRXRS-CW-%YYYY%-%MM%-%DD%.log" cw_file = "TRXRS-CW-%YYYY%-%MM%-%DD%.log"
ft8_file = "TRXRS-FT8-%YYYY%-%MM%-%DD%.log" ft8_file = "TRXRS-FT8-%YYYY%-%MM%-%DD%.log"
wspr_file = "TRXRS-WSPR-%YYYY%-%MM%-%DD%.log" wspr_file = "TRXRS-WSPR-%YYYY%-%MM%-%DD%.log"
# --- SoapySDR backend example --- [trx-server.sdr]
# To use an SDR device instead of a physical transceiver, set: sample_rate = 1920000
# bandwidth = 1500000
# [trx-server.rig] wfm_deemphasis_us = 50
# model = "soapysdr" center_offset_hz = 100000
# initial_freq_hz = 14074000 channels = []
# initial_mode = "USB" max_virtual_channels = 4
#
# [trx-server.rig.access]
# type = "sdr"
# args = "driver=rtlsdr"
#
# [trx-server.sdr]
# sample_rate = 1920000
# bandwidth = 1500000
# center_offset_hz = 200000
#
# [trx-server.sdr.gain]
# mode = "auto"
# value = 30.0
#
# [trx-server.sdr.squelch]
# enabled = false
# threshold_db = -65.0
# hysteresis_db = 3.0
# tail_ms = 180
#
# [[trx-server.sdr.channels]]
# id = "primary"
# offset_hz = 0
# mode = "auto"
# audio_bandwidth_hz = 3000
# decoders = ["ft8", "cw"]
# stream_opus = true
#
# [[trx-server.sdr.channels]]
# id = "wspr"
# offset_hz = 21600
# mode = "USB"
# audio_bandwidth_hz = 3000
# decoders = ["wspr"]
# stream_opus = false
# ============================================================================= [trx-server.sdr.gain]
# trx-client — connects to trx-server and exposes user-facing frontends mode = "auto"
# ============================================================================= value = 30.0
[trx-server.sdr.squelch]
enabled = false
threshold_db = -65.0
hysteresis_db = 3.0
tail_ms = 180
[trx-client.general] [trx-client.general]
# Callsign or station identifier displayed in frontends
callsign = "N0CALL" callsign = "N0CALL"
website_url = "https://haxx.space"
# Log level: trace, debug, info, warn, error website_name = "haxx.space"
# log_level = "info" ais_vessel_url_base = "https://www.vesselfinder.com/?mmsi="
log_level = "info"
[trx-client.remote] [trx-client.remote]
# Remote trx-server URL (host:port)
url = "192.168.1.100:9000" url = "192.168.1.100:9000"
# Optional target rig ID on a multi-rig server (omit to use server default rig) rig_id = "hf"
# rig_id = "hf"
# Poll interval in milliseconds
poll_interval_ms = 750 poll_interval_ms = 750
[trx-client.remote.auth] [trx-client.remote.auth]
# Bearer token for authenticating with the remote server
token = "my-token" token = "my-token"
[trx-client.frontends.http] [trx-client.frontends.http]
# Enable HTTP/REST frontend with SSE for real-time updates
enabled = true enabled = true
listen = "127.0.0.1" listen = "127.0.0.1"
port = 8080 port = 8080
default_rig_id = "hf"
initial_map_zoom = 10
spectrum_coverage_margin_hz = 50000
spectrum_usable_span_ratio = 0.9200000166893005
show_sdr_gain_control = true
[trx-client.frontends.http.auth] [trx-client.frontends.http.auth]
# Optional passphrase-based authentication for the HTTP frontend
# Disabled by default to preserve backward compatibility
# Enable authentication (default: false)
enabled = false enabled = false
rx_passphrase = "rx-passphrase-example"
# Read-only passphrase: grants access to status/events/audio (rx role) control_passphrase = "control-passphrase-example"
# Leave unset to disable rx access
# rx_passphrase = "rx-only-passphrase"
# Full control passphrase: grants access to all endpoints including TX/PTT (control role)
# Leave unset to disable control access
# control_passphrase = "full-control-passphrase"
# Enforce TX/PTT access control (default: true)
# When true, TX/PTT endpoints return 404 to authenticated users without control role
tx_access_control_enabled = true tx_access_control_enabled = true
# Session time-to-live in minutes (default: 480 = 8 hours)
session_ttl_min = 480 session_ttl_min = 480
# Set Secure flag on session cookie (default: false)
# Should be true if served over HTTPS; false for HTTP/localhost
cookie_secure = false cookie_secure = false
# Cookie SameSite attribute: Strict, Lax (default), or None
cookie_same_site = "Lax" cookie_same_site = "Lax"
[trx-client.frontends.rigctl] [trx-client.frontends.rigctl]
# Enable rigctl-compatible TCP interface (hamlib compatible)
enabled = false enabled = false
listen = "127.0.0.1" listen = "127.0.0.1"
port = 4532 port = 4532
[trx-client.frontends.rigctl.rig_ports]
[trx-client.frontends.http_json] [trx-client.frontends.http_json]
# Enable JSON-over-TCP control interface
enabled = true enabled = true
listen = "127.0.0.1" listen = "127.0.0.1"
# Set to 0 to bind an ephemeral port
port = 0 port = 0
# List of accepted bearer tokens (empty = no auth)
# auth.tokens = ["example-token"] [trx-client.frontends.http_json.auth]
tokens = []
[trx-client.frontends.audio] [trx-client.frontends.audio]
# Enable remote audio stream and decode transport
enabled = true enabled = true
# Remote trx-server audio port
server_port = 4531 server_port = 4531
# Optional per-rig audio ports for multi-rig servers:
# rig_ports.ft817 = 4531 [trx-client.frontends.audio.rig_ports]
# rig_ports.airspyhf = 4532
[trx-client.frontends.audio.bridge] [trx-client.frontends.audio.bridge]
# Enable local cpal bridge for WSJT-X virtual audio routing
enabled = false enabled = false
# Optional exact output device name for RX playback bitrate_bps = 192000
# rx_output_device = "BlackHole 2ch"
# Optional exact input device name for TX capture
# tx_input_device = "BlackHole 2ch"
# Playback/capture gain multipliers
rx_gain = 1.0 rx_gain = 1.0
tx_gain = 1.0 tx_gain = 1.0