[feat](trx-client): per-rig meter supervisor with auto-reconnect

Adds rig_meters: map of per-rig watch::Sender<Option<MeterUpdate>> to
RigRoutingContext with a lazy rig_meter_rx helper. run_meter_supervisor
polls for known short names and spawns one SubscribeMeter TCP connection
per rig; reconnect loop sets TCP_NODELAY and pushes samples into the
per-rig watch so slow SSE readers automatically skip intermediate frames.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-04-19 19:50:14 +02:00
parent b12d93fb3c
commit fd0f1e43c0
4 changed files with 218 additions and 1 deletions
+1
View File
@@ -12,4 +12,5 @@ bytes = "1"
uuid = { workspace = true }
serde_json = { workspace = true }
trx-core = { path = "../../trx-core" }
trx-protocol = { path = "../../trx-protocol" }
tokio = { workspace = true, features = ["sync"] }
+25
View File
@@ -22,6 +22,7 @@ use trx_core::decode::{
};
use trx_core::rig::state::{RigSnapshot, SpectrumData};
use trx_core::{DynResult, RigRequest, RigState};
use trx_protocol::MeterUpdate;
/// Shared, timestamped decode history for a single decoder type.
///
@@ -320,6 +321,10 @@ pub struct RigRoutingContext {
pub server_connected: Arc<AtomicBool>,
/// Per-rig server connection state.
pub rig_server_connected: Arc<RwLock<HashMap<String, bool>>>,
/// Per-rig meter watch channels, keyed by rig_id. Populated lazily by
/// the meter-connection supervisor in `trx-client`; `None` on the sender
/// side means "no sample yet".
pub rig_meters: Arc<RwLock<HashMap<String, watch::Sender<Option<MeterUpdate>>>>>,
}
impl Default for RigRoutingContext {
@@ -331,6 +336,7 @@ impl Default for RigRoutingContext {
rig_states: Arc::new(RwLock::new(HashMap::new())),
server_connected: Arc::new(AtomicBool::new(false)),
rig_server_connected: Arc::new(RwLock::new(HashMap::new())),
rig_meters: Arc::new(RwLock::new(HashMap::new())),
}
}
}
@@ -447,6 +453,25 @@ impl FrontendRuntimeContext {
.and_then(|map| map.get(rig_id).map(|tx| tx.subscribe()))
}
/// Get a watch receiver for a specific rig's meter stream.
/// Lazily inserts a new channel if the rig_id is not yet present so
/// SSE clients can subscribe before the meter-connection supervisor
/// has produced a first sample.
pub fn rig_meter_rx(&self, rig_id: &str) -> watch::Receiver<Option<MeterUpdate>> {
if let Ok(map) = self.routing.rig_meters.read() {
if let Some(tx) = map.get(rig_id) {
return tx.subscribe();
}
}
if let Ok(mut map) = self.routing.rig_meters.write() {
map.entry(rig_id.to_string())
.or_insert_with(|| watch::channel(None).0)
.subscribe()
} else {
watch::channel(None).1
}
}
/// Get a watch receiver for a specific rig's spectrum.
/// Lazily inserts a new channel if the rig_id is not yet present.
pub fn rig_spectrum_rx(&self, rig_id: &str) -> watch::Receiver<SharedSpectrum> {