use std::collections::HashMap;
use tokio::sync::{Mutex, mpsc};
use tracing::debug;
use spvirit_codec::spvirit_encode::{
encode_monitor_data_response_delta, encode_monitor_data_response_filtered,
encode_monitor_data_response_payload,
};
use spvirit_types::NtPayload;
use crate::state::MonitorSub;
pub struct MonitorRegistry {
pub monitors: Mutex<HashMap<String, Vec<MonitorSub>>>,
pub conns: Mutex<HashMap<u64, mpsc::Sender<Vec<u8>>>>,
}
impl MonitorRegistry {
pub fn new() -> Self {
Self {
monitors: Mutex::new(HashMap::new()),
conns: Mutex::new(HashMap::new()),
}
}
pub async fn send_msg(&self, conn_id: u64, msg: Vec<u8>) {
let conns = self.conns.lock().await;
if let Some(tx) = conns.get(&conn_id) {
let _ = tx.send(msg).await;
}
}
fn build_monitor_frame(sub: &MonitorSub, payload: &NtPayload) -> Option<Vec<u8>> {
let subcmd = 0x00;
let Some(prev) = sub.last_snapshot.as_ref() else {
let bytes = if let Some(ref desc) = sub.filtered_desc {
encode_monitor_data_response_filtered(
sub.ioid,
subcmd,
payload,
desc,
sub.version,
sub.is_be,
)
} else {
encode_monitor_data_response_payload(
sub.ioid,
subcmd,
payload,
sub.version,
sub.is_be,
)
};
return Some(bytes);
};
if let Some(ref desc) = sub.filtered_desc {
encode_monitor_data_response_delta(
sub.ioid,
subcmd,
prev,
payload,
desc,
sub.version,
sub.is_be,
)
} else if prev == payload {
None
} else {
Some(encode_monitor_data_response_payload(
sub.ioid,
subcmd,
payload,
sub.version,
sub.is_be,
))
}
}
pub async fn notify_monitors(&self, pv_name: &str, payload: &NtPayload) {
let mut to_send: Vec<(u64, Vec<u8>)> = Vec::new();
{
let mut monitors = self.monitors.lock().await;
if let Some(list) = monitors.get_mut(pv_name) {
for sub in list.iter_mut() {
if !sub.running {
continue;
}
if sub.pipeline_enabled && sub.nfree == 0 {
continue;
}
let Some(msg) = Self::build_monitor_frame(sub, payload) else {
continue;
};
if sub.pipeline_enabled && sub.nfree > 0 {
sub.nfree -= 1;
}
sub.last_snapshot = Some(payload.clone());
to_send.push((sub.conn_id, msg));
}
}
}
for (conn_id, msg) in to_send {
self.send_msg(conn_id, msg).await;
debug!("Monitor update pv='{}' conn={}", pv_name, conn_id);
}
}
pub async fn send_monitor_update_for(
&self,
pv_name: &str,
conn_id: u64,
ioid: u32,
payload: &NtPayload,
) {
let mut to_send: Option<(u64, Vec<u8>)> = None;
{
let mut monitors = self.monitors.lock().await;
if let Some(list) = monitors.get_mut(pv_name) {
if let Some(sub) = list
.iter_mut()
.find(|s| s.conn_id == conn_id && s.ioid == ioid)
{
if !sub.running {
return;
}
if sub.pipeline_enabled && sub.nfree == 0 {
return;
}
let Some(msg) = Self::build_monitor_frame(sub, payload) else {
return;
};
if sub.pipeline_enabled && sub.nfree > 0 {
sub.nfree -= 1;
}
sub.last_snapshot = Some(payload.clone());
to_send = Some((sub.conn_id, msg));
}
}
}
if let Some((conn_id, msg)) = to_send {
self.send_msg(conn_id, msg).await;
}
}
pub async fn update_monitor_subscription(
&self,
conn_id: u64,
ioid: u32,
pv_name: &str,
running: bool,
nfree: Option<u32>,
pipeline_enabled: Option<bool>,
) -> bool {
let mut monitors = self.monitors.lock().await;
if let Some(list) = monitors.get_mut(pv_name) {
if let Some(sub) = list
.iter_mut()
.find(|s| s.conn_id == conn_id && s.ioid == ioid)
{
sub.running = running;
if let Some(v) = nfree {
sub.nfree = v;
}
if let Some(enabled) = pipeline_enabled {
if enabled {
sub.pipeline_enabled = true;
}
}
return true;
}
}
false
}
pub async fn remove_monitor_subscription(&self, conn_id: u64, ioid: u32, pv_name: &str) {
let mut monitors = self.monitors.lock().await;
if let Some(list) = monitors.get_mut(pv_name) {
list.retain(|s| s.conn_id != conn_id || s.ioid != ioid);
}
}
pub async fn cleanup_connection(&self, conn_id: u64) {
{
let mut monitors = self.monitors.lock().await;
for list in monitors.values_mut() {
list.retain(|s| s.conn_id != conn_id);
}
}
{
let mut conns = self.conns.lock().await;
conns.remove(&conn_id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use spvirit_codec::spvd_decode::StructureDesc;
use spvirit_codec::spvd_encode::{filter_structure_desc, nt_payload_desc};
use spvirit_types::{NtPayload, NtScalar, ScalarValue};
fn make_sub(filtered: Option<StructureDesc>) -> MonitorSub {
MonitorSub {
conn_id: 1,
ioid: 42,
version: 2,
is_be: false,
running: true,
pipeline_enabled: false,
nfree: 0,
filtered_desc: filtered,
last_snapshot: None,
}
}
fn nt_payload(value: f64, severity: i32) -> NtPayload {
let mut nt = NtScalar::from_value(ScalarValue::F64(value));
nt.alarm_severity = severity;
NtPayload::Scalar(nt)
}
#[test]
fn unfiltered_first_frame_full_then_suppress_duplicate_then_resend_on_change() {
let mut sub = make_sub(None);
let p1 = nt_payload(1.0, 0);
let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first frame");
assert!(!f1.is_empty());
sub.last_snapshot = Some(p1.clone());
assert!(
MonitorRegistry::build_monitor_frame(&sub, &p1).is_none(),
"identical unfiltered payload must be suppressed"
);
let p2 = nt_payload(2.0, 0);
let f2 = MonitorRegistry::build_monitor_frame(&sub, &p2).expect("full on change");
assert!(!f2.is_empty());
}
#[test]
fn filtered_first_frame_then_delta_none_when_selected_fields_unchanged() {
let p1 = nt_payload(1.0, 0);
let full_desc = nt_payload_desc(&p1);
let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
let mut sub = make_sub(Some(filt));
let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first filtered frame");
assert!(!f1.is_empty());
sub.last_snapshot = Some(p1.clone());
let p2 = nt_payload(2.0, 0);
assert!(
MonitorRegistry::build_monitor_frame(&sub, &p2).is_none(),
"filtered delta must be None when selected fields unchanged"
);
}
#[test]
fn filtered_delta_emitted_when_selected_field_changes() {
let p1 = nt_payload(1.0, 0);
let full_desc = nt_payload_desc(&p1);
let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
let mut sub = make_sub(Some(filt));
let _ = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first");
sub.last_snapshot = Some(p1.clone());
let p2 = nt_payload(1.0, 2);
let delta = MonitorRegistry::build_monitor_frame(&sub, &p2)
.expect("delta required when selected field changes");
assert!(!delta.is_empty());
}
}