diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index d3498c9..22ec057 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -55,6 +55,8 @@ impl Default for GeneralConfig { pub struct RemoteConfig { /// Remote URL (host:port or tcp://host:port). pub url: Option, + /// Optional target rig ID on the remote multi-rig server. + pub rig_id: Option, /// Remote auth settings. pub auth: RemoteAuthConfig, /// Poll interval in milliseconds. @@ -65,6 +67,7 @@ impl Default for RemoteConfig { fn default() -> Self { Self { url: None, + rig_id: None, auth: RemoteAuthConfig::default(), poll_interval_ms: 750, } @@ -300,6 +303,11 @@ impl ClientConfig { return Err("[remote].url must not be empty when set".to_string()); } } + if let Some(rig_id) = &self.remote.rig_id { + if rig_id.trim().is_empty() { + return Err("[remote].rig_id must not be empty when set".to_string()); + } + } if let Some(token) = &self.remote.auth.token { if token.trim().is_empty() { return Err("[remote.auth].token must not be empty when set".to_string()); @@ -315,11 +323,13 @@ 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()); } - if !self.frontends.audio.bridge.rx_gain.is_finite() || self.frontends.audio.bridge.rx_gain < 0.0 + if !self.frontends.audio.bridge.rx_gain.is_finite() + || self.frontends.audio.bridge.rx_gain < 0.0 { return Err("[frontends.audio.bridge].rx_gain must be finite and >= 0".to_string()); } - if !self.frontends.audio.bridge.tx_gain.is_finite() || self.frontends.audio.bridge.tx_gain < 0.0 + if !self.frontends.audio.bridge.tx_gain.is_finite() + || self.frontends.audio.bridge.tx_gain < 0.0 { return Err("[frontends.audio.bridge].tx_gain must be finite and >= 0".to_string()); } @@ -353,6 +363,7 @@ impl ClientConfig { }, remote: RemoteConfig { url: Some("192.168.1.100:9000".to_string()), + rig_id: Some("hf".to_string()), auth: RemoteAuthConfig { token: Some("my-token".to_string()), }, @@ -494,6 +505,7 @@ callsign = "W1AW" [remote] url = "192.168.1.100:9000" +rig_id = "hf" auth.token = "my-token" poll_interval_ms = 500 @@ -507,6 +519,7 @@ port = 8080 let config: ClientConfig = toml::from_str(toml_str).unwrap(); assert_eq!(config.general.callsign, Some("W1AW".to_string())); assert_eq!(config.remote.url, Some("192.168.1.100:9000".to_string())); + assert_eq!(config.remote.rig_id, Some("hf".to_string())); assert_eq!(config.remote.auth.token, Some("my-token".to_string())); assert_eq!(config.remote.poll_interval_ms, 500); assert!(config.frontends.http.enabled); @@ -525,6 +538,13 @@ port = 8080 assert!(config.validate().is_err()); } + #[test] + fn test_validate_rejects_empty_remote_rig_id() { + let mut config = ClientConfig::default(); + config.remote.rig_id = Some(" ".to_string()); + assert!(config.validate().is_err()); + } + #[test] fn test_validate_rejects_empty_http_json_token() { let mut config = ClientConfig::default(); diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index 88cc3f5..d34d613 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: BSD-2-Clause -mod audio_client; mod audio_bridge; +mod audio_client; mod config; mod remote_client; @@ -58,6 +58,9 @@ struct Cli { /// Poll interval in milliseconds #[arg(long = "poll-interval")] poll_interval_ms: Option, + /// Target rig ID on a multi-rig remote server + #[arg(long = "rig-id")] + rig_id: Option, /// Frontend(s) to expose locally (e.g. http,rigctl) #[arg(short = 'f', long = "frontend", value_delimiter = ',', num_args = 1..)] frontends: Option>, @@ -162,8 +165,10 @@ async fn async_init() -> DynResult { // Set HTTP frontend authentication config frontend_runtime.http_auth_enabled = cfg.frontends.http.auth.enabled; frontend_runtime.http_auth_rx_passphrase = cfg.frontends.http.auth.rx_passphrase.clone(); - frontend_runtime.http_auth_control_passphrase = cfg.frontends.http.auth.control_passphrase.clone(); - frontend_runtime.http_auth_tx_access_control_enabled = cfg.frontends.http.auth.tx_access_control_enabled; + frontend_runtime.http_auth_control_passphrase = + cfg.frontends.http.auth.control_passphrase.clone(); + frontend_runtime.http_auth_tx_access_control_enabled = + cfg.frontends.http.auth.tx_access_control_enabled; frontend_runtime.http_auth_session_ttl_secs = cfg.frontends.http.auth.session_ttl_min * 60; frontend_runtime.http_auth_cookie_secure = cfg.frontends.http.auth.cookie_secure; frontend_runtime.http_auth_cookie_same_site = match cfg.frontends.http.auth.cookie_same_site { @@ -183,6 +188,7 @@ async fn async_init() -> DynResult { parse_remote_url(&remote_url).map_err(|e| format!("Invalid remote URL: {}", e))?; let remote_token = cli.token.clone().or_else(|| cfg.remote.auth.token.clone()); + let remote_rig_id = cli.rig_id.clone().or_else(|| cfg.remote.rig_id.clone()); let poll_interval_ms = cli.poll_interval_ms.unwrap_or(cfg.remote.poll_interval_ms); @@ -248,6 +254,7 @@ async fn async_init() -> DynResult { let remote_cfg = RemoteClientConfig { addr: remote_endpoint.connect_addr(), token: remote_token, + rig_id: remote_rig_id, poll_interval: Duration::from_millis(poll_interval_ms), }; let remote_shutdown_rx = shutdown_rx.clone(); diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index 0227bec..5a569a7 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -40,6 +40,7 @@ impl RemoteEndpoint { pub struct RemoteClientConfig { pub addr: String, pub token: Option, + pub rig_id: Option, pub poll_interval: Duration, } @@ -144,11 +145,7 @@ async fn send_command( cmd: ClientCommand, state_tx: &watch::Sender, ) -> RigResult { - let envelope = ClientEnvelope { - token: config.token.clone(), - rig_id: None, - cmd, - }; + let envelope = build_envelope(config, cmd); let payload = serde_json::to_string(&envelope) .map_err(|e| RigError::communication(format!("JSON serialize failed: {e}")))?; @@ -187,6 +184,14 @@ async fn send_command( )) } +fn build_envelope(config: &RemoteClientConfig, cmd: ClientCommand) -> ClientEnvelope { + ClientEnvelope { + token: config.token.clone(), + rig_id: config.rig_id.clone(), + cmd, + } +} + async fn read_limited_line( reader: &mut R, max_bytes: usize, @@ -476,6 +481,7 @@ mod tests { RemoteClientConfig { addr: addr.to_string(), token: None, + rig_id: None, poll_interval: Duration::from_millis(100), }, req_rx, @@ -503,4 +509,17 @@ mod tests { .expect("client shutdown timeout"); let _ = server.await; } + + #[test] + fn build_envelope_includes_rig_id() { + let config = RemoteClientConfig { + addr: "127.0.0.1:4530".to_string(), + token: Some("secret".to_string()), + rig_id: Some("sdr".to_string()), + poll_interval: Duration::from_millis(500), + }; + let envelope = super::build_envelope(&config, trx_protocol::ClientCommand::GetState); + assert_eq!(envelope.token.as_deref(), Some("secret")); + assert_eq!(envelope.rig_id.as_deref(), Some("sdr")); + } } diff --git a/trx-client.toml.example b/trx-client.toml.example index 91e89c1..e8a5435 100644 --- a/trx-client.toml.example +++ b/trx-client.toml.example @@ -19,6 +19,8 @@ callsign = "N0CALL" [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 poll_interval_ms = 750