Skip to main content

bacnet_server/
fault_detection.rs

1//! Fault detection / reliability evaluation.
2//!
3//! The [`FaultDetector`] periodically evaluates each object's reliability,
4//! checking for OVER_RANGE, UNDER_RANGE (analog objects), and optionally
5//! COMMUNICATION_FAILURE (staleness timeout).
6
7use bacnet_objects::database::ObjectDatabase;
8use bacnet_types::enums::{ObjectType, PropertyIdentifier, Reliability};
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
10
11/// A reliability change detected by the fault detector.
12#[derive(Debug, Clone, PartialEq)]
13pub struct ReliabilityChange {
14    /// The object whose reliability changed.
15    pub object_id: ObjectIdentifier,
16    /// Previous reliability value (raw u32).
17    pub old_reliability: u32,
18    /// New reliability value (raw u32).
19    pub new_reliability: u32,
20}
21
22/// Fault detection engine.
23///
24/// Call [`FaultDetector::evaluate`] periodically (e.g. every 10 s) against
25/// the object database.  It returns a list of objects whose reliability
26/// changed so the caller can update them.
27pub struct FaultDetector {
28    /// Timeout after which an object is considered to have a communication
29    /// failure.  Set to `None` to disable communication-failure detection.
30    pub comm_timeout: Option<std::time::Duration>,
31}
32
33impl Default for FaultDetector {
34    fn default() -> Self {
35        Self {
36            comm_timeout: Some(std::time::Duration::from_secs(60)),
37        }
38    }
39}
40
41impl FaultDetector {
42    /// Create a new fault detector with the given communication timeout.
43    pub fn new(comm_timeout: Option<std::time::Duration>) -> Self {
44        Self { comm_timeout }
45    }
46
47    /// Evaluate reliability for all objects in the database.
48    ///
49    /// For each analog object (AI, AO, AV) that has `MIN_PRES_VALUE` and
50    /// `MAX_PRES_VALUE` properties, the present value is compared against
51    /// those limits.  If out of range the reliability is set to
52    /// `OVER_RANGE` or `UNDER_RANGE`; otherwise `NO_FAULT_DETECTED`.
53    ///
54    /// Returns a list of changes that were applied to the database.
55    pub fn evaluate(&self, db: &mut ObjectDatabase) -> Vec<ReliabilityChange> {
56        let analog_types = [
57            ObjectType::ANALOG_INPUT,
58            ObjectType::ANALOG_OUTPUT,
59            ObjectType::ANALOG_VALUE,
60        ];
61
62        let mut updates: Vec<(ObjectIdentifier, u32, u32)> = Vec::new();
63
64        for &obj_type in &analog_types {
65            let oids = db.find_by_type(obj_type);
66            for oid in oids {
67                if let Some(obj) = db.get(&oid) {
68                    let current_reliability =
69                        match obj.read_property(PropertyIdentifier::RELIABILITY, None) {
70                            Ok(PropertyValue::Enumerated(v)) => v,
71                            _ => 0,
72                        };
73
74                    let present_value =
75                        match obj.read_property(PropertyIdentifier::PRESENT_VALUE, None) {
76                            Ok(PropertyValue::Real(v)) => v,
77                            _ => continue,
78                        };
79
80                    let min_pres = obj
81                        .read_property(PropertyIdentifier::MIN_PRES_VALUE, None)
82                        .ok()
83                        .and_then(|v| match v {
84                            PropertyValue::Real(f) => Some(f),
85                            _ => None,
86                        });
87
88                    let max_pres = obj
89                        .read_property(PropertyIdentifier::MAX_PRES_VALUE, None)
90                        .ok()
91                        .and_then(|v| match v {
92                            PropertyValue::Real(f) => Some(f),
93                            _ => None,
94                        });
95
96                    let new_reliability = if let Some(max) = max_pres {
97                        if present_value > max {
98                            Reliability::OVER_RANGE.to_raw()
99                        } else if let Some(min) = min_pres {
100                            if present_value < min {
101                                Reliability::UNDER_RANGE.to_raw()
102                            } else {
103                                Reliability::NO_FAULT_DETECTED.to_raw()
104                            }
105                        } else {
106                            Reliability::NO_FAULT_DETECTED.to_raw()
107                        }
108                    } else if let Some(min) = min_pres {
109                        if present_value < min {
110                            Reliability::UNDER_RANGE.to_raw()
111                        } else {
112                            Reliability::NO_FAULT_DETECTED.to_raw()
113                        }
114                    } else {
115                        continue;
116                    };
117
118                    if new_reliability != current_reliability {
119                        updates.push((oid, current_reliability, new_reliability));
120                    }
121                }
122            }
123        }
124
125        let mut changes = Vec::new();
126        for (oid, old_rel, new_rel) in updates {
127            if let Some(obj) = db.get_mut(&oid) {
128                if obj
129                    .write_property(
130                        PropertyIdentifier::RELIABILITY,
131                        None,
132                        PropertyValue::Enumerated(new_rel),
133                        None,
134                    )
135                    .is_ok()
136                {
137                    changes.push(ReliabilityChange {
138                        object_id: oid,
139                        old_reliability: old_rel,
140                        new_reliability: new_rel,
141                    });
142                }
143            }
144        }
145
146        changes
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
154
155    /// Helper: build an ObjectDatabase with a single AI that has min/max limits.
156    fn db_with_analog_input(
157        present_value: f32,
158        min_pres: Option<f32>,
159        max_pres: Option<f32>,
160    ) -> ObjectDatabase {
161        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
162        ai.set_present_value(present_value);
163        if let Some(min) = min_pres {
164            ai.set_min_pres_value(min);
165        }
166        if let Some(max) = max_pres {
167            ai.set_max_pres_value(max);
168        }
169        let mut db = ObjectDatabase::new();
170        db.add(Box::new(ai)).unwrap();
171        db
172    }
173
174    #[test]
175    fn no_fault_when_in_range() {
176        let mut db = db_with_analog_input(50.0, Some(0.0), Some(100.0));
177        let detector = FaultDetector::default();
178        let changes = detector.evaluate(&mut db);
179        assert!(changes.is_empty(), "no change expected for in-range value");
180    }
181
182    #[test]
183    fn over_range_detected() {
184        let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
185        let detector = FaultDetector::default();
186        let changes = detector.evaluate(&mut db);
187        assert_eq!(changes.len(), 1);
188        assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
189        assert_eq!(
190            changes[0].old_reliability,
191            Reliability::NO_FAULT_DETECTED.to_raw()
192        );
193    }
194
195    #[test]
196    fn under_range_detected() {
197        let mut db = db_with_analog_input(-10.0, Some(0.0), Some(100.0));
198        let detector = FaultDetector::default();
199        let changes = detector.evaluate(&mut db);
200        assert_eq!(changes.len(), 1);
201        assert_eq!(
202            changes[0].new_reliability,
203            Reliability::UNDER_RANGE.to_raw()
204        );
205    }
206
207    #[test]
208    fn returns_to_no_fault_after_correction() {
209        let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
210        let detector = FaultDetector::default();
211
212        // First evaluation: over-range
213        let changes = detector.evaluate(&mut db);
214        assert_eq!(changes.len(), 1);
215        assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
216
217        // Correct the value back in range
218        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
219        let obj = db.get_mut(&oid).unwrap();
220        obj.write_property(
221            PropertyIdentifier::PRESENT_VALUE,
222            None,
223            PropertyValue::Real(50.0),
224            None,
225        )
226        // AI write needs out_of_service=true
227        .unwrap_or_else(|_| {
228            obj.write_property(
229                PropertyIdentifier::OUT_OF_SERVICE,
230                None,
231                PropertyValue::Boolean(true),
232                None,
233            )
234            .unwrap();
235            obj.write_property(
236                PropertyIdentifier::PRESENT_VALUE,
237                None,
238                PropertyValue::Real(50.0),
239                None,
240            )
241            .unwrap();
242        });
243
244        // Second evaluation: back to no-fault
245        let changes = detector.evaluate(&mut db);
246        assert_eq!(changes.len(), 1);
247        assert_eq!(
248            changes[0].new_reliability,
249            Reliability::NO_FAULT_DETECTED.to_raw()
250        );
251    }
252
253    #[test]
254    fn no_limits_means_no_evaluation() {
255        // AI without min/max limits — detector should skip it entirely
256        let mut db = db_with_analog_input(999.0, None, None);
257        let detector = FaultDetector::default();
258        let changes = detector.evaluate(&mut db);
259        assert!(changes.is_empty());
260    }
261
262    #[test]
263    fn max_only_over_range() {
264        let mut db = db_with_analog_input(200.0, None, Some(100.0));
265        let detector = FaultDetector::default();
266        let changes = detector.evaluate(&mut db);
267        assert_eq!(changes.len(), 1);
268        assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
269    }
270
271    #[test]
272    fn min_only_under_range() {
273        let mut db = db_with_analog_input(-5.0, Some(0.0), None);
274        let detector = FaultDetector::default();
275        let changes = detector.evaluate(&mut db);
276        assert_eq!(changes.len(), 1);
277        assert_eq!(
278            changes[0].new_reliability,
279            Reliability::UNDER_RANGE.to_raw()
280        );
281    }
282
283    #[test]
284    fn no_change_emitted_when_already_faulted() {
285        let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
286        let detector = FaultDetector::default();
287
288        // First run: change detected
289        let changes = detector.evaluate(&mut db);
290        assert_eq!(changes.len(), 1);
291
292        // Second run: same fault, no new change
293        let changes = detector.evaluate(&mut db);
294        assert!(changes.is_empty());
295    }
296
297    #[test]
298    fn evaluates_multiple_analog_types() {
299        let mut db = ObjectDatabase::new();
300
301        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
302        ai.set_present_value(200.0);
303        ai.set_max_pres_value(100.0);
304        db.add(Box::new(ai)).unwrap();
305
306        let ao = AnalogOutputObject::new(1, "AO-1", 62).unwrap();
307        // AO starts at 0.0 — in range with no limits, so skipped
308        db.add(Box::new(ao)).unwrap();
309
310        let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
311        av.set_present_value(-10.0);
312        av.set_min_pres_value(0.0);
313        db.add(Box::new(av)).unwrap();
314
315        let detector = FaultDetector::default();
316        let changes = detector.evaluate(&mut db);
317        assert_eq!(changes.len(), 2);
318    }
319}