1use rustbac_core::types::{ObjectId, PropertyId};
8use std::collections::HashMap;
9use std::sync::RwLock;
10use std::time::Instant;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum IntrinsicEventState {
15 Normal,
16 Fault,
17 OffNormal,
18 HighLimit,
19 LowLimit,
20}
21
22impl IntrinsicEventState {
23 pub fn to_u32(self) -> u32 {
25 match self {
26 Self::Normal => 0,
27 Self::Fault => 1,
28 Self::OffNormal => 2,
29 Self::HighLimit => 3,
30 Self::LowLimit => 4,
31 }
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct AckedTransitions {
38 pub to_offnormal: bool,
39 pub to_fault: bool,
40 pub to_normal: bool,
41}
42
43impl Default for AckedTransitions {
44 fn default() -> Self {
45 Self {
46 to_offnormal: true,
47 to_fault: true,
48 to_normal: true,
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub enum IntrinsicAlgorithm {
56 ChangeOfValue {
58 cov_increment: f64,
59 },
60 OutOfRange {
62 high_limit: f64,
63 low_limit: f64,
64 deadband: f64,
65 },
66 ChangeOfState {
68 alarm_values: Vec<u32>,
69 },
70 FloatingLimit {
72 setpoint_ref: ObjectId,
73 setpoint_property: PropertyId,
74 high_diff_limit: f64,
75 low_diff_limit: f64,
76 deadband: f64,
77 },
78 CommandFailure {
80 feedback_ref: ObjectId,
81 feedback_property: PropertyId,
82 time_delay_ms: u64,
83 },
84}
85
86#[derive(Debug)]
88struct IntrinsicState {
89 object_id: ObjectId,
90 algorithm: IntrinsicAlgorithm,
91 current_event_state: IntrinsicEventState,
92 acked_transitions: AckedTransitions,
93 notification_class: u32,
94 time_delay_ms: u64,
95 pending_since: Option<Instant>,
97 pending_state: Option<IntrinsicEventState>,
99 last_reported_value: Option<f64>,
101}
102
103#[derive(Debug, Clone)]
105pub struct PendingEventNotification {
106 pub object_id: ObjectId,
108 pub notification_class: u32,
110 pub event_state: IntrinsicEventState,
112 pub from_state: IntrinsicEventState,
114 pub property_id: PropertyId,
116 pub value: Option<f64>,
118}
119
120pub struct IntrinsicEnrollment {
122 pub object_id: ObjectId,
123 pub algorithm: IntrinsicAlgorithm,
124 pub notification_class: u32,
125 pub time_delay_ms: u64,
126}
127
128pub struct IntrinsicReportingEngine {
131 states: RwLock<HashMap<ObjectId, IntrinsicState>>,
132}
133
134impl IntrinsicReportingEngine {
135 pub fn new() -> Self {
137 Self {
138 states: RwLock::new(HashMap::new()),
139 }
140 }
141
142 pub fn enroll(&self, enrollment: IntrinsicEnrollment) {
144 let state = IntrinsicState {
145 object_id: enrollment.object_id,
146 algorithm: enrollment.algorithm,
147 current_event_state: IntrinsicEventState::Normal,
148 acked_transitions: AckedTransitions::default(),
149 notification_class: enrollment.notification_class,
150 time_delay_ms: enrollment.time_delay_ms,
151 pending_since: None,
152 pending_state: None,
153 last_reported_value: None,
154 };
155 self.states
156 .write()
157 .expect("IntrinsicReportingEngine lock")
158 .insert(enrollment.object_id, state);
159 }
160
161 pub fn unenroll(&self, object_id: ObjectId) {
163 self.states
164 .write()
165 .expect("IntrinsicReportingEngine lock")
166 .remove(&object_id);
167 }
168
169 pub fn evaluate(
177 &self,
178 object_id: ObjectId,
179 property_id: PropertyId,
180 value: f64,
181 setpoint_value: Option<f64>,
182 feedback_value: Option<f64>,
183 ) -> Vec<PendingEventNotification> {
184 let mut states = self
185 .states
186 .write()
187 .expect("IntrinsicReportingEngine lock");
188
189 let state = match states.get_mut(&object_id) {
190 Some(s) => s,
191 None => return Vec::new(),
192 };
193
194 let now = Instant::now();
195 let mut notifications = Vec::new();
196
197 if let IntrinsicAlgorithm::ChangeOfValue { cov_increment } = &state.algorithm {
199 let should_fire = match state.last_reported_value {
200 Some(last) => (value - last).abs() > *cov_increment,
201 None => true, };
203 if should_fire {
204 state.last_reported_value = Some(value);
205 notifications.push(PendingEventNotification {
206 object_id,
207 notification_class: state.notification_class,
208 event_state: state.current_event_state,
209 from_state: state.current_event_state,
210 property_id,
211 value: Some(value),
212 });
213 }
214 return notifications;
215 }
216
217 let new_state = match &state.algorithm {
218 IntrinsicAlgorithm::OutOfRange {
219 high_limit,
220 low_limit,
221 deadband,
222 } => evaluate_out_of_range(
223 value,
224 *high_limit,
225 *low_limit,
226 *deadband,
227 state.current_event_state,
228 ),
229 IntrinsicAlgorithm::ChangeOfValue { .. } => unreachable!(),
230 IntrinsicAlgorithm::ChangeOfState { alarm_values } => {
231 evaluate_change_of_state(value as u32, alarm_values)
232 }
233 IntrinsicAlgorithm::FloatingLimit {
234 high_diff_limit,
235 low_diff_limit,
236 deadband,
237 ..
238 } => {
239 let setpoint = setpoint_value.unwrap_or(0.0);
240 evaluate_floating_limit(
241 value,
242 setpoint,
243 *high_diff_limit,
244 *low_diff_limit,
245 *deadband,
246 state.current_event_state,
247 )
248 }
249 IntrinsicAlgorithm::CommandFailure { time_delay_ms, .. } => {
250 let feedback = feedback_value.unwrap_or(value);
251 evaluate_command_failure(value, feedback, *time_delay_ms, state, now)
252 }
253 };
254
255 if new_state != state.current_event_state {
256 if state.time_delay_ms == 0 {
258 let from = state.current_event_state;
260 state.current_event_state = new_state;
261 state.pending_since = None;
262 state.pending_state = None;
263
264 match new_state {
266 IntrinsicEventState::Normal => {
267 state.acked_transitions.to_normal = false;
268 }
269 IntrinsicEventState::OffNormal
270 | IntrinsicEventState::HighLimit
271 | IntrinsicEventState::LowLimit => {
272 state.acked_transitions.to_offnormal = false;
273 }
274 IntrinsicEventState::Fault => {
275 state.acked_transitions.to_fault = false;
276 }
277 }
278
279 if let IntrinsicAlgorithm::ChangeOfValue { .. } = &state.algorithm {
280 state.last_reported_value = Some(value);
281 }
282
283 notifications.push(PendingEventNotification {
284 object_id,
285 notification_class: state.notification_class,
286 event_state: new_state,
287 from_state: from,
288 property_id,
289 value: Some(value),
290 });
291 } else {
292 match state.pending_state {
294 Some(pending) if pending == new_state => {
295 if let Some(since) = state.pending_since {
297 if now.duration_since(since).as_millis() as u64 >= state.time_delay_ms {
298 let from = state.current_event_state;
299 state.current_event_state = new_state;
300 state.pending_since = None;
301 state.pending_state = None;
302
303 match new_state {
304 IntrinsicEventState::Normal => {
305 state.acked_transitions.to_normal = false;
306 }
307 IntrinsicEventState::OffNormal
308 | IntrinsicEventState::HighLimit
309 | IntrinsicEventState::LowLimit => {
310 state.acked_transitions.to_offnormal = false;
311 }
312 IntrinsicEventState::Fault => {
313 state.acked_transitions.to_fault = false;
314 }
315 }
316
317 if let IntrinsicAlgorithm::ChangeOfValue { .. } = &state.algorithm {
318 state.last_reported_value = Some(value);
319 }
320
321 notifications.push(PendingEventNotification {
322 object_id,
323 notification_class: state.notification_class,
324 event_state: new_state,
325 from_state: from,
326 property_id,
327 value: Some(value),
328 });
329 }
330 }
331 }
332 _ => {
333 state.pending_since = Some(now);
335 state.pending_state = Some(new_state);
336 }
337 }
338 }
339 } else {
340 state.pending_since = None;
342 state.pending_state = None;
343 }
344
345 notifications
346 }
347
348 pub fn acknowledge(
350 &self,
351 object_id: ObjectId,
352 event_state: IntrinsicEventState,
353 ) -> bool {
354 let mut states = self
355 .states
356 .write()
357 .expect("IntrinsicReportingEngine lock");
358 if let Some(state) = states.get_mut(&object_id) {
359 match event_state {
360 IntrinsicEventState::Normal => {
361 state.acked_transitions.to_normal = true;
362 }
363 IntrinsicEventState::OffNormal
364 | IntrinsicEventState::HighLimit
365 | IntrinsicEventState::LowLimit => {
366 state.acked_transitions.to_offnormal = true;
367 }
368 IntrinsicEventState::Fault => {
369 state.acked_transitions.to_fault = true;
370 }
371 }
372 true
373 } else {
374 false
375 }
376 }
377
378 pub fn event_state(&self, object_id: ObjectId) -> Option<IntrinsicEventState> {
380 self.states
381 .read()
382 .expect("IntrinsicReportingEngine lock")
383 .get(&object_id)
384 .map(|s| s.current_event_state)
385 }
386
387 pub fn acked_transitions(&self, object_id: ObjectId) -> Option<AckedTransitions> {
389 self.states
390 .read()
391 .expect("IntrinsicReportingEngine lock")
392 .get(&object_id)
393 .map(|s| s.acked_transitions)
394 }
395}
396
397impl Default for IntrinsicReportingEngine {
398 fn default() -> Self {
399 Self::new()
400 }
401}
402
403fn evaluate_out_of_range(
408 value: f64,
409 high_limit: f64,
410 low_limit: f64,
411 deadband: f64,
412 current_state: IntrinsicEventState,
413) -> IntrinsicEventState {
414 match current_state {
415 IntrinsicEventState::Normal => {
416 if value > high_limit {
417 IntrinsicEventState::HighLimit
418 } else if value < low_limit {
419 IntrinsicEventState::LowLimit
420 } else {
421 IntrinsicEventState::Normal
422 }
423 }
424 IntrinsicEventState::HighLimit => {
425 if value < low_limit {
426 IntrinsicEventState::LowLimit
427 } else if value <= high_limit - deadband {
428 IntrinsicEventState::Normal
429 } else {
430 IntrinsicEventState::HighLimit
431 }
432 }
433 IntrinsicEventState::LowLimit => {
434 if value > high_limit {
435 IntrinsicEventState::HighLimit
436 } else if value >= low_limit + deadband {
437 IntrinsicEventState::Normal
438 } else {
439 IntrinsicEventState::LowLimit
440 }
441 }
442 _ => {
443 if value > high_limit {
445 IntrinsicEventState::HighLimit
446 } else if value < low_limit {
447 IntrinsicEventState::LowLimit
448 } else {
449 IntrinsicEventState::Normal
450 }
451 }
452 }
453}
454
455fn evaluate_change_of_state(value: u32, alarm_values: &[u32]) -> IntrinsicEventState {
456 if alarm_values.contains(&value) {
457 IntrinsicEventState::OffNormal
458 } else {
459 IntrinsicEventState::Normal
460 }
461}
462
463fn evaluate_floating_limit(
464 value: f64,
465 setpoint: f64,
466 high_diff_limit: f64,
467 low_diff_limit: f64,
468 deadband: f64,
469 current_state: IntrinsicEventState,
470) -> IntrinsicEventState {
471 let high_threshold = setpoint + high_diff_limit;
472 let low_threshold = setpoint - low_diff_limit;
473
474 match current_state {
475 IntrinsicEventState::Normal => {
476 if value > high_threshold {
477 IntrinsicEventState::HighLimit
478 } else if value < low_threshold {
479 IntrinsicEventState::LowLimit
480 } else {
481 IntrinsicEventState::Normal
482 }
483 }
484 IntrinsicEventState::HighLimit => {
485 if value < low_threshold {
486 IntrinsicEventState::LowLimit
487 } else if value <= high_threshold - deadband {
488 IntrinsicEventState::Normal
489 } else {
490 IntrinsicEventState::HighLimit
491 }
492 }
493 IntrinsicEventState::LowLimit => {
494 if value > high_threshold {
495 IntrinsicEventState::HighLimit
496 } else if value >= low_threshold + deadband {
497 IntrinsicEventState::Normal
498 } else {
499 IntrinsicEventState::LowLimit
500 }
501 }
502 _ => {
503 if value > high_threshold {
504 IntrinsicEventState::HighLimit
505 } else if value < low_threshold {
506 IntrinsicEventState::LowLimit
507 } else {
508 IntrinsicEventState::Normal
509 }
510 }
511 }
512}
513
514fn evaluate_command_failure(
515 command_value: f64,
516 feedback_value: f64,
517 _time_delay_ms: u64,
518 _state: &IntrinsicState,
519 _now: Instant,
520) -> IntrinsicEventState {
521 if (command_value - feedback_value).abs() > f64::EPSILON {
524 IntrinsicEventState::OffNormal
525 } else {
526 IntrinsicEventState::Normal
527 }
528}
529
530pub fn encode_unconfirmed_event_notification(
534 process_id: u32,
535 initiating_device_id: ObjectId,
536 event_object_id: ObjectId,
537 event_state: IntrinsicEventState,
538 notification_class: u32,
539 from_state: IntrinsicEventState,
540) -> Option<Vec<u8>> {
541 use rustbac_core::apdu::UnconfirmedRequestHeader;
542 use rustbac_core::encoding::primitives::encode_ctx_unsigned;
543 use rustbac_core::encoding::writer::Writer;
544 use rustbac_core::npdu::Npdu;
545
546 let mut buf = [0u8; 1400];
547 let mut w = Writer::new(&mut buf);
548
549 Npdu::new(0).encode(&mut w).ok()?;
550
551 UnconfirmedRequestHeader {
553 service_choice: 0x03,
554 }
555 .encode(&mut w)
556 .ok()?;
557
558 encode_ctx_unsigned(&mut w, 0, process_id).ok()?;
560 encode_ctx_unsigned(&mut w, 1, initiating_device_id.raw()).ok()?;
562 encode_ctx_unsigned(&mut w, 2, event_object_id.raw()).ok()?;
564 encode_ctx_unsigned(&mut w, 4, notification_class).ok()?;
566 encode_ctx_unsigned(&mut w, 9, 0).ok()?;
568 encode_ctx_unsigned(&mut w, 11, event_state.to_u32()).ok()?;
570
571 Some(w.as_written().to_vec())
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use rustbac_core::types::ObjectType;
578
579 fn av(instance: u32) -> ObjectId {
580 ObjectId::new(ObjectType::AnalogValue, instance)
581 }
582
583 #[test]
584 fn out_of_range_high_limit() {
585 let engine = IntrinsicReportingEngine::new();
586 engine.enroll(IntrinsicEnrollment {
587 object_id: av(1),
588 algorithm: IntrinsicAlgorithm::OutOfRange {
589 high_limit: 100.0,
590 low_limit: 0.0,
591 deadband: 5.0,
592 },
593 notification_class: 1,
594 time_delay_ms: 0,
595 });
596
597 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 105.0, None, None);
599 assert_eq!(notifs.len(), 1);
600 assert_eq!(notifs[0].event_state, IntrinsicEventState::HighLimit);
601 assert_eq!(notifs[0].from_state, IntrinsicEventState::Normal);
602 }
603
604 #[test]
605 fn out_of_range_deadband_hysteresis() {
606 let engine = IntrinsicReportingEngine::new();
607 engine.enroll(IntrinsicEnrollment {
608 object_id: av(1),
609 algorithm: IntrinsicAlgorithm::OutOfRange {
610 high_limit: 100.0,
611 low_limit: 0.0,
612 deadband: 5.0,
613 },
614 notification_class: 1,
615 time_delay_ms: 0,
616 });
617
618 engine.evaluate(av(1), PropertyId::PresentValue, 105.0, None, None);
620
621 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 97.0, None, None);
623 assert!(notifs.is_empty());
624
625 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 94.0, None, None);
627 assert_eq!(notifs.len(), 1);
628 assert_eq!(notifs[0].event_state, IntrinsicEventState::Normal);
629 }
630
631 #[test]
632 fn out_of_range_low_limit() {
633 let engine = IntrinsicReportingEngine::new();
634 engine.enroll(IntrinsicEnrollment {
635 object_id: av(1),
636 algorithm: IntrinsicAlgorithm::OutOfRange {
637 high_limit: 100.0,
638 low_limit: 10.0,
639 deadband: 2.0,
640 },
641 notification_class: 1,
642 time_delay_ms: 0,
643 });
644
645 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 5.0, None, None);
646 assert_eq!(notifs.len(), 1);
647 assert_eq!(notifs[0].event_state, IntrinsicEventState::LowLimit);
648 }
649
650 #[test]
651 fn change_of_value_fires_on_increment() {
652 let engine = IntrinsicReportingEngine::new();
653 engine.enroll(IntrinsicEnrollment {
654 object_id: av(1),
655 algorithm: IntrinsicAlgorithm::ChangeOfValue {
656 cov_increment: 1.0,
657 },
658 notification_class: 1,
659 time_delay_ms: 0,
660 });
661
662 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 50.0, None, None);
664 assert_eq!(notifs.len(), 1);
665
666 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 50.5, None, None);
668 assert!(notifs.is_empty());
669
670 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 52.0, None, None);
672 assert_eq!(notifs.len(), 1);
673 }
674
675 #[test]
676 fn change_of_state_alarm() {
677 let engine = IntrinsicReportingEngine::new();
678 engine.enroll(IntrinsicEnrollment {
679 object_id: av(1),
680 algorithm: IntrinsicAlgorithm::ChangeOfState {
681 alarm_values: vec![3, 5],
682 },
683 notification_class: 1,
684 time_delay_ms: 0,
685 });
686
687 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 1.0, None, None);
689 assert!(notifs.is_empty());
690
691 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 3.0, None, None);
693 assert_eq!(notifs.len(), 1);
694 assert_eq!(notifs[0].event_state, IntrinsicEventState::OffNormal);
695 }
696
697 #[test]
698 fn floating_limit() {
699 let engine = IntrinsicReportingEngine::new();
700 engine.enroll(IntrinsicEnrollment {
701 object_id: av(1),
702 algorithm: IntrinsicAlgorithm::FloatingLimit {
703 setpoint_ref: av(2),
704 setpoint_property: PropertyId::PresentValue,
705 high_diff_limit: 5.0,
706 low_diff_limit: 5.0,
707 deadband: 2.0,
708 },
709 notification_class: 1,
710 time_delay_ms: 0,
711 });
712
713 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 56.0, Some(50.0), None);
715 assert_eq!(notifs.len(), 1);
716 assert_eq!(notifs[0].event_state, IntrinsicEventState::HighLimit);
717 }
718
719 #[test]
720 fn command_failure() {
721 let engine = IntrinsicReportingEngine::new();
722 engine.enroll(IntrinsicEnrollment {
723 object_id: av(1),
724 algorithm: IntrinsicAlgorithm::CommandFailure {
725 feedback_ref: av(2),
726 feedback_property: PropertyId::PresentValue,
727 time_delay_ms: 0,
728 },
729 notification_class: 1,
730 time_delay_ms: 0,
731 });
732
733 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 50.0, None, Some(50.0));
735 assert!(notifs.is_empty());
736
737 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 50.0, None, Some(30.0));
739 assert_eq!(notifs.len(), 1);
740 assert_eq!(notifs[0].event_state, IntrinsicEventState::OffNormal);
741 }
742
743 #[test]
744 fn time_delay_pending() {
745 let engine = IntrinsicReportingEngine::new();
746 engine.enroll(IntrinsicEnrollment {
747 object_id: av(1),
748 algorithm: IntrinsicAlgorithm::OutOfRange {
749 high_limit: 100.0,
750 low_limit: 0.0,
751 deadband: 5.0,
752 },
753 notification_class: 1,
754 time_delay_ms: 10000, });
756
757 let notifs = engine.evaluate(av(1), PropertyId::PresentValue, 105.0, None, None);
759 assert!(notifs.is_empty(), "should not fire before time delay");
760
761 assert_eq!(
763 engine.event_state(av(1)),
764 Some(IntrinsicEventState::Normal)
765 );
766 }
767
768 #[test]
769 fn acknowledge_transition() {
770 let engine = IntrinsicReportingEngine::new();
771 engine.enroll(IntrinsicEnrollment {
772 object_id: av(1),
773 algorithm: IntrinsicAlgorithm::OutOfRange {
774 high_limit: 100.0,
775 low_limit: 0.0,
776 deadband: 5.0,
777 },
778 notification_class: 1,
779 time_delay_ms: 0,
780 });
781
782 engine.evaluate(av(1), PropertyId::PresentValue, 105.0, None, None);
783
784 let acked = engine.acked_transitions(av(1)).unwrap();
785 assert!(!acked.to_offnormal);
786
787 engine.acknowledge(av(1), IntrinsicEventState::HighLimit);
788
789 let acked = engine.acked_transitions(av(1)).unwrap();
790 assert!(acked.to_offnormal);
791 }
792
793 #[test]
794 fn enroll_unenroll() {
795 let engine = IntrinsicReportingEngine::new();
796 engine.enroll(IntrinsicEnrollment {
797 object_id: av(1),
798 algorithm: IntrinsicAlgorithm::OutOfRange {
799 high_limit: 100.0,
800 low_limit: 0.0,
801 deadband: 5.0,
802 },
803 notification_class: 1,
804 time_delay_ms: 0,
805 });
806
807 assert!(engine.event_state(av(1)).is_some());
808 engine.unenroll(av(1));
809 assert!(engine.event_state(av(1)).is_none());
810 }
811
812 #[test]
813 fn encode_event_notification_produces_bytes() {
814 let result = encode_unconfirmed_event_notification(
815 1,
816 ObjectId::new(ObjectType::Device, 42),
817 av(1),
818 IntrinsicEventState::HighLimit,
819 1,
820 IntrinsicEventState::Normal,
821 );
822 assert!(result.is_some());
823 assert!(result.unwrap().len() > 10);
824 }
825}