Skip to main content

bacnet_server/event_enrollment/
mod.rs

1//! Event Enrollment algorithmic evaluation.
2//!
3//! Unlike intrinsic reporting (built into object types), Event Enrollment is a
4//! separate object that monitors another object's property and evaluates an
5//! algorithm against it.
6//!
7//! Supported algorithms: OUT_OF_RANGE, FLOATING_LIMIT, CHANGE_OF_STATE,
8//! CHANGE_OF_BITSTRING, CHANGE_OF_VALUE.
9
10use bacnet_objects::database::ObjectDatabase;
11use bacnet_objects::event::EventStateChange;
12use bacnet_types::enums::{EventState, EventType, ObjectType, PropertyIdentifier};
13use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
14
15/// A state transition detected during event enrollment evaluation.
16#[derive(Debug, Clone, PartialEq)]
17pub struct EventEnrollmentTransition {
18    /// The EventEnrollment object that detected the transition.
19    pub enrollment_oid: ObjectIdentifier,
20    /// The monitored object whose property triggered the transition.
21    pub monitored_oid: ObjectIdentifier,
22    /// The detected state change.
23    pub change: EventStateChange,
24    /// The event type that was evaluated.
25    pub event_type: EventType,
26}
27
28// ---- Event parameter encoding helpers ----
29
30/// Encode OUT_OF_RANGE parameters: `[high_limit: f32 LE][low_limit: f32 LE][deadband: f32 LE]`
31pub fn encode_out_of_range_params(high_limit: f32, low_limit: f32, deadband: f32) -> Vec<u8> {
32    let mut buf = Vec::with_capacity(12);
33    buf.extend_from_slice(&high_limit.to_le_bytes());
34    buf.extend_from_slice(&low_limit.to_le_bytes());
35    buf.extend_from_slice(&deadband.to_le_bytes());
36    buf
37}
38
39/// Encode FLOATING_LIMIT parameters:
40/// `[setpoint: f32 LE][high_diff: f32 LE][low_diff: f32 LE][deadband: f32 LE]`
41pub fn encode_floating_limit_params(
42    setpoint: f32,
43    high_diff_limit: f32,
44    low_diff_limit: f32,
45    deadband: f32,
46) -> Vec<u8> {
47    let mut buf = Vec::with_capacity(16);
48    buf.extend_from_slice(&setpoint.to_le_bytes());
49    buf.extend_from_slice(&high_diff_limit.to_le_bytes());
50    buf.extend_from_slice(&low_diff_limit.to_le_bytes());
51    buf.extend_from_slice(&deadband.to_le_bytes());
52    buf
53}
54
55/// Encode CHANGE_OF_STATE parameters: `[count: u32 LE][alarm_values: u32 LE ...]`
56pub fn encode_change_of_state_params(alarm_values: &[u32]) -> Vec<u8> {
57    let mut buf = Vec::with_capacity(4 + alarm_values.len() * 4);
58    buf.extend_from_slice(&(alarm_values.len() as u32).to_le_bytes());
59    for &v in alarm_values {
60        buf.extend_from_slice(&v.to_le_bytes());
61    }
62    buf
63}
64
65/// Encode CHANGE_OF_VALUE parameters: `[increment: f32 LE]`
66pub fn encode_change_of_value_params(increment: f32) -> Vec<u8> {
67    increment.to_le_bytes().to_vec()
68}
69
70/// Encode CHANGE_OF_BITSTRING parameters:
71/// `[mask_len: u32 LE][mask_bytes ...][alarm_bits ...]`
72pub fn encode_change_of_bitstring_params(mask: &[u8], alarm_bits: &[u8]) -> Vec<u8> {
73    let len = mask.len().min(alarm_bits.len());
74    let mut buf = Vec::with_capacity(4 + len * 2);
75    buf.extend_from_slice(&(len as u32).to_le_bytes());
76    buf.extend_from_slice(&mask[..len]);
77    buf.extend_from_slice(&alarm_bits[..len]);
78    buf
79}
80
81// ---- Algorithm evaluation ----
82
83/// Evaluate the OUT_OF_RANGE algorithm.
84///
85/// Compares a real present_value against high/low limits with deadband hysteresis.
86fn eval_out_of_range(params: &[u8], value: f32, current: EventState) -> EventState {
87    if params.len() < 12 {
88        return current;
89    }
90    let high_limit = f32::from_le_bytes([params[0], params[1], params[2], params[3]]);
91    let low_limit = f32::from_le_bytes([params[4], params[5], params[6], params[7]]);
92    let deadband = f32::from_le_bytes([params[8], params[9], params[10], params[11]]);
93
94    match current {
95        s if s == EventState::NORMAL => {
96            if value > high_limit {
97                EventState::HIGH_LIMIT
98            } else if value < low_limit {
99                EventState::LOW_LIMIT
100            } else {
101                EventState::NORMAL
102            }
103        }
104        s if s == EventState::HIGH_LIMIT => {
105            if value < low_limit {
106                EventState::LOW_LIMIT
107            } else if value < high_limit - deadband {
108                EventState::NORMAL
109            } else {
110                EventState::HIGH_LIMIT
111            }
112        }
113        s if s == EventState::LOW_LIMIT => {
114            if value > high_limit {
115                EventState::HIGH_LIMIT
116            } else if value > low_limit + deadband {
117                EventState::NORMAL
118            } else {
119                EventState::LOW_LIMIT
120            }
121        }
122        _ => current,
123    }
124}
125
126/// Evaluate the FLOATING_LIMIT algorithm.
127///
128/// Compares a real present_value against a setpoint +/- differential limits
129/// with deadband hysteresis.
130fn eval_floating_limit(params: &[u8], value: f32, current: EventState) -> EventState {
131    if params.len() < 16 {
132        return current;
133    }
134    let setpoint = f32::from_le_bytes([params[0], params[1], params[2], params[3]]);
135    let high_diff = f32::from_le_bytes([params[4], params[5], params[6], params[7]]);
136    let low_diff = f32::from_le_bytes([params[8], params[9], params[10], params[11]]);
137    let deadband = f32::from_le_bytes([params[12], params[13], params[14], params[15]]);
138
139    let high_limit = setpoint + high_diff;
140    let low_limit = setpoint - low_diff;
141
142    match current {
143        s if s == EventState::NORMAL => {
144            if value > high_limit {
145                EventState::HIGH_LIMIT
146            } else if value < low_limit {
147                EventState::LOW_LIMIT
148            } else {
149                EventState::NORMAL
150            }
151        }
152        s if s == EventState::HIGH_LIMIT => {
153            if value < low_limit {
154                EventState::LOW_LIMIT
155            } else if value < high_limit - deadband {
156                EventState::NORMAL
157            } else {
158                EventState::HIGH_LIMIT
159            }
160        }
161        s if s == EventState::LOW_LIMIT => {
162            if value > high_limit {
163                EventState::HIGH_LIMIT
164            } else if value > low_limit + deadband {
165                EventState::NORMAL
166            } else {
167                EventState::LOW_LIMIT
168            }
169        }
170        _ => current,
171    }
172}
173
174/// Evaluate the CHANGE_OF_STATE algorithm.
175///
176/// OFFNORMAL if the value matches any alarm value, otherwise NORMAL.
177fn eval_change_of_state(params: &[u8], value: u32, _current: EventState) -> EventState {
178    if params.len() < 4 {
179        return EventState::NORMAL;
180    }
181    let count = u32::from_le_bytes([params[0], params[1], params[2], params[3]]) as usize;
182    let needed = 4usize.saturating_add(count.saturating_mul(4));
183    if params.len() < needed {
184        return EventState::NORMAL;
185    }
186    for i in 0..count {
187        let offset = 4 + i * 4;
188        let alarm_val = u32::from_le_bytes([
189            params[offset],
190            params[offset + 1],
191            params[offset + 2],
192            params[offset + 3],
193        ]);
194        if value == alarm_val {
195            return EventState::OFFNORMAL;
196        }
197    }
198    EventState::NORMAL
199}
200
201/// Evaluate the CHANGE_OF_BITSTRING algorithm.
202///
203/// Applies a mask to the monitored bitstring and compares against the alarm pattern.
204fn eval_change_of_bitstring(params: &[u8], value_bits: &[u8], _current: EventState) -> EventState {
205    if params.len() < 4 {
206        return EventState::NORMAL;
207    }
208    let mask_len = u32::from_le_bytes([params[0], params[1], params[2], params[3]]) as usize;
209    let needed = 4usize.saturating_add(mask_len.saturating_mul(2));
210    if params.len() < needed {
211        return EventState::NORMAL;
212    }
213
214    let mask = &params[4..4 + mask_len];
215    let alarm_bits = &params[4 + mask_len..4 + 2 * mask_len];
216
217    for i in 0..mask_len {
218        let monitored_byte = value_bits.get(i).copied().unwrap_or(0);
219        if (monitored_byte & mask[i]) != (alarm_bits[i] & mask[i]) {
220            return EventState::NORMAL;
221        }
222    }
223    EventState::OFFNORMAL
224}
225
226/// Evaluate the CHANGE_OF_VALUE algorithm.
227///
228/// OFFNORMAL if |current_value| >= increment, otherwise NORMAL.
229fn eval_change_of_value(params: &[u8], value: f32, _current: EventState) -> EventState {
230    if params.len() < 4 {
231        return EventState::NORMAL;
232    }
233    let increment = f32::from_le_bytes([params[0], params[1], params[2], params[3]]);
234    if increment <= 0.0 || !increment.is_finite() {
235        return EventState::NORMAL;
236    }
237    if value.abs() >= increment {
238        EventState::OFFNORMAL
239    } else {
240        EventState::NORMAL
241    }
242}
243
244/// Extract a real (f32) value from a PropertyValue.
245fn extract_real(pv: &PropertyValue) -> Option<f32> {
246    match pv {
247        PropertyValue::Real(v) => Some(*v),
248        PropertyValue::Double(v) => Some(*v as f32),
249        PropertyValue::Unsigned(v) => Some(*v as f32),
250        PropertyValue::Signed(v) => Some(*v as f32),
251        _ => None,
252    }
253}
254
255/// Extract an enumerated (u32) value from a PropertyValue.
256fn extract_enumerated(pv: &PropertyValue) -> Option<u32> {
257    match pv {
258        PropertyValue::Enumerated(v) => Some(*v),
259        PropertyValue::Unsigned(v) => Some(*v as u32),
260        _ => None,
261    }
262}
263
264/// Extract bitstring bytes from a PropertyValue.
265fn extract_bitstring(pv: &PropertyValue) -> Option<Vec<u8>> {
266    match pv {
267        PropertyValue::BitString { data, .. } => Some(data.clone()),
268        _ => None,
269    }
270}
271
272/// Read the object_property_reference from an EventEnrollment object.
273///
274/// Returns (monitored_object_id, monitored_property_id) if valid.
275fn read_object_property_ref(
276    enrollment: &dyn bacnet_objects::traits::BACnetObject,
277) -> Option<(ObjectIdentifier, PropertyIdentifier)> {
278    match enrollment.read_property(PropertyIdentifier::OBJECT_PROPERTY_REFERENCE, None) {
279        Ok(PropertyValue::List(ref items)) if items.len() >= 2 => {
280            let obj_id = match &items[0] {
281                PropertyValue::ObjectIdentifier(oid) => *oid,
282                _ => return None,
283            };
284            let prop_id = match &items[1] {
285                PropertyValue::Unsigned(v) => PropertyIdentifier::from_raw(*v as u32),
286                _ => return None,
287            };
288            Some((obj_id, prop_id))
289        }
290        _ => None,
291    }
292}
293
294/// Evaluate all EventEnrollment objects in the database.
295///
296/// For each active enrollment, reads the monitored property, evaluates the
297/// configured algorithm, and returns any state transitions.
298pub fn evaluate_event_enrollments(db: &mut ObjectDatabase) -> Vec<EventEnrollmentTransition> {
299    let oids = db.find_by_type(ObjectType::EVENT_ENROLLMENT);
300
301    let mut updates: Vec<(
302        ObjectIdentifier,
303        ObjectIdentifier,
304        u32,
305        EventState,
306        EventState,
307    )> = Vec::new();
308
309    for oid in &oids {
310        let Some(enrollment) = db.get(oid) else {
311            continue;
312        };
313
314        if let Ok(PropertyValue::Boolean(true)) =
315            enrollment.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
316        {
317            continue;
318        }
319
320        let event_type_raw = match enrollment.read_property(PropertyIdentifier::EVENT_TYPE, None) {
321            Ok(PropertyValue::Enumerated(v)) => v,
322            _ => continue,
323        };
324
325        let current_state = match enrollment.read_property(PropertyIdentifier::EVENT_STATE, None) {
326            Ok(PropertyValue::Enumerated(v)) => EventState::from_raw(v),
327            _ => continue,
328        };
329
330        let event_enable = match enrollment.read_property(PropertyIdentifier::EVENT_ENABLE, None) {
331            Ok(PropertyValue::BitString { data, .. }) => data.first().map(|b| b >> 5).unwrap_or(0),
332            _ => 0,
333        };
334
335        let params = match enrollment.read_property(PropertyIdentifier::EVENT_PARAMETERS, None) {
336            Ok(PropertyValue::OctetString(bytes)) => bytes,
337            _ => Vec::new(),
338        };
339
340        let Some((monitored_oid, monitored_prop)) = read_object_property_ref(enrollment) else {
341            continue;
342        };
343
344        let Some(monitored_obj) = db.get(&monitored_oid) else {
345            continue;
346        };
347        let monitored_value = match monitored_obj.read_property(monitored_prop, None) {
348            Ok(v) => v,
349            Err(_) => continue,
350        };
351
352        let event_type = EventType::from_raw(event_type_raw);
353        let new_state = if event_type == EventType::OUT_OF_RANGE {
354            let Some(val) = extract_real(&monitored_value) else {
355                continue;
356            };
357            eval_out_of_range(&params, val, current_state)
358        } else if event_type == EventType::FLOATING_LIMIT {
359            let Some(val) = extract_real(&monitored_value) else {
360                continue;
361            };
362            eval_floating_limit(&params, val, current_state)
363        } else if event_type == EventType::CHANGE_OF_STATE {
364            let Some(val) = extract_enumerated(&monitored_value) else {
365                continue;
366            };
367            eval_change_of_state(&params, val, current_state)
368        } else if event_type == EventType::CHANGE_OF_BITSTRING {
369            let Some(bits) = extract_bitstring(&monitored_value) else {
370                continue;
371            };
372            eval_change_of_bitstring(&params, &bits, current_state)
373        } else if event_type == EventType::CHANGE_OF_VALUE {
374            let Some(val) = extract_real(&monitored_value) else {
375                continue;
376            };
377            eval_change_of_value(&params, val, current_state)
378        } else {
379            continue;
380        };
381
382        if new_state == current_state {
383            continue;
384        }
385
386        let transition_enabled = match new_state {
387            s if s == EventState::NORMAL => event_enable & 0x04 != 0,
388            s if s == EventState::HIGH_LIMIT
389                || s == EventState::LOW_LIMIT
390                || s == EventState::OFFNORMAL =>
391            {
392                event_enable & 0x01 != 0
393            }
394            _ => event_enable & 0x02 != 0,
395        };
396
397        if transition_enabled {
398            updates.push((
399                *oid,
400                monitored_oid,
401                event_type_raw,
402                current_state,
403                new_state,
404            ));
405        }
406    }
407
408    let mut transitions = Vec::new();
409    for (oid, monitored_oid, event_type_raw, from_state, to_state) in updates {
410        if let Some(obj) = db.get_mut(&oid) {
411            if obj
412                .write_property(
413                    PropertyIdentifier::EVENT_STATE,
414                    None,
415                    PropertyValue::Enumerated(to_state.to_raw()),
416                    None,
417                )
418                .is_ok()
419            {
420                transitions.push(EventEnrollmentTransition {
421                    enrollment_oid: oid,
422                    monitored_oid,
423                    change: EventStateChange {
424                        from: from_state,
425                        to: to_state,
426                    },
427                    event_type: EventType::from_raw(event_type_raw),
428                });
429            }
430        }
431    }
432
433    transitions
434}
435
436#[cfg(test)]
437mod tests;