Skip to main content

bacnet_server/
fault_detection.rs

1//! Fault detection / reliability evaluation per ASHRAE 135-2020 Clause 12.
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        // Collect OIDs and their range-check results first (immutable borrow).
63        let mut updates: Vec<(ObjectIdentifier, u32, u32)> = Vec::new();
64
65        for &obj_type in &analog_types {
66            let oids = db.find_by_type(obj_type);
67            for oid in oids {
68                if let Some(obj) = db.get(&oid) {
69                    let current_reliability =
70                        match obj.read_property(PropertyIdentifier::RELIABILITY, None) {
71                            Ok(PropertyValue::Enumerated(v)) => v,
72                            _ => 0,
73                        };
74
75                    let present_value =
76                        match obj.read_property(PropertyIdentifier::PRESENT_VALUE, None) {
77                            Ok(PropertyValue::Real(v)) => v,
78                            _ => continue,
79                        };
80
81                    let min_pres = obj
82                        .read_property(PropertyIdentifier::MIN_PRES_VALUE, None)
83                        .ok()
84                        .and_then(|v| match v {
85                            PropertyValue::Real(f) => Some(f),
86                            _ => None,
87                        });
88
89                    let max_pres = obj
90                        .read_property(PropertyIdentifier::MAX_PRES_VALUE, None)
91                        .ok()
92                        .and_then(|v| match v {
93                            PropertyValue::Real(f) => Some(f),
94                            _ => None,
95                        });
96
97                    let new_reliability = if let Some(max) = max_pres {
98                        if present_value > max {
99                            Reliability::OVER_RANGE.to_raw()
100                        } else if let Some(min) = min_pres {
101                            if present_value < min {
102                                Reliability::UNDER_RANGE.to_raw()
103                            } else {
104                                Reliability::NO_FAULT_DETECTED.to_raw()
105                            }
106                        } else {
107                            Reliability::NO_FAULT_DETECTED.to_raw()
108                        }
109                    } else if let Some(min) = min_pres {
110                        if present_value < min {
111                            Reliability::UNDER_RANGE.to_raw()
112                        } else {
113                            Reliability::NO_FAULT_DETECTED.to_raw()
114                        }
115                    } else {
116                        // No min/max limits configured — keep current reliability unchanged.
117                        continue;
118                    };
119
120                    if new_reliability != current_reliability {
121                        updates.push((oid, current_reliability, new_reliability));
122                    }
123                }
124            }
125        }
126
127        // Apply updates (mutable borrow).
128        let mut changes = Vec::new();
129        for (oid, old_rel, new_rel) in updates {
130            if let Some(obj) = db.get_mut(&oid) {
131                if obj
132                    .write_property(
133                        PropertyIdentifier::RELIABILITY,
134                        None,
135                        PropertyValue::Enumerated(new_rel),
136                        None,
137                    )
138                    .is_ok()
139                {
140                    changes.push(ReliabilityChange {
141                        object_id: oid,
142                        old_reliability: old_rel,
143                        new_reliability: new_rel,
144                    });
145                }
146            }
147        }
148
149        changes
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
157
158    /// Helper: build an ObjectDatabase with a single AI that has min/max limits.
159    fn db_with_analog_input(
160        present_value: f32,
161        min_pres: Option<f32>,
162        max_pres: Option<f32>,
163    ) -> ObjectDatabase {
164        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
165        ai.set_present_value(present_value);
166        if let Some(min) = min_pres {
167            ai.set_min_pres_value(min);
168        }
169        if let Some(max) = max_pres {
170            ai.set_max_pres_value(max);
171        }
172        let mut db = ObjectDatabase::new();
173        db.add(Box::new(ai)).unwrap();
174        db
175    }
176
177    #[test]
178    fn no_fault_when_in_range() {
179        let mut db = db_with_analog_input(50.0, Some(0.0), Some(100.0));
180        let detector = FaultDetector::default();
181        let changes = detector.evaluate(&mut db);
182        assert!(changes.is_empty(), "no change expected for in-range value");
183    }
184
185    #[test]
186    fn over_range_detected() {
187        let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
188        let detector = FaultDetector::default();
189        let changes = detector.evaluate(&mut db);
190        assert_eq!(changes.len(), 1);
191        assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
192        assert_eq!(
193            changes[0].old_reliability,
194            Reliability::NO_FAULT_DETECTED.to_raw()
195        );
196    }
197
198    #[test]
199    fn under_range_detected() {
200        let mut db = db_with_analog_input(-10.0, Some(0.0), Some(100.0));
201        let detector = FaultDetector::default();
202        let changes = detector.evaluate(&mut db);
203        assert_eq!(changes.len(), 1);
204        assert_eq!(
205            changes[0].new_reliability,
206            Reliability::UNDER_RANGE.to_raw()
207        );
208    }
209
210    #[test]
211    fn returns_to_no_fault_after_correction() {
212        let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
213        let detector = FaultDetector::default();
214
215        // First evaluation: over-range
216        let changes = detector.evaluate(&mut db);
217        assert_eq!(changes.len(), 1);
218        assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
219
220        // Correct the value back in range
221        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
222        let obj = db.get_mut(&oid).unwrap();
223        obj.write_property(
224            PropertyIdentifier::PRESENT_VALUE,
225            None,
226            PropertyValue::Real(50.0),
227            None,
228        )
229        // AI write needs out_of_service=true
230        .unwrap_or_else(|_| {
231            obj.write_property(
232                PropertyIdentifier::OUT_OF_SERVICE,
233                None,
234                PropertyValue::Boolean(true),
235                None,
236            )
237            .unwrap();
238            obj.write_property(
239                PropertyIdentifier::PRESENT_VALUE,
240                None,
241                PropertyValue::Real(50.0),
242                None,
243            )
244            .unwrap();
245        });
246
247        // Second evaluation: back to no-fault
248        let changes = detector.evaluate(&mut db);
249        assert_eq!(changes.len(), 1);
250        assert_eq!(
251            changes[0].new_reliability,
252            Reliability::NO_FAULT_DETECTED.to_raw()
253        );
254    }
255
256    #[test]
257    fn no_limits_means_no_evaluation() {
258        // AI without min/max limits — detector should skip it entirely
259        let mut db = db_with_analog_input(999.0, None, None);
260        let detector = FaultDetector::default();
261        let changes = detector.evaluate(&mut db);
262        assert!(changes.is_empty());
263    }
264
265    #[test]
266    fn max_only_over_range() {
267        let mut db = db_with_analog_input(200.0, None, Some(100.0));
268        let detector = FaultDetector::default();
269        let changes = detector.evaluate(&mut db);
270        assert_eq!(changes.len(), 1);
271        assert_eq!(changes[0].new_reliability, Reliability::OVER_RANGE.to_raw());
272    }
273
274    #[test]
275    fn min_only_under_range() {
276        let mut db = db_with_analog_input(-5.0, Some(0.0), None);
277        let detector = FaultDetector::default();
278        let changes = detector.evaluate(&mut db);
279        assert_eq!(changes.len(), 1);
280        assert_eq!(
281            changes[0].new_reliability,
282            Reliability::UNDER_RANGE.to_raw()
283        );
284    }
285
286    #[test]
287    fn no_change_emitted_when_already_faulted() {
288        let mut db = db_with_analog_input(150.0, Some(0.0), Some(100.0));
289        let detector = FaultDetector::default();
290
291        // First run: change detected
292        let changes = detector.evaluate(&mut db);
293        assert_eq!(changes.len(), 1);
294
295        // Second run: same fault, no new change
296        let changes = detector.evaluate(&mut db);
297        assert!(changes.is_empty());
298    }
299
300    #[test]
301    fn evaluates_multiple_analog_types() {
302        let mut db = ObjectDatabase::new();
303
304        let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
305        ai.set_present_value(200.0);
306        ai.set_max_pres_value(100.0);
307        db.add(Box::new(ai)).unwrap();
308
309        let ao = AnalogOutputObject::new(1, "AO-1", 62).unwrap();
310        // AO starts at 0.0 — in range with no limits, so skipped
311        db.add(Box::new(ao)).unwrap();
312
313        let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
314        av.set_present_value(-10.0);
315        av.set_min_pres_value(0.0);
316        db.add(Box::new(av)).unwrap();
317
318        let detector = FaultDetector::default();
319        let changes = detector.evaluate(&mut db);
320        assert_eq!(changes.len(), 2);
321    }
322}