1use bacnet_types::enums::{EventState, EventType};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EventStateChange {
12 pub from: EventState,
14 pub to: EventState,
16}
17
18impl EventStateChange {
19 pub fn event_type(&self) -> EventType {
24 if self.from == EventState::HIGH_LIMIT
25 || self.from == EventState::LOW_LIMIT
26 || self.to == EventState::HIGH_LIMIT
27 || self.to == EventState::LOW_LIMIT
28 {
29 EventType::OUT_OF_RANGE
30 } else {
31 EventType::CHANGE_OF_STATE
32 }
33 }
34
35 pub fn transition(&self) -> EventTransition {
42 if self.to == EventState::NORMAL {
43 EventTransition::ToNormal
44 } else if self.to == EventState::FAULT {
45 EventTransition::ToFault
46 } else {
47 EventTransition::ToOffnormal
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum EventTransition {
55 ToOffnormal,
57 ToFault,
59 ToNormal,
61}
62
63impl EventTransition {
64 pub fn bit_mask(self) -> u8 {
68 match self {
69 EventTransition::ToOffnormal => 0x01,
70 EventTransition::ToFault => 0x02,
71 EventTransition::ToNormal => 0x04,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct LimitEnable {
81 pub low_limit_enable: bool,
82 pub high_limit_enable: bool,
83}
84
85impl LimitEnable {
86 pub const NONE: Self = Self {
87 low_limit_enable: false,
88 high_limit_enable: false,
89 };
90
91 pub const BOTH: Self = Self {
92 low_limit_enable: true,
93 high_limit_enable: true,
94 };
95
96 pub fn to_bits(self) -> u8 {
98 let mut bits = 0u8;
99 if self.low_limit_enable {
100 bits |= 0x80; }
102 if self.high_limit_enable {
103 bits |= 0x40; }
105 bits
106 }
107
108 pub fn from_bits(byte: u8) -> Self {
110 Self {
111 low_limit_enable: byte & 0x80 != 0,
112 high_limit_enable: byte & 0x40 != 0,
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
127pub struct OutOfRangeDetector {
128 pub high_limit: f32,
129 pub low_limit: f32,
130 pub deadband: f32,
131 pub limit_enable: LimitEnable,
132 pub notification_class: u32,
133 pub notify_type: u32,
134 pub event_enable: u8,
135 pub time_delay: u32,
136 pub event_state: EventState,
137 pub acked_transitions: u8,
140}
141
142impl Default for OutOfRangeDetector {
143 fn default() -> Self {
144 Self {
145 high_limit: 100.0,
146 low_limit: 0.0,
147 deadband: 1.0,
148 limit_enable: LimitEnable::NONE,
149 notification_class: 0,
150 notify_type: 0, event_enable: 0,
152 time_delay: 0,
153 event_state: EventState::NORMAL,
154 acked_transitions: 0b111, }
156 }
157}
158
159impl OutOfRangeDetector {
160 const TO_OFFNORMAL: u8 = 0x01;
162 const TO_FAULT: u8 = 0x02;
163 const TO_NORMAL: u8 = 0x04;
164
165 pub fn evaluate(&mut self, present_value: f32) -> Option<EventStateChange> {
173 let new_state = self.compute_new_state(present_value);
174 if new_state != self.event_state {
175 let change = EventStateChange {
176 from: self.event_state,
177 to: new_state,
178 };
179 self.event_state = new_state;
180
181 let enabled = match new_state {
183 s if s == EventState::NORMAL => self.event_enable & Self::TO_NORMAL != 0,
184 s if s == EventState::HIGH_LIMIT || s == EventState::LOW_LIMIT => {
185 self.event_enable & Self::TO_OFFNORMAL != 0
186 }
187 _ => self.event_enable & Self::TO_FAULT != 0,
188 };
189
190 if enabled {
191 Some(change)
192 } else {
193 None
194 }
195 } else {
196 None
197 }
198 }
199
200 fn compute_new_state(&self, pv: f32) -> EventState {
201 let high_enabled = self.limit_enable.high_limit_enable;
202 let low_enabled = self.limit_enable.low_limit_enable;
203
204 match self.event_state {
205 s if s == EventState::NORMAL => {
206 if high_enabled && pv > self.high_limit {
208 return EventState::HIGH_LIMIT;
209 }
210 if low_enabled && pv < self.low_limit {
211 return EventState::LOW_LIMIT;
212 }
213 EventState::NORMAL
214 }
215 s if s == EventState::HIGH_LIMIT => {
216 if low_enabled && pv < self.low_limit {
218 return EventState::LOW_LIMIT;
219 }
220 if pv < self.high_limit - self.deadband {
222 return EventState::NORMAL;
223 }
224 EventState::HIGH_LIMIT
225 }
226 s if s == EventState::LOW_LIMIT => {
227 if high_enabled && pv > self.high_limit {
229 return EventState::HIGH_LIMIT;
230 }
231 if pv > self.low_limit + self.deadband {
233 return EventState::NORMAL;
234 }
235 EventState::LOW_LIMIT
236 }
237 _ => self.event_state, }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn make_detector() -> OutOfRangeDetector {
247 OutOfRangeDetector {
248 high_limit: 80.0,
249 low_limit: 20.0,
250 deadband: 2.0,
251 limit_enable: LimitEnable::BOTH,
252 notification_class: 1,
253 notify_type: 0,
254 event_enable: 0x07, time_delay: 0,
256 event_state: EventState::NORMAL,
257 acked_transitions: 0b111,
258 }
259 }
260
261 #[test]
262 fn normal_stays_normal_within_limits() {
263 let mut det = make_detector();
264 assert!(det.evaluate(50.0).is_none());
265 assert_eq!(det.event_state, EventState::NORMAL);
266 }
267
268 #[test]
269 fn normal_to_high_limit() {
270 let mut det = make_detector();
271 let change = det.evaluate(81.0).unwrap();
272 assert_eq!(change.from, EventState::NORMAL);
273 assert_eq!(change.to, EventState::HIGH_LIMIT);
274 assert_eq!(det.event_state, EventState::HIGH_LIMIT);
275 }
276
277 #[test]
278 fn normal_to_low_limit() {
279 let mut det = make_detector();
280 let change = det.evaluate(19.0).unwrap();
281 assert_eq!(change.from, EventState::NORMAL);
282 assert_eq!(change.to, EventState::LOW_LIMIT);
283 assert_eq!(det.event_state, EventState::LOW_LIMIT);
284 }
285
286 #[test]
287 fn at_boundary_no_transition() {
288 let mut det = make_detector();
289 assert!(det.evaluate(80.0).is_none());
291 assert!(det.evaluate(20.0).is_none());
293 }
294
295 #[test]
296 fn high_limit_to_normal_with_deadband() {
297 let mut det = make_detector();
298 det.evaluate(81.0); assert!(det.evaluate(79.0).is_none());
302
303 let change = det.evaluate(77.0).unwrap();
305 assert_eq!(change.from, EventState::HIGH_LIMIT);
306 assert_eq!(change.to, EventState::NORMAL);
307 }
308
309 #[test]
310 fn low_limit_to_normal_with_deadband() {
311 let mut det = make_detector();
312 det.evaluate(19.0); assert!(det.evaluate(21.0).is_none());
316
317 let change = det.evaluate(23.0).unwrap();
319 assert_eq!(change.from, EventState::LOW_LIMIT);
320 assert_eq!(change.to, EventState::NORMAL);
321 }
322
323 #[test]
324 fn high_limit_to_low_limit_direct() {
325 let mut det = make_detector();
326 det.evaluate(81.0); let change = det.evaluate(19.0).unwrap();
330 assert_eq!(change.from, EventState::HIGH_LIMIT);
331 assert_eq!(change.to, EventState::LOW_LIMIT);
332 }
333
334 #[test]
335 fn low_limit_to_high_limit_direct() {
336 let mut det = make_detector();
337 det.evaluate(19.0); let change = det.evaluate(81.0).unwrap();
341 assert_eq!(change.from, EventState::LOW_LIMIT);
342 assert_eq!(change.to, EventState::HIGH_LIMIT);
343 }
344
345 #[test]
346 fn high_limit_disabled_no_transition() {
347 let mut det = make_detector();
348 det.limit_enable.high_limit_enable = false;
349
350 assert!(det.evaluate(100.0).is_none());
352 }
353
354 #[test]
355 fn low_limit_disabled_no_transition() {
356 let mut det = make_detector();
357 det.limit_enable.low_limit_enable = false;
358
359 assert!(det.evaluate(0.0).is_none());
361 }
362
363 #[test]
364 fn both_limits_disabled() {
365 let mut det = make_detector();
366 det.limit_enable = LimitEnable::NONE;
367 assert!(det.evaluate(100.0).is_none());
368 assert!(det.evaluate(0.0).is_none());
369 }
370
371 #[test]
372 fn limit_enable_bits_round_trip() {
373 let le = LimitEnable::BOTH;
374 let bits = le.to_bits();
375 let decoded = LimitEnable::from_bits(bits);
376 assert_eq!(decoded, le);
377
378 let le = LimitEnable {
379 low_limit_enable: true,
380 high_limit_enable: false,
381 };
382 let bits = le.to_bits();
383 let decoded = LimitEnable::from_bits(bits);
384 assert_eq!(decoded, le);
385 }
386
387 #[test]
388 fn deadband_at_exact_boundary() {
389 let mut det = make_detector();
390 det.evaluate(81.0); assert!(det.evaluate(78.0).is_none());
394
395 let change = det.evaluate(77.99).unwrap();
397 assert_eq!(change.to, EventState::NORMAL);
398 }
399
400 #[test]
401 fn event_state_change_derives_event_type() {
402 use bacnet_types::enums::EventType;
403
404 let change = EventStateChange {
405 from: EventState::NORMAL,
406 to: EventState::HIGH_LIMIT,
407 };
408 assert_eq!(change.event_type(), EventType::OUT_OF_RANGE);
409 }
410
411 #[test]
412 fn event_state_change_to_normal_from_high() {
413 use bacnet_types::enums::EventType;
414
415 let change = EventStateChange {
416 from: EventState::HIGH_LIMIT,
417 to: EventState::NORMAL,
418 };
419 assert_eq!(change.event_type(), EventType::OUT_OF_RANGE);
420 }
421
422 #[test]
423 fn event_enable_zero_suppresses_all_notifications() {
424 let mut det = make_detector();
425 det.event_enable = 0x00; assert!(det.evaluate(81.0).is_none());
429 assert_eq!(det.event_state, EventState::HIGH_LIMIT); assert!(det.evaluate(50.0).is_none());
432 assert_eq!(det.event_state, EventState::NORMAL); assert!(det.evaluate(19.0).is_none());
435 assert_eq!(det.event_state, EventState::LOW_LIMIT); }
437
438 #[test]
439 fn event_enable_to_normal_only() {
440 let mut det = make_detector();
441 det.event_enable = 0x04; assert!(det.evaluate(81.0).is_none());
445 assert_eq!(det.event_state, EventState::HIGH_LIMIT);
446
447 let change = det.evaluate(50.0).unwrap();
449 assert_eq!(change.from, EventState::HIGH_LIMIT);
450 assert_eq!(change.to, EventState::NORMAL);
451
452 assert!(det.evaluate(19.0).is_none());
454 assert_eq!(det.event_state, EventState::LOW_LIMIT);
455
456 let change = det.evaluate(50.0).unwrap();
458 assert_eq!(change.from, EventState::LOW_LIMIT);
459 assert_eq!(change.to, EventState::NORMAL);
460 }
461
462 #[test]
463 fn event_enable_to_offnormal_only() {
464 let mut det = make_detector();
465 det.event_enable = 0x01; let change = det.evaluate(81.0).unwrap();
469 assert_eq!(change.to, EventState::HIGH_LIMIT);
470
471 assert!(det.evaluate(50.0).is_none());
473 assert_eq!(det.event_state, EventState::NORMAL);
474 }
475
476 #[test]
477 fn event_state_change_generic() {
478 use bacnet_types::enums::EventType;
479
480 let change = EventStateChange {
481 from: EventState::NORMAL,
482 to: EventState::NORMAL,
483 };
484 assert_eq!(change.event_type(), EventType::CHANGE_OF_STATE);
485 }
486}