[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:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user