use bacnet_types::enums::{EventState, EventType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventStateChange {
pub from: EventState,
pub to: EventState,
}
impl EventStateChange {
pub fn event_type(&self) -> EventType {
if self.from == EventState::HIGH_LIMIT
|| self.from == EventState::LOW_LIMIT
|| self.to == EventState::HIGH_LIMIT
|| self.to == EventState::LOW_LIMIT
{
EventType::OUT_OF_RANGE
} else {
EventType::CHANGE_OF_STATE
}
}
pub fn transition(&self) -> EventTransition {
if self.to == EventState::NORMAL {
EventTransition::ToNormal
} else if self.to == EventState::FAULT {
EventTransition::ToFault
} else {
EventTransition::ToOffnormal
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventTransition {
ToOffnormal,
ToFault,
ToNormal,
}
impl EventTransition {
pub fn bit_mask(self) -> u8 {
match self {
EventTransition::ToOffnormal => 0x01,
EventTransition::ToFault => 0x02,
EventTransition::ToNormal => 0x04,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LimitEnable {
pub low_limit_enable: bool,
pub high_limit_enable: bool,
}
impl LimitEnable {
pub const NONE: Self = Self {
low_limit_enable: false,
high_limit_enable: false,
};
pub const BOTH: Self = Self {
low_limit_enable: true,
high_limit_enable: true,
};
pub fn to_bits(self) -> u8 {
let mut bits = 0u8;
if self.low_limit_enable {
bits |= 0x80; }
if self.high_limit_enable {
bits |= 0x40; }
bits
}
pub fn from_bits(byte: u8) -> Self {
Self {
low_limit_enable: byte & 0x80 != 0,
high_limit_enable: byte & 0x40 != 0,
}
}
}
#[derive(Debug, Clone)]
pub struct OutOfRangeDetector {
pub high_limit: f32,
pub low_limit: f32,
pub deadband: f32,
pub limit_enable: LimitEnable,
pub notification_class: u32,
pub notify_type: u32,
pub event_enable: u8,
pub time_delay: u32,
pub event_state: EventState,
pub acked_transitions: u8,
}
impl Default for OutOfRangeDetector {
fn default() -> Self {
Self {
high_limit: 100.0,
low_limit: 0.0,
deadband: 1.0,
limit_enable: LimitEnable::NONE,
notification_class: 0,
notify_type: 0, event_enable: 0,
time_delay: 0,
event_state: EventState::NORMAL,
acked_transitions: 0b111, }
}
}
impl OutOfRangeDetector {
const TO_OFFNORMAL: u8 = 0x01;
const TO_FAULT: u8 = 0x02;
const TO_NORMAL: u8 = 0x04;
pub fn evaluate(&mut self, present_value: f32) -> Option<EventStateChange> {
let new_state = self.compute_new_state(present_value);
if new_state != self.event_state {
let change = EventStateChange {
from: self.event_state,
to: new_state,
};
self.event_state = new_state;
let enabled = match new_state {
s if s == EventState::NORMAL => self.event_enable & Self::TO_NORMAL != 0,
s if s == EventState::HIGH_LIMIT || s == EventState::LOW_LIMIT => {
self.event_enable & Self::TO_OFFNORMAL != 0
}
_ => self.event_enable & Self::TO_FAULT != 0,
};
if enabled {
Some(change)
} else {
None
}
} else {
None
}
}
fn compute_new_state(&self, pv: f32) -> EventState {
let high_enabled = self.limit_enable.high_limit_enable;
let low_enabled = self.limit_enable.low_limit_enable;
match self.event_state {
s if s == EventState::NORMAL => {
if high_enabled && pv > self.high_limit {
return EventState::HIGH_LIMIT;
}
if low_enabled && pv < self.low_limit {
return EventState::LOW_LIMIT;
}
EventState::NORMAL
}
s if s == EventState::HIGH_LIMIT => {
if low_enabled && pv < self.low_limit {
return EventState::LOW_LIMIT;
}
if pv < self.high_limit - self.deadband {
return EventState::NORMAL;
}
EventState::HIGH_LIMIT
}
s if s == EventState::LOW_LIMIT => {
if high_enabled && pv > self.high_limit {
return EventState::HIGH_LIMIT;
}
if pv > self.low_limit + self.deadband {
return EventState::NORMAL;
}
EventState::LOW_LIMIT
}
_ => self.event_state, }
}
}
#[derive(Debug, Clone)]
pub struct ChangeOfStateDetector {
pub alarm_values: Vec<u32>,
pub notification_class: u32,
pub notify_type: u32,
pub event_enable: u8,
pub time_delay: u32,
pub event_state: EventState,
pub acked_transitions: u8,
}
impl Default for ChangeOfStateDetector {
fn default() -> Self {
Self {
alarm_values: Vec::new(),
notification_class: 0,
notify_type: 0,
event_enable: 0,
time_delay: 0,
event_state: EventState::NORMAL,
acked_transitions: 0b111,
}
}
}
impl ChangeOfStateDetector {
const TO_OFFNORMAL: u8 = 0x01;
const TO_FAULT: u8 = 0x02;
const TO_NORMAL: u8 = 0x04;
pub fn evaluate(&mut self, present_value: u32) -> Option<EventStateChange> {
let is_alarm = self.alarm_values.contains(&present_value);
let new_state = if is_alarm {
EventState::OFFNORMAL
} else {
EventState::NORMAL
};
if new_state != self.event_state {
let change = EventStateChange {
from: self.event_state,
to: new_state,
};
self.event_state = new_state;
let enabled = match new_state {
s if s == EventState::NORMAL => self.event_enable & Self::TO_NORMAL != 0,
s if s == EventState::OFFNORMAL => self.event_enable & Self::TO_OFFNORMAL != 0,
_ => self.event_enable & Self::TO_FAULT != 0,
};
if enabled {
Some(change)
} else {
None
}
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct CommandFailureDetector {
pub notification_class: u32,
pub notify_type: u32,
pub event_enable: u8,
pub time_delay: u32,
pub event_state: EventState,
pub acked_transitions: u8,
}
impl Default for CommandFailureDetector {
fn default() -> Self {
Self {
notification_class: 0,
notify_type: 0,
event_enable: 0,
time_delay: 0,
event_state: EventState::NORMAL,
acked_transitions: 0b111,
}
}
}
impl CommandFailureDetector {
const TO_OFFNORMAL: u8 = 0x01;
#[allow(dead_code)]
const TO_FAULT: u8 = 0x02;
const TO_NORMAL: u8 = 0x04;
pub fn evaluate(
&mut self,
present_value: u32,
feedback_value: u32,
) -> Option<EventStateChange> {
let new_state = if present_value != feedback_value {
EventState::OFFNORMAL
} else {
EventState::NORMAL
};
if new_state != self.event_state {
let change = EventStateChange {
from: self.event_state,
to: new_state,
};
self.event_state = new_state;
let enabled = match new_state {
s if s == EventState::NORMAL => self.event_enable & Self::TO_NORMAL != 0,
s if s == EventState::OFFNORMAL => self.event_enable & Self::TO_OFFNORMAL != 0,
_ => false,
};
if enabled {
Some(change)
} else {
None
}
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_detector() -> OutOfRangeDetector {
OutOfRangeDetector {
high_limit: 80.0,
low_limit: 20.0,
deadband: 2.0,
limit_enable: LimitEnable::BOTH,
notification_class: 1,
notify_type: 0,
event_enable: 0x07, time_delay: 0,
event_state: EventState::NORMAL,
acked_transitions: 0b111,
}
}
#[test]
fn normal_stays_normal_within_limits() {
let mut det = make_detector();
assert!(det.evaluate(50.0).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
}
#[test]
fn normal_to_high_limit() {
let mut det = make_detector();
let change = det.evaluate(81.0).unwrap();
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::HIGH_LIMIT);
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
}
#[test]
fn normal_to_low_limit() {
let mut det = make_detector();
let change = det.evaluate(19.0).unwrap();
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::LOW_LIMIT);
assert_eq!(det.event_state, EventState::LOW_LIMIT);
}
#[test]
fn at_boundary_no_transition() {
let mut det = make_detector();
assert!(det.evaluate(80.0).is_none());
assert!(det.evaluate(20.0).is_none());
}
#[test]
fn high_limit_to_normal_with_deadband() {
let mut det = make_detector();
det.evaluate(81.0);
assert!(det.evaluate(79.0).is_none());
let change = det.evaluate(77.0).unwrap();
assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn low_limit_to_normal_with_deadband() {
let mut det = make_detector();
det.evaluate(19.0);
assert!(det.evaluate(21.0).is_none());
let change = det.evaluate(23.0).unwrap();
assert_eq!(change.from, EventState::LOW_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn high_limit_to_low_limit_direct() {
let mut det = make_detector();
det.evaluate(81.0);
let change = det.evaluate(19.0).unwrap();
assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::LOW_LIMIT);
}
#[test]
fn low_limit_to_high_limit_direct() {
let mut det = make_detector();
det.evaluate(19.0);
let change = det.evaluate(81.0).unwrap();
assert_eq!(change.from, EventState::LOW_LIMIT);
assert_eq!(change.to, EventState::HIGH_LIMIT);
}
#[test]
fn high_limit_disabled_no_transition() {
let mut det = make_detector();
det.limit_enable.high_limit_enable = false;
assert!(det.evaluate(100.0).is_none());
}
#[test]
fn low_limit_disabled_no_transition() {
let mut det = make_detector();
det.limit_enable.low_limit_enable = false;
assert!(det.evaluate(0.0).is_none());
}
#[test]
fn both_limits_disabled() {
let mut det = make_detector();
det.limit_enable = LimitEnable::NONE;
assert!(det.evaluate(100.0).is_none());
assert!(det.evaluate(0.0).is_none());
}
#[test]
fn limit_enable_bits_round_trip() {
let le = LimitEnable::BOTH;
let bits = le.to_bits();
let decoded = LimitEnable::from_bits(bits);
assert_eq!(decoded, le);
let le = LimitEnable {
low_limit_enable: true,
high_limit_enable: false,
};
let bits = le.to_bits();
let decoded = LimitEnable::from_bits(bits);
assert_eq!(decoded, le);
}
#[test]
fn deadband_at_exact_boundary() {
let mut det = make_detector();
det.evaluate(81.0);
assert!(det.evaluate(78.0).is_none());
let change = det.evaluate(77.99).unwrap();
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn event_state_change_derives_event_type() {
use bacnet_types::enums::EventType;
let change = EventStateChange {
from: EventState::NORMAL,
to: EventState::HIGH_LIMIT,
};
assert_eq!(change.event_type(), EventType::OUT_OF_RANGE);
}
#[test]
fn event_state_change_to_normal_from_high() {
use bacnet_types::enums::EventType;
let change = EventStateChange {
from: EventState::HIGH_LIMIT,
to: EventState::NORMAL,
};
assert_eq!(change.event_type(), EventType::OUT_OF_RANGE);
}
#[test]
fn event_enable_zero_suppresses_all_notifications() {
let mut det = make_detector();
det.event_enable = 0x00;
assert!(det.evaluate(81.0).is_none());
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
assert!(det.evaluate(50.0).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
assert!(det.evaluate(19.0).is_none());
assert_eq!(det.event_state, EventState::LOW_LIMIT); }
#[test]
fn event_enable_to_normal_only() {
let mut det = make_detector();
det.event_enable = 0x04;
assert!(det.evaluate(81.0).is_none());
assert_eq!(det.event_state, EventState::HIGH_LIMIT);
let change = det.evaluate(50.0).unwrap();
assert_eq!(change.from, EventState::HIGH_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
assert!(det.evaluate(19.0).is_none());
assert_eq!(det.event_state, EventState::LOW_LIMIT);
let change = det.evaluate(50.0).unwrap();
assert_eq!(change.from, EventState::LOW_LIMIT);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn event_enable_to_offnormal_only() {
let mut det = make_detector();
det.event_enable = 0x01;
let change = det.evaluate(81.0).unwrap();
assert_eq!(change.to, EventState::HIGH_LIMIT);
assert!(det.evaluate(50.0).is_none());
assert_eq!(det.event_state, EventState::NORMAL);
}
#[test]
fn event_state_change_generic() {
use bacnet_types::enums::EventType;
let change = EventStateChange {
from: EventState::NORMAL,
to: EventState::NORMAL,
};
assert_eq!(change.event_type(), EventType::CHANGE_OF_STATE);
}
#[test]
fn cos_normal_when_no_alarm_values() {
let mut det = ChangeOfStateDetector {
event_enable: 0x07,
..Default::default()
};
assert!(det.evaluate(0).is_none()); }
#[test]
fn cos_normal_to_offnormal() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1], event_enable: 0x07,
..Default::default()
};
let change = det.evaluate(1).unwrap();
assert_eq!(change.from, EventState::NORMAL);
assert_eq!(change.to, EventState::OFFNORMAL);
}
#[test]
fn cos_offnormal_to_normal() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: 0x07,
..Default::default()
};
det.evaluate(1); let change = det.evaluate(0).unwrap(); assert_eq!(change.from, EventState::OFFNORMAL);
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn cos_stays_offnormal_while_in_alarm() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![1],
event_enable: 0x07,
..Default::default()
};
det.evaluate(1); assert!(det.evaluate(1).is_none()); }
#[test]
fn cos_multistate_alarm_values() {
let mut det = ChangeOfStateDetector {
alarm_values: vec![3, 5, 7], event_enable: 0x07,
..Default::default()
};
assert!(det.evaluate(1).is_none()); let change = det.evaluate(5).unwrap();
assert_eq!(change.to, EventState::OFFNORMAL);
assert!(det.evaluate(3).is_none()); let change = det.evaluate(2).unwrap();
assert_eq!(change.to, EventState::NORMAL);
}
#[test]
fn cmdfail_matching_stays_normal() {
let mut det = CommandFailureDetector {
event_enable: 0x07,
..Default::default()
};
assert!(det.evaluate(1, 1).is_none()); }
#[test]
fn cmdfail_mismatch_goes_offnormal() {
let mut det = CommandFailureDetector {
event_enable: 0x07,
..Default::default()
};
let change = det.evaluate(1, 0).unwrap(); assert_eq!(change.to, EventState::OFFNORMAL);
}
#[test]
fn cmdfail_match_restores_normal() {
let mut det = CommandFailureDetector {
event_enable: 0x07,
..Default::default()
};
det.evaluate(1, 0); let change = det.evaluate(1, 1).unwrap(); assert_eq!(change.to, EventState::NORMAL);
}
}