feat(trx-client): add remote rig_id selection via config and CLI

This commit is contained in:
2026-02-25 22:31:59 +01:00
parent 49118b65a1
commit 30ad6d1bb4
4 changed files with 58 additions and 10 deletions
+22 -2
View File
@@ -55,6 +55,8 @@ impl Default for GeneralConfig {
pub struct RemoteConfig {
/// Remote URL (host:port or tcp://host:port).
pub url: Option<String>,
/// Optional target rig ID on the remote multi-rig server.
pub rig_id: Option<String>,
/// 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();
+10 -3
View File
@@ -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<u64>,
/// Target rig ID on a multi-rig remote server
#[arg(long = "rig-id")]
rig_id: Option<String>,
/// Frontend(s) to expose locally (e.g. http,rigctl)
#[arg(short = 'f', long = "frontend", value_delimiter = ',', num_args = 1..)]
frontends: Option<Vec<String>>,
@@ -162,8 +165,10 @@ async fn async_init() -> DynResult<AppState> {
// 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<AppState> {
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<AppState> {
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();
+24 -5
View File
@@ -40,6 +40,7 @@ impl RemoteEndpoint {
pub struct RemoteClientConfig {
pub addr: String,
pub token: Option<String>,
pub rig_id: Option<String>,
pub poll_interval: Duration,
}
@@ -144,11 +145,7 @@ async fn send_command(
cmd: ClientCommand,
state_tx: &watch::Sender<RigState>,
) -> RigResult<trx_core::RigSnapshot> {
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<R: AsyncBufRead + Unpin>(
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"));
}
}
+2
View File
@@ -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