Skip to main content

bacnet_services/
enrollment_summary.rs

1//! GetEnrollmentSummary service per ASHRAE 135-2020 Clause 13.8.
2
3use bacnet_encoding::primitives;
4use bacnet_encoding::tags;
5use bacnet_types::enums::{EventState, EventType};
6use bacnet_types::error::Error;
7use bacnet_types::primitives::ObjectIdentifier;
8use bytes::BytesMut;
9
10use crate::common::MAX_DECODED_ITEMS;
11
12// ---------------------------------------------------------------------------
13// GetEnrollmentSummaryRequest
14// ---------------------------------------------------------------------------
15
16/// Priority filter sub-structure.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct PriorityFilter {
19    pub min_priority: u8,
20    pub max_priority: u8,
21}
22
23/// BACnetRecipientProcess — identifies a notification recipient.
24///
25/// Simplified: only the `device` choice of BACnetRecipient is supported,
26/// plus the process identifier.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RecipientProcess {
29    /// Device object identifier (from BACnetRecipient CHOICE [0] device).
30    pub device: Option<ObjectIdentifier>,
31    /// Process identifier.
32    pub process_identifier: u32,
33}
34
35/// GetEnrollmentSummary-Request service parameters.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct GetEnrollmentSummaryRequest {
38    /// [0] acknowledgmentFilter: all(0), acked(1), not-acked(2).
39    pub acknowledgment_filter: u32,
40    /// [1] enrollmentFilter (optional) — BACnetRecipientProcess.
41    pub enrollment_filter: Option<RecipientProcess>,
42    /// [2] eventStateFilter (optional).
43    pub event_state_filter: Option<EventState>,
44    /// [3] eventTypeFilter (optional).
45    pub event_type_filter: Option<EventType>,
46    /// [4] priorityFilter { [0] minPriority, [1] maxPriority } (optional).
47    pub priority_filter: Option<PriorityFilter>,
48    /// [5] notificationClassFilter (optional).
49    pub notification_class_filter: Option<u16>,
50}
51
52impl GetEnrollmentSummaryRequest {
53    pub fn encode(&self, buf: &mut BytesMut) {
54        // [0] acknowledgmentFilter
55        primitives::encode_ctx_enumerated(buf, 0, self.acknowledgment_filter);
56        // [1] enrollmentFilter (optional, constructed)
57        if let Some(ref ef) = self.enrollment_filter {
58            tags::encode_opening_tag(buf, 1);
59            // recipient CHOICE: [0] device
60            if let Some(ref device) = ef.device {
61                tags::encode_opening_tag(buf, 0);
62                primitives::encode_ctx_object_id(buf, 0, device);
63                tags::encode_closing_tag(buf, 0);
64            }
65            // processIdentifier
66            primitives::encode_ctx_unsigned(buf, 1, ef.process_identifier as u64);
67            tags::encode_closing_tag(buf, 1);
68        }
69        // [2] eventStateFilter (optional)
70        if let Some(es) = self.event_state_filter {
71            primitives::encode_ctx_enumerated(buf, 2, es.to_raw());
72        }
73        // [3] eventTypeFilter (optional)
74        if let Some(et) = self.event_type_filter {
75            primitives::encode_ctx_enumerated(buf, 3, et.to_raw());
76        }
77        // [4] priorityFilter (optional, constructed)
78        if let Some(pf) = self.priority_filter {
79            tags::encode_opening_tag(buf, 4);
80            primitives::encode_ctx_unsigned(buf, 0, pf.min_priority as u64);
81            primitives::encode_ctx_unsigned(buf, 1, pf.max_priority as u64);
82            tags::encode_closing_tag(buf, 4);
83        }
84        // [5] notificationClassFilter (optional)
85        if let Some(nc) = self.notification_class_filter {
86            primitives::encode_ctx_unsigned(buf, 5, nc as u64);
87        }
88    }
89
90    pub fn decode(data: &[u8]) -> Result<Self, Error> {
91        let mut offset = 0;
92
93        // [0] acknowledgmentFilter
94        let (tag, pos) = tags::decode_tag(data, offset)?;
95        let end = pos + tag.length as usize;
96        if end > data.len() {
97            return Err(Error::decoding(
98                pos,
99                "EnrollmentSummary truncated at acknowledgmentFilter",
100            ));
101        }
102        let acknowledgment_filter = primitives::decode_unsigned(&data[pos..end])? as u32;
103        offset = end;
104
105        // [1] enrollmentFilter (optional, constructed)
106        let mut enrollment_filter = None;
107        if offset < data.len() {
108            let (tag, tag_end) = tags::decode_tag(data, offset)?;
109            if tag.is_opening_tag(1) {
110                // Parse BACnetRecipientProcess
111                let mut device = None;
112                let mut process_id = 0u32;
113                let (content, new_offset) = tags::extract_context_value(data, tag_end, 1)?;
114                let mut inner_offset = 0;
115                while inner_offset < content.len() {
116                    let (inner_tag, inner_pos) = tags::decode_tag(content, inner_offset)?;
117                    if inner_tag.is_opening_tag(0) {
118                        // recipient CHOICE — parse device [0]
119                        let (recipient_content, recipient_end) =
120                            tags::extract_context_value(content, inner_pos, 0)?;
121                        if !recipient_content.is_empty() {
122                            let (dev_tag, dev_pos) = tags::decode_tag(recipient_content, 0)?;
123                            if dev_tag.is_context(0) {
124                                let dev_end = dev_pos + dev_tag.length as usize;
125                                if dev_end <= recipient_content.len() {
126                                    device = Some(ObjectIdentifier::decode(
127                                        &recipient_content[dev_pos..dev_end],
128                                    )?);
129                                }
130                            }
131                        }
132                        inner_offset = recipient_end;
133                    } else if inner_tag.is_context(1) {
134                        let inner_end = inner_pos + inner_tag.length as usize;
135                        if inner_end <= content.len() {
136                            process_id =
137                                primitives::decode_unsigned(&content[inner_pos..inner_end])? as u32;
138                        }
139                        inner_offset = inner_end;
140                    } else {
141                        inner_offset = inner_pos + inner_tag.length as usize;
142                    }
143                }
144                enrollment_filter = Some(RecipientProcess {
145                    device,
146                    process_identifier: process_id,
147                });
148                offset = new_offset;
149            }
150        }
151
152        // [2] eventStateFilter (optional)
153        let mut event_state_filter = None;
154        let (opt_data, new_offset) = tags::decode_optional_context(data, offset, 2)?;
155        if let Some(content) = opt_data {
156            event_state_filter = Some(EventState::from_raw(
157                primitives::decode_unsigned(content)? as u32
158            ));
159            offset = new_offset;
160        }
161
162        // [3] eventTypeFilter (optional)
163        let mut event_type_filter = None;
164        let (opt_data, new_offset) = tags::decode_optional_context(data, offset, 3)?;
165        if let Some(content) = opt_data {
166            event_type_filter = Some(EventType::from_raw(
167                primitives::decode_unsigned(content)? as u32
168            ));
169            offset = new_offset;
170        }
171
172        // [4] priorityFilter (optional, constructed)
173        let mut priority_filter = None;
174        if offset < data.len() {
175            let (tag, tag_end) = tags::decode_tag(data, offset)?;
176            if tag.is_opening_tag(4) {
177                // [0] minPriority
178                let (inner_tag, inner_pos) = tags::decode_tag(data, tag_end)?;
179                let inner_end = inner_pos + inner_tag.length as usize;
180                if inner_end > data.len() {
181                    return Err(Error::decoding(
182                        inner_pos,
183                        "EnrollmentSummary truncated at minPriority",
184                    ));
185                }
186                let min_priority = primitives::decode_unsigned(&data[inner_pos..inner_end])? as u8;
187
188                // [1] maxPriority
189                let (inner_tag, inner_pos) = tags::decode_tag(data, inner_end)?;
190                let inner_end = inner_pos + inner_tag.length as usize;
191                if inner_end > data.len() {
192                    return Err(Error::decoding(
193                        inner_pos,
194                        "EnrollmentSummary truncated at maxPriority",
195                    ));
196                }
197                let max_priority = primitives::decode_unsigned(&data[inner_pos..inner_end])? as u8;
198
199                // closing tag 4
200                let (close_tag, close_end) = tags::decode_tag(data, inner_end)?;
201                if !close_tag.is_closing_tag(4) {
202                    return Err(Error::decoding(
203                        inner_end,
204                        "EnrollmentSummary expected closing tag 4",
205                    ));
206                }
207                priority_filter = Some(PriorityFilter {
208                    min_priority,
209                    max_priority,
210                });
211                offset = close_end;
212            }
213        }
214
215        // [5] notificationClassFilter (optional)
216        let mut notification_class_filter = None;
217        if offset < data.len() {
218            let (opt_data, _new_offset) = tags::decode_optional_context(data, offset, 5)?;
219            if let Some(content) = opt_data {
220                notification_class_filter = Some(primitives::decode_unsigned(content)? as u16);
221            }
222        }
223
224        Ok(Self {
225            acknowledgment_filter,
226            enrollment_filter,
227            event_state_filter,
228            event_type_filter,
229            priority_filter,
230            notification_class_filter,
231        })
232    }
233}
234
235// ---------------------------------------------------------------------------
236// GetEnrollmentSummaryAck
237// ---------------------------------------------------------------------------
238
239/// One entry in the GetEnrollmentSummary-ACK sequence.
240#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct EnrollmentSummaryEntry {
242    pub object_identifier: ObjectIdentifier,
243    pub event_type: EventType,
244    pub event_state: EventState,
245    pub priority: u8,
246    pub notification_class: u16,
247}
248
249/// GetEnrollmentSummary-ACK: a sequence of summary entries.
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct GetEnrollmentSummaryAck {
252    pub entries: Vec<EnrollmentSummaryEntry>,
253}
254
255impl GetEnrollmentSummaryAck {
256    pub fn encode(&self, buf: &mut BytesMut) {
257        for entry in &self.entries {
258            primitives::encode_app_object_id(buf, &entry.object_identifier);
259            primitives::encode_app_enumerated(buf, entry.event_type.to_raw());
260            primitives::encode_app_enumerated(buf, entry.event_state.to_raw());
261            primitives::encode_app_unsigned(buf, entry.priority as u64);
262            primitives::encode_app_unsigned(buf, entry.notification_class as u64);
263        }
264    }
265
266    pub fn decode(data: &[u8]) -> Result<Self, Error> {
267        let mut entries = Vec::new();
268        let mut offset = 0;
269
270        while offset < data.len() {
271            if entries.len() >= MAX_DECODED_ITEMS {
272                return Err(Error::decoding(
273                    offset,
274                    "EnrollmentSummaryAck too many entries",
275                ));
276            }
277
278            // objectIdentifier (app)
279            let (tag, pos) = tags::decode_tag(data, offset)?;
280            let end = pos + tag.length as usize;
281            if end > data.len() {
282                return Err(Error::decoding(
283                    pos,
284                    "EnrollmentSummaryAck truncated at object-id",
285                ));
286            }
287            let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
288            offset = end;
289
290            // eventType (app enumerated)
291            let (tag, pos) = tags::decode_tag(data, offset)?;
292            let end = pos + tag.length as usize;
293            if end > data.len() {
294                return Err(Error::decoding(
295                    pos,
296                    "EnrollmentSummaryAck truncated at eventType",
297                ));
298            }
299            let event_type =
300                EventType::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
301            offset = end;
302
303            // eventState (app enumerated)
304            let (tag, pos) = tags::decode_tag(data, offset)?;
305            let end = pos + tag.length as usize;
306            if end > data.len() {
307                return Err(Error::decoding(
308                    pos,
309                    "EnrollmentSummaryAck truncated at eventState",
310                ));
311            }
312            let event_state =
313                EventState::from_raw(primitives::decode_unsigned(&data[pos..end])? as u32);
314            offset = end;
315
316            // priority (app unsigned)
317            let (tag, pos) = tags::decode_tag(data, offset)?;
318            let end = pos + tag.length as usize;
319            if end > data.len() {
320                return Err(Error::decoding(
321                    pos,
322                    "EnrollmentSummaryAck truncated at priority",
323                ));
324            }
325            let priority = primitives::decode_unsigned(&data[pos..end])? as u8;
326            offset = end;
327
328            // notificationClass (app unsigned)
329            let (tag, pos) = tags::decode_tag(data, offset)?;
330            let end = pos + tag.length as usize;
331            if end > data.len() {
332                return Err(Error::decoding(
333                    pos,
334                    "EnrollmentSummaryAck truncated at notificationClass",
335                ));
336            }
337            let notification_class = primitives::decode_unsigned(&data[pos..end])? as u16;
338            offset = end;
339
340            entries.push(EnrollmentSummaryEntry {
341                object_identifier,
342                event_type,
343                event_state,
344                priority,
345                notification_class,
346            });
347        }
348
349        Ok(Self { entries })
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use bacnet_types::enums::ObjectType;
357
358    #[test]
359    fn request_round_trip() {
360        let req = GetEnrollmentSummaryRequest {
361            acknowledgment_filter: 0, // all
362            enrollment_filter: None,
363            event_state_filter: Some(EventState::OFFNORMAL),
364            event_type_filter: None,
365            priority_filter: Some(PriorityFilter {
366                min_priority: 1,
367                max_priority: 10,
368            }),
369            notification_class_filter: Some(5),
370        };
371        let mut buf = BytesMut::new();
372        req.encode(&mut buf);
373        let decoded = GetEnrollmentSummaryRequest::decode(&buf).unwrap();
374        assert_eq!(req, decoded);
375    }
376
377    #[test]
378    fn request_minimal_round_trip() {
379        let req = GetEnrollmentSummaryRequest {
380            acknowledgment_filter: 2, // not-acked
381            enrollment_filter: None,
382            event_state_filter: None,
383            event_type_filter: None,
384            priority_filter: None,
385            notification_class_filter: None,
386        };
387        let mut buf = BytesMut::new();
388        req.encode(&mut buf);
389        let decoded = GetEnrollmentSummaryRequest::decode(&buf).unwrap();
390        assert_eq!(req, decoded);
391    }
392
393    #[test]
394    fn ack_round_trip() {
395        let ack = GetEnrollmentSummaryAck {
396            entries: vec![
397                EnrollmentSummaryEntry {
398                    object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
399                    event_type: EventType::OUT_OF_RANGE,
400                    event_state: EventState::HIGH_LIMIT,
401                    priority: 3,
402                    notification_class: 10,
403                },
404                EnrollmentSummaryEntry {
405                    object_identifier: ObjectIdentifier::new(ObjectType::BINARY_INPUT, 5).unwrap(),
406                    event_type: EventType::CHANGE_OF_STATE,
407                    event_state: EventState::NORMAL,
408                    priority: 7,
409                    notification_class: 20,
410                },
411            ],
412        };
413        let mut buf = BytesMut::new();
414        ack.encode(&mut buf);
415        let decoded = GetEnrollmentSummaryAck::decode(&buf).unwrap();
416        assert_eq!(ack, decoded);
417    }
418
419    #[test]
420    fn ack_empty_round_trip() {
421        let ack = GetEnrollmentSummaryAck { entries: vec![] };
422        let mut buf = BytesMut::new();
423        ack.encode(&mut buf);
424        let decoded = GetEnrollmentSummaryAck::decode(&buf).unwrap();
425        assert_eq!(ack, decoded);
426    }
427
428    // -----------------------------------------------------------------------
429    // Malformed-input decode error tests
430    // -----------------------------------------------------------------------
431
432    #[test]
433    fn test_decode_request_empty_input() {
434        assert!(GetEnrollmentSummaryRequest::decode(&[]).is_err());
435    }
436
437    #[test]
438    fn test_decode_request_truncated_1_byte() {
439        let req = GetEnrollmentSummaryRequest {
440            acknowledgment_filter: 0,
441            enrollment_filter: None,
442            event_state_filter: Some(EventState::FAULT),
443            event_type_filter: None,
444            priority_filter: None,
445            notification_class_filter: None,
446        };
447        let mut buf = BytesMut::new();
448        req.encode(&mut buf);
449        assert!(GetEnrollmentSummaryRequest::decode(&buf[..1]).is_err());
450    }
451
452    #[test]
453    fn test_decode_request_invalid_tag() {
454        assert!(GetEnrollmentSummaryRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
455    }
456
457    #[test]
458    fn test_decode_ack_truncated_1_byte() {
459        let ack = GetEnrollmentSummaryAck {
460            entries: vec![EnrollmentSummaryEntry {
461                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
462                event_type: EventType::OUT_OF_RANGE,
463                event_state: EventState::HIGH_LIMIT,
464                priority: 3,
465                notification_class: 10,
466            }],
467        };
468        let mut buf = BytesMut::new();
469        ack.encode(&mut buf);
470        assert!(GetEnrollmentSummaryAck::decode(&buf[..1]).is_err());
471    }
472
473    #[test]
474    fn test_decode_ack_truncated_half() {
475        let ack = GetEnrollmentSummaryAck {
476            entries: vec![EnrollmentSummaryEntry {
477                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
478                event_type: EventType::OUT_OF_RANGE,
479                event_state: EventState::HIGH_LIMIT,
480                priority: 3,
481                notification_class: 10,
482            }],
483        };
484        let mut buf = BytesMut::new();
485        ack.encode(&mut buf);
486        let half = buf.len() / 2;
487        assert!(GetEnrollmentSummaryAck::decode(&buf[..half]).is_err());
488    }
489
490    #[test]
491    fn test_decode_ack_invalid_tag() {
492        assert!(GetEnrollmentSummaryAck::decode(&[0xFF, 0xFF, 0xFF]).is_err());
493    }
494}