1use std::collections::HashMap;
6
7use tokio::sync::{Mutex, mpsc};
8use tracing::debug;
9
10use spvirit_codec::spvirit_encode::{
11 encode_monitor_data_response_delta, encode_monitor_data_response_filtered,
12 encode_monitor_data_response_payload,
13};
14use spvirit_types::NtPayload;
15
16use crate::state::MonitorSub;
17
18pub struct MonitorRegistry {
20 pub monitors: Mutex<HashMap<String, Vec<MonitorSub>>>,
22 pub conns: Mutex<HashMap<u64, mpsc::Sender<Vec<u8>>>>,
24}
25
26impl MonitorRegistry {
27 pub fn new() -> Self {
28 Self {
29 monitors: Mutex::new(HashMap::new()),
30 conns: Mutex::new(HashMap::new()),
31 }
32 }
33
34 pub async fn send_msg(&self, conn_id: u64, msg: Vec<u8>) {
36 let conns = self.conns.lock().await;
37 if let Some(tx) = conns.get(&conn_id) {
38 let _ = tx.send(msg).await;
39 }
40 }
41
42 fn build_monitor_frame(sub: &MonitorSub, payload: &NtPayload) -> Option<Vec<u8>> {
50 let subcmd = 0x00;
51 let Some(prev) = sub.last_snapshot.as_ref() else {
53 let bytes = if let Some(ref desc) = sub.filtered_desc {
54 encode_monitor_data_response_filtered(
55 sub.ioid, subcmd, payload, desc, sub.version, sub.is_be,
56 )
57 } else {
58 encode_monitor_data_response_payload(
59 sub.ioid, subcmd, payload, sub.version, sub.is_be,
60 )
61 };
62 return Some(bytes);
63 };
64 if let Some(ref desc) = sub.filtered_desc {
66 encode_monitor_data_response_delta(
69 sub.ioid, subcmd, prev, payload, desc, sub.version, sub.is_be,
70 )
71 } else if prev == payload {
72 None
74 } else {
75 Some(encode_monitor_data_response_payload(
77 sub.ioid, subcmd, payload, sub.version, sub.is_be,
78 ))
79 }
80 }
81
82 pub async fn notify_monitors(&self, pv_name: &str, payload: &NtPayload) {
84 let mut to_send: Vec<(u64, Vec<u8>)> = Vec::new();
85 {
86 let mut monitors = self.monitors.lock().await;
87 if let Some(list) = monitors.get_mut(pv_name) {
88 for sub in list.iter_mut() {
89 if !sub.running {
90 continue;
91 }
92 if sub.pipeline_enabled && sub.nfree == 0 {
93 continue;
94 }
95 let Some(msg) = Self::build_monitor_frame(sub, payload) else {
96 continue;
98 };
99 if sub.pipeline_enabled && sub.nfree > 0 {
100 sub.nfree -= 1;
101 }
102 sub.last_snapshot = Some(payload.clone());
103 to_send.push((sub.conn_id, msg));
104 }
105 }
106 }
107
108 for (conn_id, msg) in to_send {
109 self.send_msg(conn_id, msg).await;
110 debug!("Monitor update pv='{}' conn={}", pv_name, conn_id);
111 }
112 }
113
114 pub async fn send_monitor_update_for(
116 &self,
117 pv_name: &str,
118 conn_id: u64,
119 ioid: u32,
120 payload: &NtPayload,
121 ) {
122 let mut to_send: Option<(u64, Vec<u8>)> = None;
123 {
124 let mut monitors = self.monitors.lock().await;
125 if let Some(list) = monitors.get_mut(pv_name) {
126 if let Some(sub) = list
127 .iter_mut()
128 .find(|s| s.conn_id == conn_id && s.ioid == ioid)
129 {
130 if !sub.running {
131 return;
132 }
133 if sub.pipeline_enabled && sub.nfree == 0 {
134 return;
135 }
136 let Some(msg) = Self::build_monitor_frame(sub, payload) else {
137 return;
138 };
139 if sub.pipeline_enabled && sub.nfree > 0 {
140 sub.nfree -= 1;
141 }
142 sub.last_snapshot = Some(payload.clone());
143 to_send = Some((sub.conn_id, msg));
144 }
145 }
146 }
147
148 if let Some((conn_id, msg)) = to_send {
149 self.send_msg(conn_id, msg).await;
150 }
151 }
152
153 pub async fn update_monitor_subscription(
155 &self,
156 conn_id: u64,
157 ioid: u32,
158 pv_name: &str,
159 running: bool,
160 nfree: Option<u32>,
161 pipeline_enabled: Option<bool>,
162 ) -> bool {
163 let mut monitors = self.monitors.lock().await;
164 if let Some(list) = monitors.get_mut(pv_name) {
165 if let Some(sub) = list
166 .iter_mut()
167 .find(|s| s.conn_id == conn_id && s.ioid == ioid)
168 {
169 sub.running = running;
170 if let Some(v) = nfree {
171 sub.nfree = v;
172 }
173 if let Some(enabled) = pipeline_enabled {
174 if enabled {
175 sub.pipeline_enabled = true;
176 }
177 }
178 return true;
179 }
180 }
181 false
182 }
183
184 pub async fn remove_monitor_subscription(&self, conn_id: u64, ioid: u32, pv_name: &str) {
186 let mut monitors = self.monitors.lock().await;
187 if let Some(list) = monitors.get_mut(pv_name) {
188 list.retain(|s| s.conn_id != conn_id || s.ioid != ioid);
189 }
190 }
191
192 pub async fn cleanup_connection(&self, conn_id: u64) {
194 {
195 let mut monitors = self.monitors.lock().await;
196 for list in monitors.values_mut() {
197 list.retain(|s| s.conn_id != conn_id);
198 }
199 }
200 {
201 let mut conns = self.conns.lock().await;
202 conns.remove(&conn_id);
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use spvirit_codec::spvd_decode::StructureDesc;
211 use spvirit_codec::spvd_encode::{filter_structure_desc, nt_payload_desc};
212 use spvirit_types::{NtPayload, NtScalar, ScalarValue};
213
214 fn make_sub(filtered: Option<StructureDesc>) -> MonitorSub {
215 MonitorSub {
216 conn_id: 1,
217 ioid: 42,
218 version: 2,
219 is_be: false,
220 running: true,
221 pipeline_enabled: false,
222 nfree: 0,
223 filtered_desc: filtered,
224 last_snapshot: None,
225 }
226 }
227
228 fn nt_payload(value: f64, severity: i32) -> NtPayload {
229 let mut nt = NtScalar::from_value(ScalarValue::F64(value));
230 nt.alarm_severity = severity;
231 NtPayload::Scalar(nt)
232 }
233
234 #[test]
235 fn unfiltered_first_frame_full_then_suppress_duplicate_then_resend_on_change() {
236 let mut sub = make_sub(None);
237 let p1 = nt_payload(1.0, 0);
238
239 let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first frame");
241 assert!(!f1.is_empty());
242 sub.last_snapshot = Some(p1.clone());
243
244 assert!(
246 MonitorRegistry::build_monitor_frame(&sub, &p1).is_none(),
247 "identical unfiltered payload must be suppressed"
248 );
249
250 let p2 = nt_payload(2.0, 0);
252 let f2 = MonitorRegistry::build_monitor_frame(&sub, &p2).expect("full on change");
253 assert!(!f2.is_empty());
254 }
255
256 #[test]
257 fn filtered_first_frame_then_delta_none_when_selected_fields_unchanged() {
258 let p1 = nt_payload(1.0, 0);
260 let full_desc = nt_payload_desc(&p1);
261 let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
262 let mut sub = make_sub(Some(filt));
263
264 let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first filtered frame");
265 assert!(!f1.is_empty());
266 sub.last_snapshot = Some(p1.clone());
267
268 let p2 = nt_payload(2.0, 0);
270 assert!(
271 MonitorRegistry::build_monitor_frame(&sub, &p2).is_none(),
272 "filtered delta must be None when selected fields unchanged"
273 );
274 }
275
276 #[test]
277 fn filtered_delta_emitted_when_selected_field_changes() {
278 let p1 = nt_payload(1.0, 0);
279 let full_desc = nt_payload_desc(&p1);
280 let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
281 let mut sub = make_sub(Some(filt));
282
283 let _ = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first");
284 sub.last_snapshot = Some(p1.clone());
285
286 let p2 = nt_payload(1.0, 2);
288 let delta = MonitorRegistry::build_monitor_frame(&sub, &p2)
289 .expect("delta required when selected field changes");
290 assert!(!delta.is_empty());
291 }
292}