Skip to main content

spvirit_server/
monitor.rs

1//! Monitor subscription management for the PVA server.
2//!
3//! Tracks per-PV subscriber lists and dispatches monitor update messages.
4
5use 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
18/// Active connection channels and monitor subscriptions managed by the server.
19pub struct MonitorRegistry {
20    /// PV name → list of active monitor subscriptions.
21    pub monitors: Mutex<HashMap<String, Vec<MonitorSub>>>,
22    /// Connection id → message sender.
23    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    /// Send a raw message to a connection.
35    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    /// Build the wire bytes (if any) to send for `sub` given `payload`.
43    ///
44    /// Returns `Some(bytes)` when there is something new to send, in which
45    /// case the caller should also update `sub.last_snapshot` and apply any
46    /// pipeline credit accounting. Returns `None` when the update is a no-op
47    /// (duplicate of the last snapshot under the subscriber's field view) —
48    /// in that case the caller must NOT decrement `nfree`.
49    fn build_monitor_frame(sub: &MonitorSub, payload: &NtPayload) -> Option<Vec<u8>> {
50        let subcmd = 0x00;
51        // First frame: send the whole (possibly filtered) payload with bit 0 set.
52        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        // Subsequent frames.
65        if let Some(ref desc) = sub.filtered_desc {
66            // Filtered subscribers get a true sparse delta (may be None if the
67            // filtered view is unchanged).
68            encode_monitor_data_response_delta(
69                sub.ioid, subcmd, prev, payload, desc, sub.version, sub.is_be,
70            )
71        } else if prev == payload {
72            // Unfiltered subscriber, unchanged payload: suppress.
73            None
74        } else {
75            // Unfiltered subscriber, payload changed: send full.
76            Some(encode_monitor_data_response_payload(
77                sub.ioid, subcmd, payload, sub.version, sub.is_be,
78            ))
79        }
80    }
81
82    /// Broadcast a monitor update for `pv_name` to all running subscribers.
83    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                        // No-op update — preserve pipeline credit.
97                        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    /// Send a monitor update to a specific subscriber.
115    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    /// Update a monitor subscription's running/pipeline state.
154    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    /// Remove a monitor subscription.
185    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    /// Remove all subscriptions and connection entries for a given connection.
193    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        // First frame: full payload.
240        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        // Same payload: suppressed.
245        assert!(
246            MonitorRegistry::build_monitor_frame(&sub, &p1).is_none(),
247            "identical unfiltered payload must be suppressed"
248        );
249
250        // Changed payload: full again.
251        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        // Subscriber only cares about alarm.severity.
259        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        // Value changed, but alarm.severity is unchanged in the filtered view.
269        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        // Severity changes: delta must be emitted.
287        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}