diff --git a/README.md b/README.md index 2574a72..f542d4d 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,15 @@ Configuration reference: see `CONFIGURATION.md` for all server/client options an ## Configuration Files -`trx-server` and `trx-client` now support a shared `trx-rs.toml` as well as the legacy per-binary files. +`trx-server` and `trx-client` read configuration from a shared `trx-rs.toml`. - Default search order for each app: current directory, then `~/.config/trx-rs`, then `/etc/trx-rs` - At each location, the loader checks: - `trx-rs.toml` first (`[trx-server]` or `[trx-client]` section), then the legacy flat file -- Combined file names: + `trx-rs.toml` and reads the `[trx-server]` or `[trx-client]` section +- Config file name: `trx-rs.toml` -- Legacy flat file names: - `trx-server.toml` in the current directory, `server.toml` under XDG/`/etc` - `trx-client.toml` in the current directory, `client.toml` under XDG/`/etc` -- `--config ` still loads an explicit file path. If that file contains a `[trx-server]` or `[trx-client]` section, only that section is used; otherwise the whole file is parsed as the legacy flat format. +- `--config ` 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. @@ -46,12 +43,11 @@ The HTTP frontend supports optional passphrase-based authentication with two rol - **rx**: Read-only access to status, events, decode history, and audio streams - **control**: Full access including transmit control (TX/PTT) and power toggling -Authentication is disabled by default for backward compatibility. 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. +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. ### Configuration -In a combined `trx-rs.toml`, enable authentication under `[trx-client.frontends.http.auth]`. -If you use a legacy `trx-client.toml`, use the same keys under `[frontends.http.auth]`. +Enable authentication under `[trx-client.frontends.http.auth]` in `trx-rs.toml`. ```toml [trx-client.frontends.http.auth] diff --git a/src/trx-client/src/config.rs b/src/trx-client/src/config.rs index 723323c..086eca2 100644 --- a/src/trx-client/src/config.rs +++ b/src/trx-client/src/config.rs @@ -247,8 +247,12 @@ pub struct RigctlFrontendConfig { pub enabled: bool, /// Listen address pub listen: IpAddr, - /// Listen port + /// Listen port (used for single-rig setups or as the fallback base port) pub port: u16, + /// Per-rig port overrides for multi-rig servers. + /// Maps rig ID → local rigctl port. When non-empty, one rigctl listener + /// is spawned per entry, each routing commands to its assigned rig. + pub rig_ports: HashMap, } impl Default for RigctlFrontendConfig { @@ -257,6 +261,7 @@ impl Default for RigctlFrontendConfig { enabled: false, listen: IpAddr::from([127, 0, 0, 1]), port: 4532, + rig_ports: HashMap::new(), } } } @@ -408,6 +413,7 @@ impl ClientConfig { enabled: false, listen: IpAddr::from([127, 0, 0, 1]), port: 4532, + rig_ports: HashMap::new(), }, http_json: HttpJsonFrontendConfig::default(), audio: AudioClientConfig::default(), diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index f0ee36e..7239605 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -336,6 +336,46 @@ async fn async_init() -> DynResult { // Spawn frontends with runtime context for frontend in &frontends { let frontend_state_rx = state_rx.clone(); + + // rigctl with per-rig port mapping: spawn one listener per rig entry. + if frontend == "rigctl" && !cfg.frontends.rigctl.rig_ports.is_empty() { + let mut first = true; + for (rig_id, &port) in &cfg.frontends.rigctl.rig_ports { + let addr = SocketAddr::from((rigctl_listen, port)); + if first { + if let Ok(mut listen_addr) = frontend_runtime_ctx.rigctl_listen_addr.lock() { + *listen_addr = Some(addr); + } + first = false; + } + // Proxy channel: inject rig_id_override before forwarding to main tx. + let (proxy_tx, mut proxy_rx) = + mpsc::channel::(RIG_TASK_CHANNEL_BUFFER); + let main_tx = tx.clone(); + let rig_id_owned = rig_id.clone(); + tokio::spawn(async move { + while let Some(req) = proxy_rx.recv().await { + let forwarded = RigRequest { + cmd: req.cmd, + respond_to: req.respond_to, + rig_id_override: Some(rig_id_owned.clone()), + }; + let _ = main_tx.send(forwarded).await; + } + }); + info!("rigctl frontend for rig '{}' on {}", rig_id, addr); + frontend_reg_ctx.spawn_frontend( + frontend, + state_rx.clone(), + proxy_tx, + callsign.clone(), + addr, + frontend_runtime_ctx.clone(), + )?; + } + continue; + } + let addr = match frontend.as_str() { "http" => SocketAddr::from((http_listen, http_port)), "rigctl" => SocketAddr::from((rigctl_listen, rigctl_port)), diff --git a/src/trx-client/src/remote_client.rs b/src/trx-client/src/remote_client.rs index 0ad0a99..2c2258d 100644 --- a/src/trx-client/src/remote_client.rs +++ b/src/trx-client/src/remote_client.rs @@ -167,10 +167,11 @@ async fn handle_connection( let Some(req) = req else { return Ok(()); }; + let rig_id_override = req.rig_id_override; let cmd = req.cmd; let result = { let client_cmd = rig_command_to_client(cmd); - send_command(config, &mut writer, &mut reader, client_cmd, state_tx).await + send_command(config, &mut writer, &mut reader, client_cmd, rig_id_override, state_tx).await }; let _ = req.respond_to.send(result); @@ -184,9 +185,10 @@ async fn send_command( writer: &mut tokio::net::tcp::OwnedWriteHalf, reader: &mut BufReader, cmd: ClientCommand, + rig_id_override: Option, state_tx: &watch::Sender, ) -> RigResult { - let envelope = build_envelope(config, cmd); + let envelope = build_envelope(config, cmd, rig_id_override); let payload = serde_json::to_string(&envelope) .map_err(|e| RigError::communication(format!("JSON serialize failed: {e}")))?; @@ -233,7 +235,7 @@ async fn send_command_no_state_update( reader: &mut BufReader, cmd: ClientCommand, ) -> RigResult { - let envelope = build_envelope(config, cmd); + let envelope = build_envelope(config, cmd, None); let payload = serde_json::to_string(&envelope) .map_err(|e| RigError::communication(format!("JSON serialize failed: {e}")))?; time::timeout( @@ -265,10 +267,14 @@ async fn send_command_no_state_update( )) } -fn build_envelope(config: &RemoteClientConfig, cmd: ClientCommand) -> ClientEnvelope { +fn build_envelope( + config: &RemoteClientConfig, + cmd: ClientCommand, + rig_id_override: Option, +) -> ClientEnvelope { ClientEnvelope { token: config.token.clone(), - rig_id: selected_rig_id(config), + rig_id: rig_id_override.or_else(|| selected_rig_id(config)), cmd, } } @@ -305,7 +311,7 @@ async fn send_get_rigs( writer: &mut tokio::net::tcp::OwnedWriteHalf, reader: &mut BufReader, ) -> RigResult> { - let envelope = build_envelope(config, ClientCommand::GetRigs); + let envelope = build_envelope(config, ClientCommand::GetRigs, None); let payload = serde_json::to_string(&envelope) .map_err(|e| RigError::communication(format!("JSON serialize failed: {e}")))?; @@ -712,7 +718,7 @@ mod tests { poll_interval: Duration::from_millis(500), spectrum: Arc::new(Mutex::new(SharedSpectrum::default())), }; - let envelope = super::build_envelope(&config, trx_protocol::ClientCommand::GetState); + let envelope = super::build_envelope(&config, trx_protocol::ClientCommand::GetState, None); assert_eq!(envelope.token.as_deref(), Some("secret")); assert_eq!(envelope.rig_id.as_deref(), Some("sdr")); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs index d6fd6aa..5b43f08 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http-json/src/server.rs @@ -156,6 +156,7 @@ async fn handle_client( let req = RigRequest { cmd: rig_cmd, respond_to: resp_tx, + rig_id_override: None, }; match time::timeout(IO_TIMEOUT, tx.send(req)).await { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 98e525e..b7c568c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -812,6 +812,7 @@ async fn send_command( .send(RigRequest { cmd, respond_to: resp_tx, + rig_id_override: None, }) .await .map_err(|e| { diff --git a/src/trx-client/trx-frontend/trx-frontend-rigctl/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-rigctl/src/server.rs index 001ba62..23a2d10 100644 --- a/src/trx-client/trx-frontend/trx-frontend-rigctl/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-rigctl/src/server.rs @@ -337,6 +337,7 @@ async fn send_rig_command( .send(RigRequest { cmd, respond_to: resp_tx, + rig_id_override: None, }) .await .map_err(|e| format!("failed to send to rig: {e:?}"))?; diff --git a/src/trx-core/src/rig/request.rs b/src/trx-core/src/rig/request.rs index 0dd1ae0..1212409 100644 --- a/src/trx-core/src/rig/request.rs +++ b/src/trx-core/src/rig/request.rs @@ -11,4 +11,7 @@ use crate::{RigCommand, RigResult, RigSnapshot}; pub struct RigRequest { pub cmd: RigCommand, pub respond_to: oneshot::Sender>, + /// When set, the remote client routes this request to the specified rig + /// instead of the globally selected rig. Used for per-rig rigctl listeners. + pub rig_id_override: Option, } diff --git a/src/trx-server/src/listener.rs b/src/trx-server/src/listener.rs index 8bd983c..72279ee 100644 --- a/src/trx-server/src/listener.rs +++ b/src/trx-server/src/listener.rs @@ -300,6 +300,7 @@ async fn handle_client( let req = RigRequest { cmd: rig_cmd, respond_to: resp_tx, + rig_id_override: None, }; match time::timeout(IO_TIMEOUT, handle.rig_tx.send(req)).await { diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 6c427d4..bd4bd6e 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -316,7 +316,7 @@ pub async fn run_rig_task( } // Process each request - while let Some(RigRequest { cmd, respond_to }) = batch.pop() { + while let Some(RigRequest { cmd, respond_to, .. }) = batch.pop() { let cmd_label = format!("{:?}", cmd); let log_command = !matches!(&cmd, RigCommand::GetSpectrum); let started = Instant::now();