fix(trx-client): route audio by selected rig with per-rig port map

This commit is contained in:
2026-02-25 23:23:48 +01:00
parent 8ffeba47df
commit b1c232f388
4 changed files with 95 additions and 6 deletions
+57 -1
View File
@@ -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<String, u16>,
selected_rig_id: Arc<Mutex<Option<String>>>,
rx_tx: broadcast::Sender<Bytes>,
mut tx_rx: mpsc::Receiver<Bytes>,
stream_info_tx: watch::Sender<Option<AudioStreamInfo>>,
@@ -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<String, u16>,
selected_rig_id: &Arc<Mutex<Option<String>>>,
connected_addr: &str,
rx_tx: &broadcast::Sender<Bytes>,
tx_rx: &mut mpsc::Receiver<Bytes>,
stream_info_tx: &watch::Sender<Option<AudioStreamInfo>>,
@@ -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<String, u16>,
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)
}
+27
View File
@@ -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<String, u16>,
/// 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();
+8 -5
View File
@@ -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<AppState> {
let (stream_info_tx, stream_info_rx) = watch::channel::<Option<AudioStreamInfo>>(None);
let (decode_tx, _) = broadcast::channel::<DecodedMessage>(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<String, u16> = 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,
+3
View File
@@ -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