Skip to main content

bacnet_services/
cov_multiple.rs

1//! SubscribeCOVPropertyMultiple and COVNotificationMultiple services
2//! per ASHRAE 135-2020 Clauses 13.14.3 / 13.15.
3
4use bacnet_encoding::primitives;
5use bacnet_encoding::tags;
6use bacnet_types::enums::PropertyIdentifier;
7use bacnet_types::error::Error;
8use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier};
9use bytes::BytesMut;
10
11use crate::common::{PropertyReference, MAX_DECODED_ITEMS};
12
13// ---------------------------------------------------------------------------
14// SubscribeCOVPropertyMultipleRequest (Clause 13.14.3)
15// ---------------------------------------------------------------------------
16
17/// A single COV reference within a subscription specification.
18#[derive(Debug, Clone, PartialEq)]
19pub struct COVReference {
20    pub monitored_property: PropertyReference,
21    pub cov_increment: Option<f32>,
22    pub timestamped: bool,
23}
24
25/// A single subscription specification (object + list of property references).
26#[derive(Debug, Clone, PartialEq)]
27pub struct COVSubscriptionSpecification {
28    pub monitored_object_identifier: ObjectIdentifier,
29    pub list_of_cov_references: Vec<COVReference>,
30}
31
32/// SubscribeCOVPropertyMultiple-Request service parameters.
33#[derive(Debug, Clone, PartialEq)]
34pub struct SubscribeCOVPropertyMultipleRequest {
35    pub subscriber_process_identifier: u32,
36    pub max_notification_delay: Option<u32>,
37    pub issue_confirmed_notifications: Option<bool>,
38    pub list_of_cov_subscription_specifications: Vec<COVSubscriptionSpecification>,
39}
40
41impl SubscribeCOVPropertyMultipleRequest {
42    pub fn encode(&self, buf: &mut BytesMut) {
43        // [0] subscriberProcessIdentifier
44        primitives::encode_ctx_unsigned(buf, 0, self.subscriber_process_identifier as u64);
45        // [1] maxNotificationDelay OPTIONAL
46        if let Some(v) = self.max_notification_delay {
47            primitives::encode_ctx_unsigned(buf, 1, v as u64);
48        }
49        // [2] issueConfirmedNotifications OPTIONAL
50        if let Some(v) = self.issue_confirmed_notifications {
51            primitives::encode_ctx_boolean(buf, 2, v);
52        }
53        // [3] listOfCovSubscriptionSpecifications
54        tags::encode_opening_tag(buf, 3);
55        for spec in &self.list_of_cov_subscription_specifications {
56            // [0] monitoredObjectIdentifier
57            primitives::encode_ctx_object_id(buf, 0, &spec.monitored_object_identifier);
58            // [1] listOfCovReferences
59            tags::encode_opening_tag(buf, 1);
60            for cov_ref in &spec.list_of_cov_references {
61                // [0] monitoredProperty (BACnetPropertyReference)
62                tags::encode_opening_tag(buf, 0);
63                cov_ref.monitored_property.encode(buf);
64                tags::encode_closing_tag(buf, 0);
65                // [1] covIncrement OPTIONAL
66                if let Some(inc) = cov_ref.cov_increment {
67                    primitives::encode_ctx_real(buf, 1, inc);
68                }
69                // [2] timestamped DEFAULT FALSE
70                if cov_ref.timestamped {
71                    primitives::encode_ctx_boolean(buf, 2, true);
72                }
73            }
74            tags::encode_closing_tag(buf, 1);
75        }
76        tags::encode_closing_tag(buf, 3);
77    }
78
79    pub fn decode(data: &[u8]) -> Result<Self, Error> {
80        let mut offset = 0;
81
82        // [0] subscriberProcessIdentifier
83        let (tag, pos) = tags::decode_tag(data, offset)?;
84        let end = pos + tag.length as usize;
85        if end > data.len() {
86            return Err(Error::decoding(
87                pos,
88                "SubscribeCOVPropertyMultiple truncated at process-id",
89            ));
90        }
91        let subscriber_process_identifier = primitives::decode_unsigned(&data[pos..end])? as u32;
92        offset = end;
93
94        // [1] maxNotificationDelay OPTIONAL
95        let mut max_notification_delay = None;
96        if offset < data.len() {
97            let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
98            if let Some(content) = opt {
99                max_notification_delay = Some(primitives::decode_unsigned(content)? as u32);
100                offset = new_off;
101            }
102        }
103
104        // [2] issueConfirmedNotifications OPTIONAL
105        let mut issue_confirmed_notifications = None;
106        if offset < data.len() {
107            let (opt, new_off) = tags::decode_optional_context(data, offset, 2)?;
108            if let Some(content) = opt {
109                issue_confirmed_notifications = Some(!content.is_empty() && content[0] != 0);
110                offset = new_off;
111            }
112        }
113
114        // [3] listOfCovSubscriptionSpecifications — opening tag 3
115        let (tag, tag_end) = tags::decode_tag(data, offset)?;
116        if !tag.is_opening_tag(3) {
117            return Err(Error::decoding(
118                offset,
119                "SubscribeCOVPropertyMultiple expected opening tag 3",
120            ));
121        }
122        offset = tag_end;
123
124        let mut specs = Vec::new();
125        loop {
126            if offset >= data.len() {
127                return Err(Error::decoding(
128                    offset,
129                    "SubscribeCOVPropertyMultiple missing closing tag 3",
130                ));
131            }
132            if specs.len() >= MAX_DECODED_ITEMS {
133                return Err(Error::decoding(offset, "too many subscription specs"));
134            }
135            let (tag, tag_end) = tags::decode_tag(data, offset)?;
136            if tag.is_closing_tag(3) {
137                offset = tag_end;
138                break;
139            }
140
141            // [0] monitoredObjectIdentifier
142            let end = tag_end + tag.length as usize;
143            if end > data.len() {
144                return Err(Error::decoding(
145                    tag_end,
146                    "SubscribeCOVPropertyMultiple truncated at object-id",
147                ));
148            }
149            let oid = ObjectIdentifier::decode(&data[tag_end..end])?;
150            offset = end;
151
152            // [1] listOfCovReferences — opening tag 1
153            let (tag, tag_end) = tags::decode_tag(data, offset)?;
154            if !tag.is_opening_tag(1) {
155                return Err(Error::decoding(
156                    offset,
157                    "SubscribeCOVPropertyMultiple expected opening tag 1",
158                ));
159            }
160            offset = tag_end;
161
162            let mut refs = Vec::new();
163            loop {
164                if offset >= data.len() {
165                    return Err(Error::decoding(
166                        offset,
167                        "SubscribeCOVPropertyMultiple missing closing tag 1",
168                    ));
169                }
170                if refs.len() >= MAX_DECODED_ITEMS {
171                    return Err(Error::decoding(offset, "too many COV references"));
172                }
173                let (tag, tag_end) = tags::decode_tag(data, offset)?;
174                if tag.is_closing_tag(1) {
175                    offset = tag_end;
176                    break;
177                }
178
179                // [0] monitoredProperty — opening tag 0
180                if !tag.is_opening_tag(0) {
181                    return Err(Error::decoding(
182                        offset,
183                        "SubscribeCOVPropertyMultiple expected opening tag 0 for property ref",
184                    ));
185                }
186                let (prop_ref, new_off) = PropertyReference::decode(data, tag_end)?;
187                offset = new_off;
188                // closing tag 0
189                let (_tag, tag_end) = tags::decode_tag(data, offset)?;
190                offset = tag_end;
191
192                // [1] covIncrement OPTIONAL
193                let mut cov_increment = None;
194                if offset < data.len() {
195                    let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
196                    if let Some(content) = opt {
197                        cov_increment = Some(primitives::decode_real(content)?);
198                        offset = new_off;
199                    }
200                }
201
202                // [2] timestamped DEFAULT FALSE
203                let mut timestamped = false;
204                if offset < data.len() {
205                    let (opt, new_off) = tags::decode_optional_context(data, offset, 2)?;
206                    if let Some(content) = opt {
207                        timestamped = !content.is_empty() && content[0] != 0;
208                        offset = new_off;
209                    }
210                }
211
212                refs.push(COVReference {
213                    monitored_property: prop_ref,
214                    cov_increment,
215                    timestamped,
216                });
217            }
218
219            specs.push(COVSubscriptionSpecification {
220                monitored_object_identifier: oid,
221                list_of_cov_references: refs,
222            });
223        }
224        let _ = offset;
225
226        Ok(Self {
227            subscriber_process_identifier,
228            max_notification_delay,
229            issue_confirmed_notifications,
230            list_of_cov_subscription_specifications: specs,
231        })
232    }
233}
234
235// ---------------------------------------------------------------------------
236// COVNotificationMultipleRequest (Confirmed service 31, Unconfirmed 11)
237// Clause 13.15
238// ---------------------------------------------------------------------------
239
240/// A single value entry in a COV notification list.
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct COVNotificationValue {
243    pub property_identifier: PropertyIdentifier,
244    pub property_array_index: Option<u32>,
245    /// Raw application-tagged bytes for the value.
246    pub value: Vec<u8>,
247    pub time_of_change: Option<Vec<u8>>,
248}
249
250/// A single object notification within a COVNotificationMultiple.
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct COVNotificationItem {
253    pub monitored_object_identifier: ObjectIdentifier,
254    pub list_of_values: Vec<COVNotificationValue>,
255}
256
257/// COVNotificationMultiple-Request service parameters.
258#[derive(Debug, Clone, PartialEq)]
259pub struct COVNotificationMultipleRequest {
260    pub subscriber_process_identifier: u32,
261    pub initiating_device_identifier: ObjectIdentifier,
262    pub time_remaining: u32,
263    pub timestamp: BACnetTimeStamp,
264    pub list_of_cov_notifications: Vec<COVNotificationItem>,
265}
266
267impl COVNotificationMultipleRequest {
268    pub fn encode(&self, buf: &mut BytesMut) {
269        // [0] subscriberProcessIdentifier
270        primitives::encode_ctx_unsigned(buf, 0, self.subscriber_process_identifier as u64);
271        // [1] initiatingDeviceIdentifier
272        primitives::encode_ctx_object_id(buf, 1, &self.initiating_device_identifier);
273        // [2] timeRemaining
274        primitives::encode_ctx_unsigned(buf, 2, self.time_remaining as u64);
275        // [3] timestamp
276        primitives::encode_timestamp(buf, 3, &self.timestamp);
277        // [4] listOfCovNotifications
278        tags::encode_opening_tag(buf, 4);
279        for item in &self.list_of_cov_notifications {
280            // [0] monitoredObjectIdentifier
281            primitives::encode_ctx_object_id(buf, 0, &item.monitored_object_identifier);
282            // [1] listOfValues
283            tags::encode_opening_tag(buf, 1);
284            for val in &item.list_of_values {
285                // [0] propertyIdentifier
286                primitives::encode_ctx_unsigned(buf, 0, val.property_identifier.to_raw() as u64);
287                // [1] propertyArrayIndex OPTIONAL
288                if let Some(idx) = val.property_array_index {
289                    primitives::encode_ctx_unsigned(buf, 1, idx as u64);
290                }
291                // [2] value (opening/closing)
292                tags::encode_opening_tag(buf, 2);
293                buf.extend_from_slice(&val.value);
294                tags::encode_closing_tag(buf, 2);
295                // [3] timeOfChange OPTIONAL (opening/closing)
296                if let Some(ref ts) = val.time_of_change {
297                    tags::encode_opening_tag(buf, 3);
298                    buf.extend_from_slice(ts);
299                    tags::encode_closing_tag(buf, 3);
300                }
301            }
302            tags::encode_closing_tag(buf, 1);
303        }
304        tags::encode_closing_tag(buf, 4);
305    }
306
307    pub fn decode(data: &[u8]) -> Result<Self, Error> {
308        let mut offset = 0;
309
310        // [0] subscriberProcessIdentifier
311        let (tag, pos) = tags::decode_tag(data, offset)?;
312        let end = pos + tag.length as usize;
313        if end > data.len() {
314            return Err(Error::decoding(
315                pos,
316                "COVNotificationMultiple truncated at process-id",
317            ));
318        }
319        let subscriber_process_identifier = primitives::decode_unsigned(&data[pos..end])? as u32;
320        offset = end;
321
322        // [1] initiatingDeviceIdentifier
323        let (tag, pos) = tags::decode_tag(data, offset)?;
324        let end = pos + tag.length as usize;
325        if end > data.len() {
326            return Err(Error::decoding(
327                pos,
328                "COVNotificationMultiple truncated at device-id",
329            ));
330        }
331        let initiating_device_identifier = ObjectIdentifier::decode(&data[pos..end])?;
332        offset = end;
333
334        // [2] timeRemaining
335        let (tag, pos) = tags::decode_tag(data, offset)?;
336        let end = pos + tag.length as usize;
337        if end > data.len() {
338            return Err(Error::decoding(
339                pos,
340                "COVNotificationMultiple truncated at time-remaining",
341            ));
342        }
343        let time_remaining = primitives::decode_unsigned(&data[pos..end])? as u32;
344        offset = end;
345
346        // [3] timestamp
347        let (timestamp, new_off) = primitives::decode_timestamp(data, offset, 3)?;
348        offset = new_off;
349
350        // [4] listOfCovNotifications — opening tag 4
351        let (tag, tag_end) = tags::decode_tag(data, offset)?;
352        if !tag.is_opening_tag(4) {
353            return Err(Error::decoding(
354                offset,
355                "COVNotificationMultiple expected opening tag 4",
356            ));
357        }
358        offset = tag_end;
359
360        let mut items = Vec::new();
361        loop {
362            if offset >= data.len() {
363                return Err(Error::decoding(
364                    offset,
365                    "COVNotificationMultiple missing closing tag 4",
366                ));
367            }
368            if items.len() >= MAX_DECODED_ITEMS {
369                return Err(Error::decoding(offset, "too many notification items"));
370            }
371            let (tag, tag_end) = tags::decode_tag(data, offset)?;
372            if tag.is_closing_tag(4) {
373                offset = tag_end;
374                break;
375            }
376
377            // [0] monitoredObjectIdentifier
378            let end = tag_end + tag.length as usize;
379            if end > data.len() {
380                return Err(Error::decoding(
381                    tag_end,
382                    "COVNotificationMultiple truncated at monitored-id",
383                ));
384            }
385            let oid = ObjectIdentifier::decode(&data[tag_end..end])?;
386            offset = end;
387
388            // [1] listOfValues — opening tag 1
389            let (tag, tag_end) = tags::decode_tag(data, offset)?;
390            if !tag.is_opening_tag(1) {
391                return Err(Error::decoding(
392                    offset,
393                    "COVNotificationMultiple expected opening tag 1",
394                ));
395            }
396            offset = tag_end;
397
398            let mut values = Vec::new();
399            loop {
400                if offset >= data.len() {
401                    return Err(Error::decoding(
402                        offset,
403                        "COVNotificationMultiple missing closing tag 1",
404                    ));
405                }
406                if values.len() >= MAX_DECODED_ITEMS {
407                    return Err(Error::decoding(offset, "too many notification values"));
408                }
409                let (tag, tag_end) = tags::decode_tag(data, offset)?;
410                if tag.is_closing_tag(1) {
411                    offset = tag_end;
412                    break;
413                }
414
415                // [0] propertyIdentifier
416                let end = tag_end + tag.length as usize;
417                if end > data.len() {
418                    return Err(Error::decoding(
419                        tag_end,
420                        "COVNotificationMultiple truncated at property-id",
421                    ));
422                }
423                let prop_id = primitives::decode_unsigned(&data[tag_end..end])? as u32;
424                offset = end;
425
426                // [1] propertyArrayIndex OPTIONAL
427                let mut array_index = None;
428                if offset < data.len() {
429                    let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
430                    if let Some(content) = opt {
431                        array_index = Some(primitives::decode_unsigned(content)? as u32);
432                        offset = new_off;
433                    }
434                }
435
436                // [2] value (opening/closing)
437                let (tag, tag_end) = tags::decode_tag(data, offset)?;
438                if !tag.is_opening_tag(2) {
439                    return Err(Error::decoding(
440                        offset,
441                        "COVNotificationMultiple expected opening tag 2",
442                    ));
443                }
444                let (value_bytes, new_off) = tags::extract_context_value(data, tag_end, 2)?;
445                let value = value_bytes.to_vec();
446                offset = new_off;
447
448                // [3] timeOfChange OPTIONAL (opening/closing)
449                let mut time_of_change = None;
450                if offset < data.len() {
451                    let (peek, _) = tags::decode_tag(data, offset)?;
452                    if peek.is_opening_tag(3) {
453                        let (_, inner_start) = tags::decode_tag(data, offset)?;
454                        let (ts_bytes, new_off) =
455                            tags::extract_context_value(data, inner_start, 3)?;
456                        time_of_change = Some(ts_bytes.to_vec());
457                        offset = new_off;
458                    }
459                }
460
461                values.push(COVNotificationValue {
462                    property_identifier: PropertyIdentifier::from_raw(prop_id),
463                    property_array_index: array_index,
464                    value,
465                    time_of_change,
466                });
467            }
468
469            items.push(COVNotificationItem {
470                monitored_object_identifier: oid,
471                list_of_values: values,
472            });
473        }
474        let _ = offset;
475
476        Ok(Self {
477            subscriber_process_identifier,
478            initiating_device_identifier,
479            time_remaining,
480            timestamp,
481            list_of_cov_notifications: items,
482        })
483    }
484}
485
486// ---------------------------------------------------------------------------
487// Tests
488// ---------------------------------------------------------------------------
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use bacnet_types::enums::ObjectType;
494    use bacnet_types::primitives::Time;
495
496    #[test]
497    fn subscribe_cov_property_multiple_round_trip() {
498        let req = SubscribeCOVPropertyMultipleRequest {
499            subscriber_process_identifier: 42,
500            max_notification_delay: Some(10),
501            issue_confirmed_notifications: Some(true),
502            list_of_cov_subscription_specifications: vec![COVSubscriptionSpecification {
503                monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
504                    .unwrap(),
505                list_of_cov_references: vec![
506                    COVReference {
507                        monitored_property: PropertyReference {
508                            property_identifier: PropertyIdentifier::PRESENT_VALUE,
509                            property_array_index: None,
510                        },
511                        cov_increment: Some(1.0),
512                        timestamped: true,
513                    },
514                    COVReference {
515                        monitored_property: PropertyReference {
516                            property_identifier: PropertyIdentifier::STATUS_FLAGS,
517                            property_array_index: None,
518                        },
519                        cov_increment: None,
520                        timestamped: false,
521                    },
522                ],
523            }],
524        };
525        let mut buf = BytesMut::new();
526        req.encode(&mut buf);
527        let decoded = SubscribeCOVPropertyMultipleRequest::decode(&buf).unwrap();
528        assert_eq!(req, decoded);
529    }
530
531    #[test]
532    fn subscribe_cov_property_multiple_minimal() {
533        let req = SubscribeCOVPropertyMultipleRequest {
534            subscriber_process_identifier: 1,
535            max_notification_delay: None,
536            issue_confirmed_notifications: None,
537            list_of_cov_subscription_specifications: vec![COVSubscriptionSpecification {
538                monitored_object_identifier: ObjectIdentifier::new(ObjectType::BINARY_INPUT, 5)
539                    .unwrap(),
540                list_of_cov_references: vec![COVReference {
541                    monitored_property: PropertyReference {
542                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
543                        property_array_index: None,
544                    },
545                    cov_increment: None,
546                    timestamped: false,
547                }],
548            }],
549        };
550        let mut buf = BytesMut::new();
551        req.encode(&mut buf);
552        let decoded = SubscribeCOVPropertyMultipleRequest::decode(&buf).unwrap();
553        assert_eq!(req, decoded);
554    }
555
556    #[test]
557    fn cov_notification_multiple_round_trip() {
558        let req = COVNotificationMultipleRequest {
559            subscriber_process_identifier: 1,
560            initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
561            time_remaining: 60,
562            timestamp: BACnetTimeStamp::SequenceNumber(42),
563            list_of_cov_notifications: vec![COVNotificationItem {
564                monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
565                    .unwrap(),
566                list_of_values: vec![
567                    COVNotificationValue {
568                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
569                        property_array_index: None,
570                        value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
571                        time_of_change: None,
572                    },
573                    COVNotificationValue {
574                        property_identifier: PropertyIdentifier::STATUS_FLAGS,
575                        property_array_index: None,
576                        value: vec![0x82, 0x04, 0x00],
577                        time_of_change: Some(vec![0x19, 0x2A]), // raw timestamp bytes
578                    },
579                ],
580            }],
581        };
582        let mut buf = BytesMut::new();
583        req.encode(&mut buf);
584        let decoded = COVNotificationMultipleRequest::decode(&buf).unwrap();
585        assert_eq!(req, decoded);
586    }
587
588    #[test]
589    fn cov_notification_multiple_with_time_timestamp() {
590        let req = COVNotificationMultipleRequest {
591            subscriber_process_identifier: 5,
592            initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap(),
593            time_remaining: 0,
594            timestamp: BACnetTimeStamp::Time(Time {
595                hour: 12,
596                minute: 30,
597                second: 0,
598                hundredths: 0,
599            }),
600            list_of_cov_notifications: vec![COVNotificationItem {
601                monitored_object_identifier: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3)
602                    .unwrap(),
603                list_of_values: vec![COVNotificationValue {
604                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
605                    property_array_index: None,
606                    value: vec![0x91, 0x01],
607                    time_of_change: None,
608                }],
609            }],
610        };
611        let mut buf = BytesMut::new();
612        req.encode(&mut buf);
613        let decoded = COVNotificationMultipleRequest::decode(&buf).unwrap();
614        assert_eq!(req, decoded);
615    }
616
617    #[test]
618    fn subscribe_cov_property_multiple_empty_input() {
619        assert!(SubscribeCOVPropertyMultipleRequest::decode(&[]).is_err());
620    }
621
622    #[test]
623    fn cov_notification_multiple_empty_input() {
624        assert!(COVNotificationMultipleRequest::decode(&[]).is_err());
625    }
626}