diff --git a/README.md b/README.md index cf1e02d..408b09b 100644 --- a/README.md +++ b/README.md @@ -4,114 +4,185 @@ # 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: - 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 ` 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`. +## What It Does -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`) -- Planned: other rigs I own; contributions and reports are welcome. +At a high level: -## 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`) -- JSON TCP control frontend (`trx-frontend-http-json`) -- rigctl-compatible TCP frontend (`trx-frontend-rigctl`, listens on 127.0.0.1:4532) +This separation is intentional: it keeps hardware access local to one host while +making control and monitoring available elsewhere on the network. -## 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 -- **control**: Full access including transmit control (TX/PTT) and power toggling +## Supported Pieces -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 -[trx-client.frontends.http.auth] -enabled = true -rx_passphrase = "read-only-secret" -control_passphrase = "full-control-secret" -session_ttl_min = 480 # 8 hours -cookie_secure = false # Set to true for HTTPS -cookie_same_site = "Lax" +- HTTP web frontend +- rigctl-compatible TCP frontend +- JSON-over-TCP frontend + +### Decoders + +- AIS +- 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 ` 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. -- **Remote access**: For internet-exposed deployments: - - 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. +```bash +cp trx-rs.toml.example trx-rs.toml +``` -### Architecture +Adjust backend, frontend, audio, and auth settings for your environment. -- **Sessions**: In-memory, expire after configured TTL (default 8 hours) -- **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`) +### 3. Run the server -## 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. -- **Client** connects to the server's audio TCP port and relays Opus frames to/from the HTTP frontend via a WebSocket at `/audio`. -- **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`. +```bash +cargo run -p trx-client +``` -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 | -|---------|---------|---------| -| **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) | +## Authentication -## Plugin discovery +The HTTP frontend supports optional passphrase-based authentication. -`trx-server` and `trx-client` can load shared-library plugins that register backends/frontends -via a `trx_register` entrypoint. Search paths: +- `rx`: read-only access +- `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` - `~/.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 -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. diff --git a/VCHANNELS.md b/VCHANNELS.md deleted file mode 100644 index 4d008f5..0000000 --- a/VCHANNELS.md +++ /dev/null @@ -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": "", "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, // index 0 = primary - max_channels: usize, - // DSP handles indexed by position - dsp_handles: Vec, -} -``` - -### 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>>) - ├─ 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>` -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 diff --git a/VIRTAUDIO.md b/VIRTAUDIO.md deleted file mode 100644 index 8c6ea49..0000000 --- a/VIRTAUDIO.md +++ /dev/null @@ -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>) - -trx-server/src/main.rs - subscribe_pcm(slot 0) → Opus encode → rx_audio_tx (broadcast::Sender) - -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) - -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) │ - │ 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 - on 0x0c (allocated): publish UUID to per-channel map - -FrontendRuntimeContext - audio_rx: Option> (primary, unchanged) - vchan_audio: Arc>>> ← NEW - -ClientChannelManager (trx-frontend-http/src/vchan.rs) - allocate(): after creating local entry, sends VCHAN_SUB via new - vchan_audio_tx: mpsc::Sender ← NEW - delete_channel(): sends VCHAN_UNSUB - expose: subscribe_audio(channel_id) → Option> - -audio_ws() (trx-frontend-http/src/audio.rs) - accepts ?channel_id= 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` from `RigHandle`. -- Spawn a `VChanAudioMixer` task: - - Holds `HashMap)>`. - - 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` parameter - (where `VChanAudioEvent = Allocated(Uuid, broadcast::Sender) | 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>>>` - shared between audio_client task and FrontendRuntimeContext. -- Add an `mpsc::Sender` 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>>>, - pub vchan_audio_cmd: Option>, - ``` -- 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>`: - 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` from query string. -- If `Some(uuid)`: - - Look up `context.vchan_audio.read()[uuid]` → `broadcast::Sender`. - - 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 diff --git a/trx-rs.toml.example b/trx-rs.toml.example index 9997a40..1e7e65e 100644 --- a/trx-rs.toml.example +++ b/trx-rs.toml.example @@ -1,245 +1,141 @@ -# trx-rs Combined Configuration File -# -# 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] +rigs = [] [trx-server.general] -# Callsign or station identifier callsign = "N0CALL" - -# Log level: trace, debug, info, warn, error -# log_level = "info" +log_level = "info" +latitude = 52.2297 +longitude = 21.0122 [trx-server.rig] -# Rig model: ft817 (more models coming) model = "ft817" -# Initial frequency (Hz) before first CAT read initial_freq_hz = 144300000 -# Initial mode before first CAT read (LSB, USB, CW, CWR, AM, WFM, FM, DIG, PKT) initial_mode = "USB" [trx-server.rig.access] -# Access type: "serial" or "tcp" type = "serial" - -# Serial port settings (when type = "serial") port = "/dev/ttyUSB0" baud = 9600 -# TCP settings (when type = "tcp") -# host = "192.168.1.100" -# tcp_port = 4532 - [trx-server.behavior] -# Polling interval when idle (milliseconds) poll_interval_ms = 500 - -# Polling interval when transmitting (milliseconds) poll_interval_tx_ms = 100 - -# Maximum retry attempts for transient errors max_retries = 3 - -# Base delay for exponential backoff (milliseconds) retry_base_delay_ms = 100 [trx-server.listen] -# Enable the JSON TCP listener for client connections enabled = true - -# IP address to listen on (use "0.0.0.0" for all interfaces) listen = "127.0.0.1" - -# TCP port to listen on port = 4530 [trx-server.listen.auth] -# Authentication tokens (empty = no auth required) tokens = [] -[trx-server.pskreporter] -# Enable uploads of decoded FT8/WSPR spots to PSK Reporter -enabled = false +[trx-server.audio] +enabled = true +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" 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] -# Enable APRS-IS IGate uplink (forwards received RF APRS packets to APRS-IS / aprs.fi) enabled = false - -# APRS-IS server (rotate.aprs.net does DNS round-robin across all tier-2 servers) host = "rotate.aprs.net" port = 14580 - -# APRS-IS passcode. -1 = auto-computed from [trx-server.general] callsign. -# passcode = -1 +passcode = -1 [trx-server.decode_logs] -# Optional decoder message logs to files (APRS/CW/FT8/WSPR) enabled = false - -# 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%) +dir = "/path/to/log/dir" aprs_file = "TRXRS-APRS-%YYYY%-%MM%-%DD%.log" cw_file = "TRXRS-CW-%YYYY%-%MM%-%DD%.log" ft8_file = "TRXRS-FT8-%YYYY%-%MM%-%DD%.log" wspr_file = "TRXRS-WSPR-%YYYY%-%MM%-%DD%.log" -# --- SoapySDR backend example --- -# To use an SDR device instead of a physical transceiver, set: -# -# [trx-server.rig] -# model = "soapysdr" -# initial_freq_hz = 14074000 -# initial_mode = "USB" -# -# [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] +sample_rate = 1920000 +bandwidth = 1500000 +wfm_deemphasis_us = 50 +center_offset_hz = 100000 +channels = [] +max_virtual_channels = 4 -# ============================================================================= -# trx-client — connects to trx-server and exposes user-facing frontends -# ============================================================================= +[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-client.general] -# Callsign or station identifier displayed in frontends callsign = "N0CALL" - -# Log level: trace, debug, info, warn, error -# log_level = "info" +website_url = "https://haxx.space" +website_name = "haxx.space" +ais_vessel_url_base = "https://www.vesselfinder.com/?mmsi=" +log_level = "info" [trx-client.remote] -# Remote trx-server URL (host:port) url = "192.168.1.100:9000" -# Optional target rig ID on a multi-rig server (omit to use server default rig) -# rig_id = "hf" - -# Poll interval in milliseconds +rig_id = "hf" poll_interval_ms = 750 [trx-client.remote.auth] -# Bearer token for authenticating with the remote server token = "my-token" [trx-client.frontends.http] -# Enable HTTP/REST frontend with SSE for real-time updates enabled = true listen = "127.0.0.1" 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] -# Optional passphrase-based authentication for the HTTP frontend -# Disabled by default to preserve backward compatibility - -# Enable authentication (default: false) enabled = false - -# Read-only passphrase: grants access to status/events/audio (rx role) -# 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 +rx_passphrase = "rx-passphrase-example" +control_passphrase = "control-passphrase-example" tx_access_control_enabled = true - -# Session time-to-live in minutes (default: 480 = 8 hours) 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 SameSite attribute: Strict, Lax (default), or None cookie_same_site = "Lax" [trx-client.frontends.rigctl] -# Enable rigctl-compatible TCP interface (hamlib compatible) enabled = false listen = "127.0.0.1" port = 4532 +[trx-client.frontends.rigctl.rig_ports] + [trx-client.frontends.http_json] -# Enable JSON-over-TCP control interface enabled = true listen = "127.0.0.1" -# Set to 0 to bind an ephemeral port 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] -# Enable remote audio stream and decode transport enabled = true -# Remote trx-server audio port server_port = 4531 -# Optional per-rig audio ports for multi-rig servers: -# rig_ports.ft817 = 4531 -# rig_ports.airspyhf = 4532 + +[trx-client.frontends.audio.rig_ports] [trx-client.frontends.audio.bridge] -# Enable local cpal bridge for WSJT-X virtual audio routing enabled = false -# Optional exact output device name for RX playback -# rx_output_device = "BlackHole 2ch" -# Optional exact input device name for TX capture -# tx_input_device = "BlackHole 2ch" -# Playback/capture gain multipliers +bitrate_bps = 192000 rx_gain = 1.0 tx_gain = 1.0