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,
56 subcmd,
57 payload,
58 desc,
59 sub.version,
60 sub.is_be,
61 )
62 } else {
63 encode_monitor_data_response_payload(
64 sub.ioid,
65 subcmd,
66 payload,
67 sub.version,
68 sub.is_be,
69 )
70 };
71 return Some(bytes);
72 };
73 if let Some(ref desc) = sub.filtered_desc {
75 encode_monitor_data_response_delta(
78 sub.ioid,
79 subcmd,
80 prev,
81 payload,
82 desc,
83 sub.version,
84 sub.is_be,
85 )
86 } else if prev == payload {
87 None
89 } else {
90 Some(encode_monitor_data_response_payload(
92 sub.ioid,
93 subcmd,
94 payload,
95 sub.version,
96 sub.is_be,
97 ))
98 }
99 }
100
101 pub async fn notify_monitors(&self, pv_name: &str, payload: &NtPayload) {
103 let mut to_send: Vec<(u64, Vec<u8>)> = Vec::new();
104 {
105 let mut monitors = self.monitors.lock().await;
106 if let Some(list) = monitors.get_mut(pv_name) {
107 for sub in list.iter_mut() {
108 if !sub.running {
109 continue;
110 }
111 if sub.pipeline_enabled && sub.nfree == 0 {
112 continue;
113 }
114 let Some(msg) = Self::build_monitor_frame(sub, payload) else {
115 continue;
117 };
118 if sub.pipeline_enabled && sub.nfree > 0 {
119 sub.nfree -= 1;
120 }
121 sub.last_snapshot = Some(payload.clone());
122 to_send.push((sub.conn_id, msg));
123 }
124 }
125 }
126
127 for (conn_id, msg) in to_send {
128 self.send_msg(conn_id, msg).await;
129 debug!("Monitor update pv='{}' conn={}", pv_name, conn_id);
130 }
131 }
132
133 pub async fn send_monitor_update_for(
135 &self,
136 pv_name: &str,
137 conn_id: u64,
138 ioid: u32,
139 payload: &NtPayload,
140 ) {
141 let mut to_send: Option<(u64, Vec<u8>)> = None;
142 {
143 let mut monitors = self.monitors.lock().await;
144 if let Some(list) = monitors.get_mut(pv_name) {
145 if let Some(sub) = list
146 .iter_mut()
147 .find(|s| s.conn_id == conn_id && s.ioid == ioid)
148 {
149 if !sub.running {
150 return;
151 }
152 if sub.pipeline_enabled && sub.nfree == 0 {
153 return;
154 }
155 let Some(msg) = Self::build_monitor_frame(sub, payload) else {
156 return;
157 };
158 if sub.pipeline_enabled && sub.nfree > 0 {
159 sub.nfree -= 1;
160 }
161 sub.last_snapshot = Some(payload.clone());
162 to_send = Some((sub.conn_id, msg));
163 }
164 }
165 }
166
167 if let Some((conn_id, msg)) = to_send {
168 self.send_msg(conn_id, msg).await;
169 }
170 }
171
172 pub async fn update_monitor_subscription(
174 &self,
175 conn_id: u64,
176 ioid: u32,
177 pv_name: &str,
178 running: bool,
179 nfree: Option<u32>,
180 pipeline_enabled: Option<bool>,
181 ) -> bool {
182 let mut monitors = self.monitors.lock().await;
183 if let Some(list) = monitors.get_mut(pv_name) {
184 if let Some(sub) = list
185 .iter_mut()
186 .find(|s| s.conn_id == conn_id && s.ioid == ioid)
187 {
188 sub.running = running;
189 if let Some(v) = nfree {
190 sub.nfree = v;
191 }
192 if let Some(enabled) = pipeline_enabled {
193 if enabled {
194 sub.pipeline_enabled = true;
195 }
196 }
197 return true;
198 }
199 }
200 false
201 }
202
203 pub async fn remove_monitor_subscription(&self, conn_id: u64, ioid: u32, pv_name: &str) {
205 let mut monitors = self.monitors.lock().await;
206 if let Some(list) = monitors.get_mut(pv_name) {
207 list.retain(|s| s.conn_id != conn_id || s.ioid != ioid);
208 }
209 }
210
211 pub async fn cleanup_connection(&self, conn_id: u64) {
213 {
214 let mut monitors = self.monitors.lock().await;
215 for list in monitors.values_mut() {
216 list.retain(|s| s.conn_id != conn_id);
217 }
218 }
219 {
220 let mut conns = self.conns.lock().await;
221 conns.remove(&conn_id);
222 }
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use spvirit_codec::spvd_decode::StructureDesc;
230 use spvirit_codec::spvd_encode::{filter_structure_desc, nt_payload_desc};
231 use spvirit_types::{NtPayload, NtScalar, ScalarValue};
232
233 fn make_sub(filtered: Option<StructureDesc>) -> MonitorSub {
234 MonitorSub {
235 conn_id: 1,
236 ioid: 42,
237 version: 2,
238 is_be: false,
239 running: true,
240 pipeline_enabled: false,
241 nfree: 0,
242 filtered_desc: filtered,
243 last_snapshot: None,
244 }
245 }
246
247 fn nt_payload(value: f64, severity: i32) -> NtPayload {
248 let mut nt = NtScalar::from_value(ScalarValue::F64(value));
249 nt.alarm_severity = severity;
250 NtPayload::Scalar(nt)
251 }
252
253 #[test]
254 fn unfiltered_first_frame_full_then_suppress_duplicate_then_resend_on_change() {
255 let mut sub = make_sub(None);
256 let p1 = nt_payload(1.0, 0);
257
258 let f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first frame");
260 assert!(!f1.is_empty());
261 sub.last_snapshot = Some(p1.clone());
262
263 assert!(
265 MonitorRegistry::build_monitor_frame(&sub, &p1).is_none(),
266 "identical unfiltered payload must be suppressed"
267 );
268
269 let p2 = nt_payload(2.0, 0);
271 let f2 = MonitorRegistry::build_monitor_frame(&sub, &p2).expect("full on change");
272 assert!(!f2.is_empty());
273 }
274
275 #[test]
276 fn filtered_first_frame_then_delta_none_when_selected_fields_unchanged() {
277 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 f1 = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first filtered frame");
284 assert!(!f1.is_empty());
285 sub.last_snapshot = Some(p1.clone());
286
287 let p2 = nt_payload(2.0, 0);
289 assert!(
290 MonitorRegistry::build_monitor_frame(&sub, &p2).is_none(),
291 "filtered delta must be None when selected fields unchanged"
292 );
293 }
294
295 #[test]
296 fn filtered_delta_emitted_when_selected_field_changes() {
297 let p1 = nt_payload(1.0, 0);
298 let full_desc = nt_payload_desc(&p1);
299 let filt = filter_structure_desc(&full_desc, &["alarm.severity".to_string()]);
300 let mut sub = make_sub(Some(filt));
301
302 let _ = MonitorRegistry::build_monitor_frame(&sub, &p1).expect("first");
303 sub.last_snapshot = Some(p1.clone());
304
305 let p2 = nt_payload(1.0, 2);
307 let delta = MonitorRegistry::build_monitor_frame(&sub, &p2)
308 .expect("delta required when selected field changes");
309 assert!(!delta.is_empty());
310 }
311}