1use std::collections::HashMap;
7use std::sync::Mutex;
8
9use bacnet_objects::database::ObjectDatabase;
10use bacnet_objects::traits::ReliabilityEvaluation;
11use bacnet_types::primitives::ObjectIdentifier;
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct ReliabilityChange {
16 pub object_id: ObjectIdentifier,
18 pub old_reliability: u32,
20 pub new_reliability: u32,
22}
23
24pub struct FaultDetector {
32 pub comm_timeout: Option<std::time::Duration>,
35 warned_internal_failures: Mutex<HashMap<ObjectIdentifier, String>>,
36}
37
38impl Default for FaultDetector {
39 fn default() -> Self {
40 Self {
41 comm_timeout: Some(std::time::Duration::from_secs(60)),
42 warned_internal_failures: Mutex::new(HashMap::new()),
43 }
44 }
45}
46
47impl FaultDetector {
48 pub fn new(comm_timeout: Option<std::time::Duration>) -> Self {
50 Self {
51 comm_timeout,
52 warned_internal_failures: Mutex::new(HashMap::new()),
53 }
54 }
55
56 fn should_warn_internal_failure(&self, oid: ObjectIdentifier, error: &str) -> bool {
57 let mut warned = self
58 .warned_internal_failures
59 .lock()
60 .unwrap_or_else(|poisoned| poisoned.into_inner());
61 if warned.get(&oid).is_some_and(|previous| previous == error) {
62 false
63 } else {
64 warned.insert(oid, error.to_owned());
65 true
66 }
67 }
68
69 fn clear_internal_failure(&self, oid: &ObjectIdentifier) {
70 self.warned_internal_failures
71 .lock()
72 .unwrap_or_else(|poisoned| poisoned.into_inner())
73 .remove(oid);
74 }
75
76 fn prune_internal_failures(&self, db: &ObjectDatabase) {
77 self.warned_internal_failures
78 .lock()
79 .unwrap_or_else(|poisoned| poisoned.into_inner())
80 .retain(|oid, _| db.get(oid).is_some());
81 }
82
83 pub fn evaluate(&self, db: &mut ObjectDatabase) -> Vec<ReliabilityChange> {
93 let mut changes = Vec::new();
94 db.for_each_object_mut(|oid, object| {
95 if object.reliability_evaluation_inhibited_internal() {
96 self.clear_internal_failure(&oid);
97 return;
98 }
99 match object.evaluate_reliability_internal() {
100 Ok(ReliabilityEvaluation::Unchanged) => {
101 self.clear_internal_failure(&oid);
102 }
103 Ok(ReliabilityEvaluation::Changed {
104 old_reliability,
105 new_reliability,
106 }) => {
107 self.clear_internal_failure(&oid);
108 changes.push(ReliabilityChange {
109 object_id: oid,
110 old_reliability,
111 new_reliability,
112 });
113 }
114 Err(error) => {
115 let error_text = error.to_string();
116 if self.should_warn_internal_failure(oid, &error_text) {
117 tracing::warn!(
118 object = %oid,
119 error = %error,
120 "Fault detection object-owned reliability evaluation failed"
121 );
122 }
123 }
124 }
125 });
126 self.prune_internal_failures(db);
127 changes
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use std::borrow::Cow;
134 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
135 use std::sync::Arc;
136
137 use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
138 use bacnet_objects::traits::BACnetObject;
139 use bacnet_types::enums::{EventState, ObjectType, PropertyIdentifier, Reliability};
140 use bacnet_types::error::Error;
141 use bacnet_types::primitives::PropertyValue;
142 use tracing::span::{Attributes, Id, Record};
143 use tracing::{Event, Level, Metadata, Subscriber};
144
145 use super::*;
146
147 const HOOK_ERROR: &str = "test reliability hook failed";
148
149 struct WarningCounter {
150 warnings: Arc<AtomicUsize>,
151 }
152
153 impl Subscriber for WarningCounter {
154 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
155 *metadata.level() == Level::WARN
156 }
157
158 fn new_span(&self, _span: &Attributes<'_>) -> Id {
159 Id::from_u64(1)
160 }
161
162 fn record(&self, _span: &Id, _values: &Record<'_>) {}
163
164 fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
165
166 fn event(&self, event: &Event<'_>) {
167 if *event.metadata().level() == Level::WARN {
168 self.warnings.fetch_add(1, Ordering::SeqCst);
169 }
170 }
171
172 fn enter(&self, _span: &Id) {}
173
174 fn exit(&self, _span: &Id) {}
175 }
176
177 fn with_warning_counter(test: impl FnOnce(&AtomicUsize)) {
178 let warnings = Arc::new(AtomicUsize::new(0));
179 let subscriber = WarningCounter {
180 warnings: Arc::clone(&warnings),
181 };
182 tracing::subscriber::with_default(subscriber, || test(warnings.as_ref()));
183 }
184
185 fn read_reliability(db: &ObjectDatabase, oid: ObjectIdentifier) -> u32 {
186 match db
187 .get(&oid)
188 .unwrap()
189 .read_property(PropertyIdentifier::RELIABILITY, None)
190 .unwrap()
191 {
192 PropertyValue::Enumerated(value) => value,
193 other => panic!("expected Enumerated Reliability, got {other:?}"),
194 }
195 }
196
197 struct OptInReliabilityObject {
198 oid: ObjectIdentifier,
199 name: String,
200 reliability: u32,
201 target_reliability: u32,
202 fail: Arc<AtomicBool>,
203 }
204
205 impl OptInReliabilityObject {
206 fn new(instance: u32, name: &str, target_reliability: u32, fail: Arc<AtomicBool>) -> Self {
207 Self {
208 oid: ObjectIdentifier::new(ObjectType::BINARY_INPUT, instance).unwrap(),
209 name: name.to_owned(),
210 reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
211 target_reliability,
212 fail,
213 }
214 }
215 }
216
217 impl BACnetObject for OptInReliabilityObject {
218 fn object_identifier(&self) -> ObjectIdentifier {
219 self.oid
220 }
221
222 fn object_name(&self) -> &str {
223 &self.name
224 }
225
226 fn read_property(
227 &self,
228 property: PropertyIdentifier,
229 _array_index: Option<u32>,
230 ) -> Result<PropertyValue, Error> {
231 if property == PropertyIdentifier::RELIABILITY {
232 Ok(PropertyValue::Enumerated(self.reliability))
233 } else {
234 Err(Error::Encoding("test property is unsupported".into()))
235 }
236 }
237
238 fn write_property(
239 &mut self,
240 _property: PropertyIdentifier,
241 _array_index: Option<u32>,
242 _value: PropertyValue,
243 _priority: Option<u8>,
244 ) -> Result<(), Error> {
245 Err(Error::Encoding("test object is read-only".into()))
246 }
247
248 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
249 Cow::Borrowed(&[PropertyIdentifier::RELIABILITY])
250 }
251
252 fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
253 if self.fail.load(Ordering::SeqCst) {
254 return Err(Error::Encoding(HOOK_ERROR.into()));
255 }
256 if self.reliability == self.target_reliability {
257 return Ok(ReliabilityEvaluation::Unchanged);
258 }
259
260 let old_reliability = self.reliability;
261 self.reliability = self.target_reliability;
262 Ok(ReliabilityEvaluation::Changed {
263 old_reliability,
264 new_reliability: self.reliability,
265 })
266 }
267 }
268
269 struct InhibitedProbeObject {
270 oid: ObjectIdentifier,
271 calls: Arc<AtomicUsize>,
272 }
273
274 impl BACnetObject for InhibitedProbeObject {
275 fn object_identifier(&self) -> ObjectIdentifier {
276 self.oid
277 }
278
279 fn object_name(&self) -> &str {
280 "inhibited-probe"
281 }
282
283 fn read_property(
284 &self,
285 _property: PropertyIdentifier,
286 _array_index: Option<u32>,
287 ) -> Result<PropertyValue, Error> {
288 Err(Error::Encoding("test property is unsupported".into()))
289 }
290
291 fn write_property(
292 &mut self,
293 _property: PropertyIdentifier,
294 _array_index: Option<u32>,
295 _value: PropertyValue,
296 _priority: Option<u8>,
297 ) -> Result<(), Error> {
298 Err(Error::Encoding("test object is read-only".into()))
299 }
300
301 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
302 Cow::Borrowed(&[])
303 }
304
305 fn reliability_evaluation_inhibited_internal(&self) -> bool {
306 true
307 }
308
309 fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
310 self.calls.fetch_add(1, Ordering::SeqCst);
311 Err(Error::Encoding(HOOK_ERROR.into()))
312 }
313 }
314
315 #[test]
316 fn inhibited_object_skips_hook_and_clears_stale_failure_warning() {
317 let detector = FaultDetector::default();
318 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 77).unwrap();
319 let calls = Arc::new(AtomicUsize::new(0));
320 let mut db = ObjectDatabase::new();
321 db.add(Box::new(InhibitedProbeObject {
322 oid,
323 calls: Arc::clone(&calls),
324 }))
325 .unwrap();
326 assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
327
328 assert!(detector.evaluate(&mut db).is_empty());
329 assert_eq!(calls.load(Ordering::SeqCst), 0, "inhibited hook was called");
330 assert!(
331 detector.should_warn_internal_failure(oid, HOOK_ERROR),
332 "inhibition must clear an old warning so recovery cannot leave stale state"
333 );
334 }
335
336 #[test]
337 fn identical_internal_failure_warning_is_suppressed_until_state_changes() {
338 let detector = FaultDetector::default();
339 let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
340
341 assert!(detector.should_warn_internal_failure(oid, "first error"));
342 assert!(!detector.should_warn_internal_failure(oid, "first error"));
343 assert!(detector.should_warn_internal_failure(oid, "different error"));
344 assert!(!detector.should_warn_internal_failure(oid, "different error"));
345
346 detector.clear_internal_failure(&oid);
347 assert!(detector.should_warn_internal_failure(oid, "first error"));
348 }
349
350 #[test]
351 fn stock_analog_engineering_bounds_never_change_reliability() {
352 let mut db = ObjectDatabase::new();
353
354 let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
355 ai.set_min_pres_value(0.0);
356 ai.set_max_pres_value(100.0);
357 ai.set_present_value(200.0);
358 let ai_oid = ai.object_identifier();
359 db.add(Box::new(ai)).unwrap();
360
361 let mut ao = AnalogOutputObject::new(1, "AO-1", 62).unwrap();
362 ao.set_min_pres_value(0.0);
363 ao.set_max_pres_value(100.0);
364 ao.write_property(
365 PropertyIdentifier::PRESENT_VALUE,
366 None,
367 PropertyValue::Real(150.0),
368 Some(8),
369 )
370 .unwrap();
371 let ao_oid = ao.object_identifier();
372 db.add(Box::new(ao)).unwrap();
373
374 let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
375 av.set_min_pres_value(0.0);
376 av.set_max_pres_value(100.0);
377 av.set_present_value(-10.0);
378 let av_oid = av.object_identifier();
379 db.add(Box::new(av)).unwrap();
380
381 let changes = FaultDetector::default().evaluate(&mut db);
382
383 assert!(changes.is_empty());
384 for oid in [ai_oid, ao_oid, av_oid] {
385 assert_eq!(
386 read_reliability(&db, oid),
387 Reliability::NO_FAULT_DETECTED.to_raw()
388 );
389 }
390 let ao = db.get(&ao_oid).unwrap();
391 assert_eq!(
392 ao.read_property(PropertyIdentifier::PRESENT_VALUE, None)
393 .unwrap(),
394 PropertyValue::Real(150.0)
395 );
396 assert_eq!(
397 ao.read_property(PropertyIdentifier::PRIORITY_ARRAY, Some(8))
398 .unwrap(),
399 PropertyValue::Real(150.0)
400 );
401 }
402
403 #[test]
404 fn configured_ai_and_av_changes_are_recorded_and_seen_by_intrinsic_fault_observation() {
405 let mut db = ObjectDatabase::new();
406
407 let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
408 ai.configure_fault_out_of_range(10.0, 20.0).unwrap();
409 ai.set_present_value(21.0);
410 let ai_oid = ai.object_identifier();
411 db.add(Box::new(ai)).unwrap();
412
413 let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
414 av.configure_fault_out_of_range(10.0, 20.0).unwrap();
415 av.write_property(
416 PropertyIdentifier::PRESENT_VALUE,
417 None,
418 PropertyValue::Real(9.0),
419 Some(8),
420 )
421 .unwrap();
422 let av_oid = av.object_identifier();
423 db.add(Box::new(av)).unwrap();
424
425 let detector = FaultDetector::default();
426 let changes = detector.evaluate(&mut db);
427 assert_eq!(changes.len(), 2);
428 assert!(changes.contains(&ReliabilityChange {
429 object_id: ai_oid,
430 old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
431 new_reliability: Reliability::OVER_RANGE.to_raw(),
432 }));
433 assert!(changes.contains(&ReliabilityChange {
434 object_id: av_oid,
435 old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
436 new_reliability: Reliability::UNDER_RANGE.to_raw(),
437 }));
438 assert!(detector.evaluate(&mut db).is_empty());
439
440 for oid in [ai_oid, av_oid] {
441 let proposal = db
442 .get_mut(&oid)
443 .unwrap()
444 .evaluate_intrinsic_reporting()
445 .expect("range Reliability must be observed as an intrinsic fault");
446 assert_eq!(proposal.change.from, EventState::NORMAL);
447 assert_eq!(proposal.change.to, EventState::FAULT);
448 }
449 }
450
451 #[cfg(not(debug_assertions))]
455 #[test]
456 fn non_finite_configured_analogs_create_no_change_and_keep_owned_faults() {
457 let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
458 ai.configure_fault_out_of_range(10.0, 20.0).unwrap();
459 ai.set_present_value(21.0);
460 ai.evaluate_reliability_internal().unwrap();
461 ai.set_present_value(f32::NAN);
462 let ai_oid = ai.object_identifier();
463
464 let mut av = AnalogValueObject::new(1, "AV-1", 62).unwrap();
465 av.configure_fault_out_of_range(10.0, 20.0).unwrap();
466 av.set_present_value(9.0);
467 av.evaluate_reliability_internal().unwrap();
468 av.set_present_value(f32::INFINITY);
469 let av_oid = av.object_identifier();
470
471 let mut db = ObjectDatabase::new();
472 db.add(Box::new(ai)).unwrap();
473 db.add(Box::new(av)).unwrap();
474 let detector = FaultDetector::default();
475
476 assert!(detector.evaluate(&mut db).is_empty());
477 assert!(detector.evaluate(&mut db).is_empty());
478 assert_eq!(
479 read_reliability(&db, ai_oid),
480 Reliability::OVER_RANGE.to_raw()
481 );
482 assert_eq!(
483 read_reliability(&db, av_oid),
484 Reliability::UNDER_RANGE.to_raw()
485 );
486
487 let ai = db.get_mut(&ai_oid).unwrap();
488 ai.write_property(
489 PropertyIdentifier::OUT_OF_SERVICE,
490 None,
491 PropertyValue::Boolean(true),
492 None,
493 )
494 .unwrap();
495 ai.write_property(
496 PropertyIdentifier::PRESENT_VALUE,
497 None,
498 PropertyValue::Real(9.0),
499 None,
500 )
501 .unwrap();
502 ai.write_property(
503 PropertyIdentifier::OUT_OF_SERVICE,
504 None,
505 PropertyValue::Boolean(false),
506 None,
507 )
508 .unwrap();
509 db.get_mut(&av_oid)
510 .unwrap()
511 .write_property(
512 PropertyIdentifier::PRESENT_VALUE,
513 None,
514 PropertyValue::Real(21.0),
515 Some(8),
516 )
517 .unwrap();
518
519 let changes = detector.evaluate(&mut db);
520 assert_eq!(changes.len(), 2);
521 assert!(changes.contains(&ReliabilityChange {
522 object_id: ai_oid,
523 old_reliability: Reliability::OVER_RANGE.to_raw(),
524 new_reliability: Reliability::UNDER_RANGE.to_raw(),
525 }));
526 assert!(changes.contains(&ReliabilityChange {
527 object_id: av_oid,
528 old_reliability: Reliability::UNDER_RANGE.to_raw(),
529 new_reliability: Reliability::OVER_RANGE.to_raw(),
530 }));
531 }
532
533 #[test]
534 fn existing_nonzero_in_service_reliability_is_preserved() {
535 let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
536 ai.set_min_pres_value(0.0);
537 ai.set_max_pres_value(100.0);
538 ai.set_present_value(50.0);
539 ai.set_reliability_internal(Reliability::NO_SENSOR.to_raw())
540 .unwrap();
541 let oid = ai.object_identifier();
542 let mut db = ObjectDatabase::new();
543 db.add(Box::new(ai)).unwrap();
544
545 assert!(FaultDetector::default().evaluate(&mut db).is_empty());
546 assert_eq!(read_reliability(&db, oid), Reliability::NO_SENSOR.to_raw());
547 }
548
549 #[test]
550 fn out_of_service_client_write_and_restore_remain_object_owned() {
551 let mut ai = AnalogInputObject::new(1, "AI-1", 62).unwrap();
552 ai.set_reliability_internal(Reliability::OVER_RANGE.to_raw())
553 .unwrap();
554 let oid = ai.object_identifier();
555 let mut db = ObjectDatabase::new();
556 db.add(Box::new(ai)).unwrap();
557
558 let object = db.get_mut(&oid).unwrap();
559 object
560 .write_property(
561 PropertyIdentifier::OUT_OF_SERVICE,
562 None,
563 PropertyValue::Boolean(true),
564 None,
565 )
566 .unwrap();
567 object
568 .write_property(
569 PropertyIdentifier::RELIABILITY,
570 None,
571 PropertyValue::Enumerated(Reliability::NO_SENSOR.to_raw()),
572 None,
573 )
574 .unwrap();
575
576 assert!(FaultDetector::default().evaluate(&mut db).is_empty());
577 assert_eq!(read_reliability(&db, oid), Reliability::NO_SENSOR.to_raw());
578
579 db.get_mut(&oid)
580 .unwrap()
581 .write_property(
582 PropertyIdentifier::OUT_OF_SERVICE,
583 None,
584 PropertyValue::Boolean(false),
585 None,
586 )
587 .unwrap();
588 assert_eq!(read_reliability(&db, oid), Reliability::OVER_RANGE.to_raw());
589 }
590
591 #[test]
592 fn only_hook_returned_changed_creates_a_change_record() {
593 let fail = Arc::new(AtomicBool::new(false));
594 let object =
595 OptInReliabilityObject::new(1, "custom-hook", Reliability::NO_SENSOR.to_raw(), fail);
596 let oid = object.object_identifier();
597 let mut db = ObjectDatabase::new();
598 db.add(Box::new(object)).unwrap();
599 let detector = FaultDetector::default();
600 assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
601
602 assert_eq!(
603 detector.evaluate(&mut db),
604 vec![ReliabilityChange {
605 object_id: oid,
606 old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
607 new_reliability: Reliability::NO_SENSOR.to_raw(),
608 }]
609 );
610 assert_eq!(read_reliability(&db, oid), Reliability::NO_SENSOR.to_raw());
611 assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
612 assert!(detector.evaluate(&mut db).is_empty());
613 assert!(detector.should_warn_internal_failure(oid, HOOK_ERROR));
614 }
615
616 #[test]
617 fn failing_hook_warning_is_rate_limited_and_restored_after_recovery() {
618 let fail = Arc::new(AtomicBool::new(true));
619 let object = OptInReliabilityObject::new(
620 1,
621 "failing-hook",
622 Reliability::NO_SENSOR.to_raw(),
623 Arc::clone(&fail),
624 );
625 let oid = object.object_identifier();
626 let mut db = ObjectDatabase::new();
627 db.add(Box::new(object)).unwrap();
628 let detector = FaultDetector::default();
629
630 with_warning_counter(|warnings| {
631 assert!(detector.evaluate(&mut db).is_empty());
632 assert_eq!(warnings.load(Ordering::SeqCst), 1);
633 assert_eq!(
634 read_reliability(&db, oid),
635 Reliability::NO_FAULT_DETECTED.to_raw()
636 );
637
638 assert!(detector.evaluate(&mut db).is_empty());
639 assert_eq!(warnings.load(Ordering::SeqCst), 1);
640
641 fail.store(false, Ordering::SeqCst);
642 assert_eq!(
643 detector.evaluate(&mut db),
644 vec![ReliabilityChange {
645 object_id: oid,
646 old_reliability: Reliability::NO_FAULT_DETECTED.to_raw(),
647 new_reliability: Reliability::NO_SENSOR.to_raw(),
648 }]
649 );
650 assert_eq!(warnings.load(Ordering::SeqCst), 1);
651
652 fail.store(true, Ordering::SeqCst);
653 assert!(detector.evaluate(&mut db).is_empty());
654 assert_eq!(warnings.load(Ordering::SeqCst), 2);
655
656 fail.store(false, Ordering::SeqCst);
657 assert!(detector.evaluate(&mut db).is_empty());
658 assert_eq!(warnings.load(Ordering::SeqCst), 2);
659
660 fail.store(true, Ordering::SeqCst);
661 assert!(detector.evaluate(&mut db).is_empty());
662 assert_eq!(warnings.load(Ordering::SeqCst), 3);
663 });
664 }
665
666 #[test]
667 fn deleted_failure_warning_is_pruned_and_oid_reuse_warns() {
668 let fail = Arc::new(AtomicBool::new(true));
669 let object = OptInReliabilityObject::new(
670 1,
671 "removed-failing-hook",
672 Reliability::NO_FAULT_DETECTED.to_raw(),
673 Arc::clone(&fail),
674 );
675 let oid = object.object_identifier();
676 let mut db = ObjectDatabase::new();
677 db.add(Box::new(object)).unwrap();
678 let detector = FaultDetector::default();
679
680 with_warning_counter(|warnings| {
681 assert!(detector.evaluate(&mut db).is_empty());
682 assert_eq!(warnings.load(Ordering::SeqCst), 1);
683
684 assert!(detector.evaluate(&mut db).is_empty());
685 assert_eq!(warnings.load(Ordering::SeqCst), 1);
686
687 assert!(db.remove(&oid).is_some());
688 assert!(detector.evaluate(&mut db).is_empty());
689 assert_eq!(warnings.load(Ordering::SeqCst), 1);
690
691 db.add(Box::new(OptInReliabilityObject::new(
692 1,
693 "replacement-failing-hook",
694 Reliability::NO_FAULT_DETECTED.to_raw(),
695 fail,
696 )))
697 .unwrap();
698 assert!(detector.evaluate(&mut db).is_empty());
699 assert_eq!(warnings.load(Ordering::SeqCst), 2);
700 });
701 }
702}