From b1c232f38839da1392c10548707ad75d9fb37bf3 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Wed, 25 Feb 2026 23:23:48 +0100 Subject: [PATCH] fix(trx-client): route audio by selected rig with per-rig port map --- src/trx-client/src/audio_client.rs | 58 +++++++++++++++++++++++++++++- src/trx-client/src/config.rs | 27 ++++++++++++++ src/trx-client/src/main.rs | 13 ++++--- trx-client.toml.example | 3 ++ 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/trx-client/src/audio_client.rs b/src/trx-client/src/audio_client.rs index ae5268a..1c0ee5a 100644 --- a/src/trx-client/src/audio_client.rs +++ b/src/trx-client/src/audio_client.rs @@ -5,6 +5,8 @@ //! Audio TCP client that connects to the server's audio port and relays //! RX/TX Opus frames via broadcast/mpsc channels. +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use std::time::Duration; use bytes::Bytes; @@ -23,7 +25,10 @@ use trx_core::decode::DecodedMessage; /// Run the audio client with auto-reconnect. pub async fn run_audio_client( - server_addr: String, + server_host: String, + default_port: u16, + rig_ports: HashMap, + selected_rig_id: Arc>>, rx_tx: broadcast::Sender, mut tx_rx: mpsc::Receiver, stream_info_tx: watch::Sender>, @@ -38,12 +43,27 @@ pub async fn run_audio_client( return; } + let server_addr = resolve_audio_addr( + &server_host, + default_port, + &rig_ports, + selected_rig_id + .lock() + .ok() + .and_then(|v| v.clone()) + .as_deref(), + ); info!("Audio client: connecting to {}", server_addr); match TcpStream::connect(&server_addr).await { Ok(stream) => { reconnect_delay = Duration::from_secs(1); if let Err(e) = handle_audio_connection( stream, + &server_host, + default_port, + &rig_ports, + &selected_rig_id, + &server_addr, &rx_tx, &mut tx_rx, &stream_info_tx, @@ -80,6 +100,11 @@ pub async fn run_audio_client( async fn handle_audio_connection( stream: TcpStream, + server_host: &str, + default_port: u16, + rig_ports: &HashMap, + selected_rig_id: &Arc>>, + connected_addr: &str, rx_tx: &broadcast::Sender, tx_rx: &mut mpsc::Receiver, stream_info_tx: &watch::Sender>, @@ -135,6 +160,7 @@ async fn handle_audio_connection( }); // Forward TX frames to server + let mut rig_check = time::interval(Duration::from_millis(500)); loop { tokio::select! { changed = shutdown_rx.changed() => { @@ -164,8 +190,38 @@ async fn handle_audio_connection( _ = &mut rx_handle => { break; } + _ = rig_check.tick() => { + let current_rig = selected_rig_id.lock().ok().and_then(|v| v.clone()); + let desired_addr = resolve_audio_addr( + server_host, + default_port, + rig_ports, + current_rig.as_deref(), + ); + if desired_addr != connected_addr { + info!( + "Audio client: active rig changed ({} -> {}), reconnecting audio", + connected_addr, + desired_addr + ); + break; + } + } } } Ok(()) } + +fn resolve_audio_addr( + host: &str, + default_port: u16, + rig_ports: &HashMap, + selected_rig_id: Option<&str>, +) -> String { + let port = selected_rig_id + .and_then(|rig_id| rig_ports.get(rig_id)) + .copied() + .unwrap_or(default_port); + format!("{}:{}", host, port) +} diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index 22ec057..74308a0 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -11,6 +11,7 @@ //! 4. `~/.config/trx-rs/client.toml` (XDG config) //! 5. `/etc/trx-rs/client.toml` (system-wide) +use std::collections::HashMap; use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -104,6 +105,8 @@ pub struct AudioClientConfig { pub enabled: bool, /// Audio TCP port on the remote server pub server_port: u16, + /// Optional per-rig audio port overrides for multi-rig servers. + pub rig_ports: HashMap, /// Local audio bridge (virtual device integration) pub bridge: AudioBridgeConfig, } @@ -113,6 +116,7 @@ impl Default for AudioClientConfig { Self { enabled: true, server_port: 4531, + rig_ports: HashMap::new(), bridge: AudioBridgeConfig::default(), } } @@ -323,6 +327,17 @@ impl ClientConfig { if self.frontends.audio.enabled && self.frontends.audio.server_port == 0 { return Err("[frontends.audio].server_port must be > 0 when enabled".to_string()); } + for (rig_id, port) in &self.frontends.audio.rig_ports { + if rig_id.trim().is_empty() { + return Err("[frontends.audio].rig_ports keys must not be empty".to_string()); + } + if *port == 0 { + return Err(format!( + "[frontends.audio].rig_ports[\"{}\"] must be > 0", + rig_id + )); + } + } if !self.frontends.audio.bridge.rx_gain.is_finite() || self.frontends.audio.bridge.rx_gain < 0.0 { @@ -492,6 +507,7 @@ mod tests { assert_eq!(config.remote.poll_interval_ms, 750); assert!(config.frontends.audio.enabled); assert_eq!(config.frontends.audio.server_port, 4531); + assert!(config.frontends.audio.rig_ports.is_empty()); assert!(!config.frontends.audio.bridge.enabled); assert_eq!(config.frontends.audio.bridge.rx_gain, 1.0); assert_eq!(config.frontends.audio.bridge.tx_gain, 1.0); @@ -552,6 +568,17 @@ port = 8080 assert!(config.validate().is_err()); } + #[test] + fn test_validate_rejects_zero_audio_rig_port() { + let mut config = ClientConfig::default(); + config + .frontends + .audio + .rig_ports + .insert("ft817".to_string(), 0); + assert!(config.validate().is_err()); + } + #[test] fn test_validate_rejects_http_auth_enabled_without_passphrases() { let mut config = ClientConfig::default(); diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index df0ec28..3e94517 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -7,6 +7,7 @@ mod audio_client; mod config; mod remote_client; +use std::collections::HashMap; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; use std::ptr::NonNull; @@ -277,21 +278,23 @@ async fn async_init() -> DynResult { let (stream_info_tx, stream_info_rx) = watch::channel::>(None); let (decode_tx, _) = broadcast::channel::(256); - let audio_addr = format!("{}:{}", remote_host, cfg.frontends.audio.server_port); - frontend_runtime.audio_rx = Some(rx_audio_tx.clone()); frontend_runtime.audio_tx = Some(tx_audio_tx); frontend_runtime.audio_info = Some(stream_info_rx); frontend_runtime.decode_rx = Some(decode_tx.clone()); info!( - "Audio enabled: connecting to {}, decode channel set", - audio_addr + "Audio enabled: default port {}, decode channel set", + cfg.frontends.audio.server_port ); + let audio_rig_ports: HashMap = cfg.frontends.audio.rig_ports.clone(); let audio_shutdown_rx = shutdown_rx.clone(); task_handles.push(tokio::spawn(audio_client::run_audio_client( - audio_addr, + remote_host, + cfg.frontends.audio.server_port, + audio_rig_ports, + frontend_runtime.remote_active_rig_id.clone(), rx_audio_tx, tx_audio_rx, stream_info_tx, diff --git a/trx-client.toml.example b/trx-client.toml.example index e8a5435..0e1ae8d 100644 --- a/trx-client.toml.example +++ b/trx-client.toml.example @@ -85,6 +85,9 @@ port = 0 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 [frontends.audio.bridge] # Enable local cpal bridge for WSJT-X virtual audio routing