From 55c70f0fb7b385e52f0f43370f9aa17a8c1ab50f Mon Sep 17 00:00:00 2001 From: Stanislaw Grams Date: Fri, 13 Feb 2026 01:33:54 +0100 Subject: [PATCH] [feat](trx-rs): expose rigctl metadata in HTTP about tab Add rigctl frontend visibility in HTTP status/about UI and refine frequency controls layout.\n\n- track rigctl listen endpoint and active rigctl client count in frontend runtime context\n- inject rigctl metadata into HTTP /events payload\n- show rigctl endpoint and rigctl client count in About tab\n- remove frequency Set button from UI\n- move MHz/kHz/Hz selector beside frequency input and enlarge it\n- center jog wheel row and keep Enter-to-set frequency behavior\n\nCo-authored-by: OpenAI Codex Signed-off-by: Stanislaw Grams --- src/trx-client/src/main.rs | 5 ++ src/trx-client/trx-frontend/src/lib.rs | 8 ++- .../trx-frontend-http/assets/web/app.js | 20 ++++--- .../trx-frontend-http/assets/web/index.html | 15 ++--- .../trx-frontend-http/assets/web/style.css | 11 ++-- .../trx-frontend/trx-frontend-http/src/api.rs | 59 +++++++++++++++---- .../trx-frontend-rigctl/src/server.rs | 13 +++- 7 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/trx-client/src/main.rs b/src/trx-client/src/main.rs index f565180..f557d15 100644 --- a/src/trx-client/src/main.rs +++ b/src/trx-client/src/main.rs @@ -314,6 +314,11 @@ async fn async_init() -> DynResult { return Err(format!("Frontend missing listen configuration: {}", other).into()); } }; + if frontend == "rigctl" { + if let Ok(mut listen_addr) = frontend_runtime_ctx.rigctl_listen_addr.lock() { + *listen_addr = Some(addr); + } + } frontend_reg_ctx.spawn_frontend( frontend, frontend_state_rx, diff --git a/src/trx-client/trx-frontend/src/lib.rs b/src/trx-client/trx-frontend/src/lib.rs index 3936d40..585a7bc 100644 --- a/src/trx-client/trx-frontend/src/lib.rs +++ b/src/trx-client/trx-frontend/src/lib.rs @@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::net::SocketAddr; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, AtomicUsize}; use std::sync::{Arc, Mutex}; use std::time::Instant; @@ -120,6 +120,10 @@ pub struct FrontendRuntimeContext { pub wspr_history: Arc>>, /// Authentication tokens for HTTP-JSON frontend pub auth_tokens: HashSet, + /// Active rigctl TCP clients. + pub rigctl_clients: Arc, + /// rigctl listen endpoint, if enabled. + pub rigctl_listen_addr: Arc>>, /// Guard to avoid spawning duplicate decode collectors. pub decode_collector_started: AtomicBool, } @@ -137,6 +141,8 @@ impl FrontendRuntimeContext { ft8_history: Arc::new(Mutex::new(VecDeque::new())), wspr_history: Arc::new(Mutex::new(VecDeque::new())), auth_tokens: HashSet::new(), + rigctl_clients: Arc::new(AtomicUsize::new(0)), + rigctl_listen_addr: Arc::new(Mutex::new(None)), decode_collector_started: AtomicBool::new(false), } } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 40bd54b..cc47099 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -19,7 +19,6 @@ const vfoPicker = document.getElementById("vfo-picker"); const signalBar = document.getElementById("signal-bar"); const signalValue = document.getElementById("signal-value"); const pttBtn = document.getElementById("ptt-btn"); -const freqBtn = document.getElementById("freq-apply"); const modeBtn = document.getElementById("mode-apply"); const txLimitInput = document.getElementById("tx-limit"); const txLimitBtn = document.getElementById("tx-limit-btn"); @@ -229,7 +228,7 @@ function formatSignal(sUnits) { } function setDisabled(disabled) { - [freqEl, modeEl, freqBtn, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => { + [freqEl, modeEl, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => { if (el) el.disabled = disabled; }); } @@ -494,6 +493,12 @@ function render(update) { if (typeof update.clients === "number") { document.getElementById("about-clients").textContent = update.clients; } + if (typeof update.rigctl_clients === "number") { + document.getElementById("about-rigctl-clients").textContent = update.rigctl_clients; + } + if (typeof update.rigctl_addr === "string" && update.rigctl_addr.length > 0) { + document.getElementById("about-rigctl-endpoint").textContent = update.rigctl_addr; + } powerHint.textContent = readyText(); lastLocked = update.status && update.status.lock === true; lockBtn.textContent = lastLocked ? "Unlock" : "Lock"; @@ -621,7 +626,7 @@ pttBtn.addEventListener("click", async () => { } }); -freqBtn.addEventListener("click", async () => { +async function applyFreqFromInput() { const parsedRaw = parseFreqInput(freqEl.value, jogStep); const parsed = alignFreqToRigStep(parsedRaw); if (parsed === null) { @@ -633,7 +638,7 @@ freqBtn.addEventListener("click", async () => { return; } freqDirty = false; - freqBtn.disabled = true; + freqEl.disabled = true; showHint("Setting frequency…"); try { await postPath(`/set_freq?hz=${parsed}`); @@ -642,14 +647,15 @@ freqBtn.addEventListener("click", async () => { showHint("Set freq failed", 2000); console.error(err); } finally { - freqBtn.disabled = false; + freqEl.disabled = false; } -}); +} + freqEl.addEventListener("keydown", (e) => { freqDirty = true; if (e.key === "Enter") { e.preventDefault(); - freqBtn.click(); + applyFreqFromInput(); } }); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index e569ec2..6f1a3d1 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -33,9 +33,13 @@
Frequency--
-
+
- +
+ + + +
@@ -43,11 +47,6 @@
-
- - - -
@@ -224,6 +223,8 @@ Rig connection-- Supported modes-- VFOs-- + Rigctl endpoint-- + Rigctl clients-- PSK Reporter-- Client{pkg} v{ver} Connected clients-- diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 1d4a3c7..999017c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -35,6 +35,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem; .jog-container { display: flex; align-items: center; + justify-content: center; gap: 0.5rem; margin-top: 0.6rem; } @@ -80,15 +81,16 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem; border: 1px solid var(--border-light); border-radius: 6px; overflow: hidden; - margin-left: 0.3rem; + height: 3.35rem; + flex-shrink: 0; } .jog-step button { border: none; border-right: 1px solid var(--border-light); border-radius: 0; - height: 2rem; - padding: 0 0.55rem; - font-size: 0.78rem; + height: 100%; + padding: 0 0.8rem; + font-size: 0.92rem; background: var(--input-bg); color: var(--text-muted); cursor: pointer; @@ -131,6 +133,7 @@ button { padding: 0.5rem 0.9rem; border-radius: 6px; border: 1px solid var(--btn button:disabled { opacity: 0.6; cursor: not-allowed; } .hint { color: var(--text-muted); font-size: 0.85rem; } .inline { display: flex; gap: 0.5rem; align-items: center; } +.freq-inline #freq { flex: 1 1 auto; } small { color: var(--text-muted); } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; } .title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; } 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 2acb02b..eb998be 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 @@ -37,23 +37,46 @@ pub async fn status_api( } /// Inject `"clients": N` into a JSON object string. -fn inject_clients(json: &str, count: usize) -> String { - // Fast path: insert after the opening '{'. - if let Some(pos) = json.find('{') { - let mut out = String::with_capacity(json.len() + 20); - out.push_str(&json[..=pos]); - out.push_str(&format!("\"clients\":{count},")); - out.push_str(&json[pos + 1..]); - out - } else { - json.to_string() +fn inject_frontend_meta( + json: &str, + http_clients: usize, + rigctl_clients: usize, + rigctl_addr: Option, +) -> String { + let mut value: serde_json::Value = match serde_json::from_str(json) { + Ok(v) => v, + Err(_) => return json.to_string(), + }; + + let Some(map) = value.as_object_mut() else { + return json.to_string(); + }; + map.insert("clients".to_string(), serde_json::json!(http_clients)); + map.insert( + "rigctl_clients".to_string(), + serde_json::json!(rigctl_clients), + ); + if let Some(addr) = rigctl_addr { + map.insert("rigctl_addr".to_string(), serde_json::json!(addr)); } + + serde_json::to_string(&value).unwrap_or_else(|_| json.to_string()) +} + +fn rigctl_addr_from_context(context: &FrontendRuntimeContext) -> Option { + context + .rigctl_listen_addr + .lock() + .ok() + .and_then(|v| *v) + .map(|addr| addr.to_string()) } #[get("/events")] pub async fn events( state: web::Data>, clients: web::Data>, + context: web::Data>, ) -> Result { let rx = state.get_ref().clone(); let initial = wait_for_view(rx.clone()).await?; @@ -63,17 +86,29 @@ pub async fn events( let initial_json = serde_json::to_string(&initial).map_err(actix_web::error::ErrorInternalServerError)?; - let initial_json = inject_clients(&initial_json, count); + let initial_json = inject_frontend_meta( + &initial_json, + count, + context.rigctl_clients.load(Ordering::Relaxed), + rigctl_addr_from_context(context.get_ref().as_ref()), + ); let initial_stream = once(async move { Ok::(Bytes::from(format!("data: {initial_json}\n\n"))) }); let counter_updates = counter.clone(); + let context_updates = context.get_ref().clone(); let updates = WatchStream::new(rx).filter_map(move |state| { let counter = counter_updates.clone(); + let context = context_updates.clone(); async move { state.snapshot().and_then(|v| { serde_json::to_string(&v).ok().map(|json| { - let json = inject_clients(&json, counter.load(Ordering::Relaxed)); + let json = inject_frontend_meta( + &json, + counter.load(Ordering::Relaxed), + context.rigctl_clients.load(Ordering::Relaxed), + rigctl_addr_from_context(context.as_ref()), + ); Ok::(Bytes::from(format!("data: {json}\n\n"))) }) }) 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 484e397..3d8af41 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 @@ -3,6 +3,7 @@ // SPDX-License-Identifier: BSD-2-Clause use std::net::SocketAddr; +use std::sync::Arc; use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -10,6 +11,7 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{mpsc, oneshot, watch}; use tokio::task::JoinHandle; use tokio::time::timeout; +use std::sync::atomic::Ordering; use tracing::{debug, error, info, warn}; use trx_protocol::{mode_to_string, parse_mode}; @@ -31,10 +33,10 @@ impl FrontendSpawner for RigctlFrontend { rig_tx: mpsc::Sender, _callsign: Option, listen_addr: SocketAddr, - _context: std::sync::Arc, + context: Arc, ) -> JoinHandle<()> { tokio::spawn(async move { - if let Err(e) = serve(listen_addr, state_rx, rig_tx).await { + if let Err(e) = serve(listen_addr, state_rx, rig_tx, context).await { error!("rigctl server error: {:?}", e); } }) @@ -45,7 +47,11 @@ async fn serve( listen_addr: SocketAddr, state_rx: watch::Receiver, rig_tx: mpsc::Sender, + context: Arc, ) -> std::io::Result<()> { + if let Ok(mut slot) = context.rigctl_listen_addr.lock() { + *slot = Some(listen_addr); + } let listener = TcpListener::bind(listen_addr).await?; info!("rigctl frontend listening on {}", listen_addr); info!("rigctl frontend ready (rigctld-compatible)"); @@ -55,10 +61,13 @@ async fn serve( info!("rigctl client connected: {}", addr); let state_rx = state_rx.clone(); let rig_tx = rig_tx.clone(); + let context = context.clone(); + context.rigctl_clients.fetch_add(1, Ordering::Relaxed); tokio::spawn(async move { if let Err(e) = handle_client(stream, addr, state_rx, rig_tx).await { warn!("rigctl client {} error: {:?}", addr, e); } + context.rigctl_clients.fetch_sub(1, Ordering::Relaxed); }); } }