[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 <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -314,6 +314,11 @@ async fn async_init() -> DynResult<AppState> {
|
|||||||
return Err(format!("Frontend missing listen configuration: {}", other).into());
|
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_reg_ctx.spawn_frontend(
|
||||||
frontend,
|
frontend,
|
||||||
frontend_state_rx,
|
frontend_state_rx,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -120,6 +120,10 @@ pub struct FrontendRuntimeContext {
|
|||||||
pub wspr_history: Arc<Mutex<VecDeque<(Instant, WsprMessage)>>>,
|
pub wspr_history: Arc<Mutex<VecDeque<(Instant, WsprMessage)>>>,
|
||||||
/// Authentication tokens for HTTP-JSON frontend
|
/// Authentication tokens for HTTP-JSON frontend
|
||||||
pub auth_tokens: HashSet<String>,
|
pub auth_tokens: HashSet<String>,
|
||||||
|
/// Active rigctl TCP clients.
|
||||||
|
pub rigctl_clients: Arc<AtomicUsize>,
|
||||||
|
/// rigctl listen endpoint, if enabled.
|
||||||
|
pub rigctl_listen_addr: Arc<Mutex<Option<SocketAddr>>>,
|
||||||
/// Guard to avoid spawning duplicate decode collectors.
|
/// Guard to avoid spawning duplicate decode collectors.
|
||||||
pub decode_collector_started: AtomicBool,
|
pub decode_collector_started: AtomicBool,
|
||||||
}
|
}
|
||||||
@@ -137,6 +141,8 @@ impl FrontendRuntimeContext {
|
|||||||
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
|
ft8_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
|
wspr_history: Arc::new(Mutex::new(VecDeque::new())),
|
||||||
auth_tokens: HashSet::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),
|
decode_collector_started: AtomicBool::new(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const vfoPicker = document.getElementById("vfo-picker");
|
|||||||
const signalBar = document.getElementById("signal-bar");
|
const signalBar = document.getElementById("signal-bar");
|
||||||
const signalValue = document.getElementById("signal-value");
|
const signalValue = document.getElementById("signal-value");
|
||||||
const pttBtn = document.getElementById("ptt-btn");
|
const pttBtn = document.getElementById("ptt-btn");
|
||||||
const freqBtn = document.getElementById("freq-apply");
|
|
||||||
const modeBtn = document.getElementById("mode-apply");
|
const modeBtn = document.getElementById("mode-apply");
|
||||||
const txLimitInput = document.getElementById("tx-limit");
|
const txLimitInput = document.getElementById("tx-limit");
|
||||||
const txLimitBtn = document.getElementById("tx-limit-btn");
|
const txLimitBtn = document.getElementById("tx-limit-btn");
|
||||||
@@ -229,7 +228,7 @@ function formatSignal(sUnits) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setDisabled(disabled) {
|
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;
|
if (el) el.disabled = disabled;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -494,6 +493,12 @@ function render(update) {
|
|||||||
if (typeof update.clients === "number") {
|
if (typeof update.clients === "number") {
|
||||||
document.getElementById("about-clients").textContent = update.clients;
|
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();
|
powerHint.textContent = readyText();
|
||||||
lastLocked = update.status && update.status.lock === true;
|
lastLocked = update.status && update.status.lock === true;
|
||||||
lockBtn.textContent = lastLocked ? "Unlock" : "Lock";
|
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 parsedRaw = parseFreqInput(freqEl.value, jogStep);
|
||||||
const parsed = alignFreqToRigStep(parsedRaw);
|
const parsed = alignFreqToRigStep(parsedRaw);
|
||||||
if (parsed === null) {
|
if (parsed === null) {
|
||||||
@@ -633,7 +638,7 @@ freqBtn.addEventListener("click", async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
freqDirty = false;
|
freqDirty = false;
|
||||||
freqBtn.disabled = true;
|
freqEl.disabled = true;
|
||||||
showHint("Setting frequency…");
|
showHint("Setting frequency…");
|
||||||
try {
|
try {
|
||||||
await postPath(`/set_freq?hz=${parsed}`);
|
await postPath(`/set_freq?hz=${parsed}`);
|
||||||
@@ -642,14 +647,15 @@ freqBtn.addEventListener("click", async () => {
|
|||||||
showHint("Set freq failed", 2000);
|
showHint("Set freq failed", 2000);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
freqBtn.disabled = false;
|
freqEl.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
freqEl.addEventListener("keydown", (e) => {
|
freqEl.addEventListener("keydown", (e) => {
|
||||||
freqDirty = true;
|
freqDirty = true;
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
freqBtn.click();
|
applyFreqFromInput();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,13 @@
|
|||||||
<div class="status">
|
<div class="status">
|
||||||
<div class="full-row">
|
<div class="full-row">
|
||||||
<div class="label">Frequency<span class="band-tag" id="band-label">--</span></div>
|
<div class="label">Frequency<span class="band-tag" id="band-label">--</span></div>
|
||||||
<div class="inline">
|
<div class="inline freq-inline">
|
||||||
<input class="status-input" id="freq" type="text" value="--" />
|
<input class="status-input" id="freq" type="text" value="--" />
|
||||||
<button id="freq-apply" type="button">Set</button>
|
<div class="jog-step" id="jog-step">
|
||||||
|
<button type="button" data-step="1000000">MHz</button>
|
||||||
|
<button type="button" data-step="1000" class="active">kHz</button>
|
||||||
|
<button type="button" data-step="1">Hz</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jog-container">
|
<div class="jog-container">
|
||||||
<button id="jog-down" type="button" class="jog-btn">−</button>
|
<button id="jog-down" type="button" class="jog-btn">−</button>
|
||||||
@@ -43,11 +47,6 @@
|
|||||||
<div class="jog-indicator" id="jog-indicator"></div>
|
<div class="jog-indicator" id="jog-indicator"></div>
|
||||||
</div>
|
</div>
|
||||||
<button id="jog-up" type="button" class="jog-btn">+</button>
|
<button id="jog-up" type="button" class="jog-btn">+</button>
|
||||||
<div class="jog-step" id="jog-step">
|
|
||||||
<button type="button" data-step="1000000">MHz</button>
|
|
||||||
<button type="button" data-step="1000" class="active">kHz</button>
|
|
||||||
<button type="button" data-step="1">Hz</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row full-row">
|
<div class="controls-row full-row">
|
||||||
@@ -224,6 +223,8 @@
|
|||||||
<tr><td>Rig connection</td><td id="about-rig-access">--</td></tr>
|
<tr><td>Rig connection</td><td id="about-rig-access">--</td></tr>
|
||||||
<tr><td>Supported modes</td><td id="about-modes">--</td></tr>
|
<tr><td>Supported modes</td><td id="about-modes">--</td></tr>
|
||||||
<tr><td>VFOs</td><td id="about-vfos">--</td></tr>
|
<tr><td>VFOs</td><td id="about-vfos">--</td></tr>
|
||||||
|
<tr><td>Rigctl endpoint</td><td id="about-rigctl-endpoint">--</td></tr>
|
||||||
|
<tr><td>Rigctl clients</td><td id="about-rigctl-clients">--</td></tr>
|
||||||
<tr><td>PSK Reporter</td><td id="about-pskreporter">--</td></tr>
|
<tr><td>PSK Reporter</td><td id="about-pskreporter">--</td></tr>
|
||||||
<tr><td>Client</td><td>{pkg} v{ver}</td></tr>
|
<tr><td>Client</td><td>{pkg} v{ver}</td></tr>
|
||||||
<tr><td>Connected clients</td><td id="about-clients">--</td></tr>
|
<tr><td>Connected clients</td><td id="about-clients">--</td></tr>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
.jog-container {
|
.jog-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.6rem;
|
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: 1px solid var(--border-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-left: 0.3rem;
|
height: 3.35rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.jog-step button {
|
.jog-step button {
|
||||||
border: none;
|
border: none;
|
||||||
border-right: 1px solid var(--border-light);
|
border-right: 1px solid var(--border-light);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
height: 2rem;
|
height: 100%;
|
||||||
padding: 0 0.55rem;
|
padding: 0 0.8rem;
|
||||||
font-size: 0.78rem;
|
font-size: 0.92rem;
|
||||||
background: var(--input-bg);
|
background: var(--input-bg);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
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; }
|
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
.hint { color: var(--text-muted); font-size: 0.85rem; }
|
.hint { color: var(--text-muted); font-size: 0.85rem; }
|
||||||
.inline { display: flex; gap: 0.5rem; align-items: center; }
|
.inline { display: flex; gap: 0.5rem; align-items: center; }
|
||||||
|
.freq-inline #freq { flex: 1 1 auto; }
|
||||||
small { color: var(--text-muted); }
|
small { color: var(--text-muted); }
|
||||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; }
|
.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; }
|
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
|
||||||
|
|||||||
@@ -37,23 +37,46 @@ pub async fn status_api(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Inject `"clients": N` into a JSON object string.
|
/// Inject `"clients": N` into a JSON object string.
|
||||||
fn inject_clients(json: &str, count: usize) -> String {
|
fn inject_frontend_meta(
|
||||||
// Fast path: insert after the opening '{'.
|
json: &str,
|
||||||
if let Some(pos) = json.find('{') {
|
http_clients: usize,
|
||||||
let mut out = String::with_capacity(json.len() + 20);
|
rigctl_clients: usize,
|
||||||
out.push_str(&json[..=pos]);
|
rigctl_addr: Option<String>,
|
||||||
out.push_str(&format!("\"clients\":{count},"));
|
) -> String {
|
||||||
out.push_str(&json[pos + 1..]);
|
let mut value: serde_json::Value = match serde_json::from_str(json) {
|
||||||
out
|
Ok(v) => v,
|
||||||
} else {
|
Err(_) => return json.to_string(),
|
||||||
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<String> {
|
||||||
|
context
|
||||||
|
.rigctl_listen_addr
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| *v)
|
||||||
|
.map(|addr| addr.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/events")]
|
#[get("/events")]
|
||||||
pub async fn events(
|
pub async fn events(
|
||||||
state: web::Data<watch::Receiver<RigState>>,
|
state: web::Data<watch::Receiver<RigState>>,
|
||||||
clients: web::Data<Arc<AtomicUsize>>,
|
clients: web::Data<Arc<AtomicUsize>>,
|
||||||
|
context: web::Data<Arc<FrontendRuntimeContext>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let rx = state.get_ref().clone();
|
let rx = state.get_ref().clone();
|
||||||
let initial = wait_for_view(rx.clone()).await?;
|
let initial = wait_for_view(rx.clone()).await?;
|
||||||
@@ -63,17 +86,29 @@ pub async fn events(
|
|||||||
|
|
||||||
let initial_json =
|
let initial_json =
|
||||||
serde_json::to_string(&initial).map_err(actix_web::error::ErrorInternalServerError)?;
|
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 =
|
let initial_stream =
|
||||||
once(async move { Ok::<Bytes, Error>(Bytes::from(format!("data: {initial_json}\n\n"))) });
|
once(async move { Ok::<Bytes, Error>(Bytes::from(format!("data: {initial_json}\n\n"))) });
|
||||||
|
|
||||||
let counter_updates = counter.clone();
|
let counter_updates = counter.clone();
|
||||||
|
let context_updates = context.get_ref().clone();
|
||||||
let updates = WatchStream::new(rx).filter_map(move |state| {
|
let updates = WatchStream::new(rx).filter_map(move |state| {
|
||||||
let counter = counter_updates.clone();
|
let counter = counter_updates.clone();
|
||||||
|
let context = context_updates.clone();
|
||||||
async move {
|
async move {
|
||||||
state.snapshot().and_then(|v| {
|
state.snapshot().and_then(|v| {
|
||||||
serde_json::to_string(&v).ok().map(|json| {
|
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, Error>(Bytes::from(format!("data: {json}\n\n")))
|
Ok::<Bytes, Error>(Bytes::from(format!("data: {json}\n\n")))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// SPDX-License-Identifier: BSD-2-Clause
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
@@ -10,6 +11,7 @@ use tokio::net::{TcpListener, TcpStream};
|
|||||||
use tokio::sync::{mpsc, oneshot, watch};
|
use tokio::sync::{mpsc, oneshot, watch};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use trx_protocol::{mode_to_string, parse_mode};
|
use trx_protocol::{mode_to_string, parse_mode};
|
||||||
|
|
||||||
@@ -31,10 +33,10 @@ impl FrontendSpawner for RigctlFrontend {
|
|||||||
rig_tx: mpsc::Sender<RigRequest>,
|
rig_tx: mpsc::Sender<RigRequest>,
|
||||||
_callsign: Option<String>,
|
_callsign: Option<String>,
|
||||||
listen_addr: SocketAddr,
|
listen_addr: SocketAddr,
|
||||||
_context: std::sync::Arc<trx_frontend::FrontendRuntimeContext>,
|
context: Arc<trx_frontend::FrontendRuntimeContext>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
tokio::spawn(async move {
|
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);
|
error!("rigctl server error: {:?}", e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -45,7 +47,11 @@ async fn serve(
|
|||||||
listen_addr: SocketAddr,
|
listen_addr: SocketAddr,
|
||||||
state_rx: watch::Receiver<RigState>,
|
state_rx: watch::Receiver<RigState>,
|
||||||
rig_tx: mpsc::Sender<RigRequest>,
|
rig_tx: mpsc::Sender<RigRequest>,
|
||||||
|
context: Arc<trx_frontend::FrontendRuntimeContext>,
|
||||||
) -> std::io::Result<()> {
|
) -> std::io::Result<()> {
|
||||||
|
if let Ok(mut slot) = context.rigctl_listen_addr.lock() {
|
||||||
|
*slot = Some(listen_addr);
|
||||||
|
}
|
||||||
let listener = TcpListener::bind(listen_addr).await?;
|
let listener = TcpListener::bind(listen_addr).await?;
|
||||||
info!("rigctl frontend listening on {}", listen_addr);
|
info!("rigctl frontend listening on {}", listen_addr);
|
||||||
info!("rigctl frontend ready (rigctld-compatible)");
|
info!("rigctl frontend ready (rigctld-compatible)");
|
||||||
@@ -55,10 +61,13 @@ async fn serve(
|
|||||||
info!("rigctl client connected: {}", addr);
|
info!("rigctl client connected: {}", addr);
|
||||||
let state_rx = state_rx.clone();
|
let state_rx = state_rx.clone();
|
||||||
let rig_tx = rig_tx.clone();
|
let rig_tx = rig_tx.clone();
|
||||||
|
let context = context.clone();
|
||||||
|
context.rigctl_clients.fetch_add(1, Ordering::Relaxed);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = handle_client(stream, addr, state_rx, rig_tx).await {
|
if let Err(e) = handle_client(stream, addr, state_rx, rig_tx).await {
|
||||||
warn!("rigctl client {} error: {:?}", addr, e);
|
warn!("rigctl client {} error: {:?}", addr, e);
|
||||||
}
|
}
|
||||||
|
context.rigctl_clients.fetch_sub(1, Ordering::Relaxed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user