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