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
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                let (_tag, tag_end) = tags::decode_tag(data, offset)?;
189                offset = tag_end;
190
191                // [1] covIncrement OPTIONAL
192                let mut cov_increment = None;
193                if offset < data.len() {
194                    let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
195                    if let Some(content) = opt {
196                        cov_increment = Some(primitives::decode_real(content)?);
197                        offset = new_off;
198                    }
199                }
200
201                // [2] timestamped DEFAULT FALSE
202                let mut timestamped = false;
203                if offset < data.len() {
204                    let (opt, new_off) = tags::decode_optional_context(data, offset, 2)?;
205                    if let Some(content) = opt {
206                        timestamped = !content.is_empty() && content[0] != 0;
207                        offset = new_off;
208                    }
209                }
210
211                refs.push(COVReference {
212                    monitored_property: prop_ref,
213                    cov_increment,
214                    timestamped,
215                });
216            }
217
218            specs.push(COVSubscriptionSpecification {
219                monitored_object_identifier: oid,
220                list_of_cov_references: refs,
221            });
222        }
223        let _ = offset;
224
225        Ok(Self {
226            subscriber_process_identifier,
227            max_notification_delay,
228            issue_confirmed_notifications,
229            list_of_cov_subscription_specifications: specs,
230        })
231    }
232}
233
234// ---------------------------------------------------------------------------
235// COVNotificationMultipleRequest
236// ---------------------------------------------------------------------------
237
238/// A single value entry in a COV notification list.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct COVNotificationValue {
241    pub property_identifier: PropertyIdentifier,
242    pub property_array_index: Option<u32>,
243    /// Raw application-tagged bytes for the value.
244    pub value: Vec<u8>,
245    pub time_of_change: Option<Vec<u8>>,
246}
247
248/// A single object notification within a COVNotificationMultiple.
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct COVNotificationItem {
251    pub monitored_object_identifier: ObjectIdentifier,
252    pub list_of_values: Vec<COVNotificationValue>,
253}
254
255/// COVNotificationMultiple-Request service parameters.
256#[derive(Debug, Clone, PartialEq)]
257pub struct COVNotificationMultipleRequest {
258    pub subscriber_process_identifier: u32,
259    pub initiating_device_identifier: ObjectIdentifier,
260    pub time_remaining: u32,
261    pub timestamp: BACnetTimeStamp,
262    pub list_of_cov_notifications: Vec<COVNotificationItem>,
263}
264
265impl COVNotificationMultipleRequest {
266    pub fn encode(&self, buf: &mut BytesMut) {
267        // [0] subscriberProcessIdentifier
268        primitives::encode_ctx_unsigned(buf, 0, self.subscriber_process_identifier as u64);
269        // [1] initiatingDeviceIdentifier
270        primitives::encode_ctx_object_id(buf, 1, &self.initiating_device_identifier);
271        // [2] timeRemaining
272        primitives::encode_ctx_unsigned(buf, 2, self.time_remaining as u64);
273        // [3] timestamp
274        primitives::encode_timestamp(buf, 3, &self.timestamp);
275        // [4] listOfCovNotifications
276        tags::encode_opening_tag(buf, 4);
277        for item in &self.list_of_cov_notifications {
278            // [0] monitoredObjectIdentifier
279            primitives::encode_ctx_object_id(buf, 0, &item.monitored_object_identifier);
280            // [1] listOfValues
281            tags::encode_opening_tag(buf, 1);
282            for val in &item.list_of_values {
283                // [0] propertyIdentifier
284                primitives::encode_ctx_unsigned(buf, 0, val.property_identifier.to_raw() as u64);
285                // [1] propertyArrayIndex OPTIONAL
286                if let Some(idx) = val.property_array_index {
287                    primitives::encode_ctx_unsigned(buf, 1, idx as u64);
288                }
289                // [2] value (opening/closing)
290                tags::encode_opening_tag(buf, 2);
291                buf.extend_from_slice(&val.value);
292                tags::encode_closing_tag(buf, 2);
293                // [3] timeOfChange OPTIONAL (opening/closing)
294                if let Some(ref ts) = val.time_of_change {
295                    tags::encode_opening_tag(buf, 3);
296                    buf.extend_from_slice(ts);
297                    tags::encode_closing_tag(buf, 3);
298                }
299            }
300            tags::encode_closing_tag(buf, 1);
301        }
302        tags::encode_closing_tag(buf, 4);
303    }
304
305    pub fn decode(data: &[u8]) -> Result<Self, Error> {
306        let mut offset = 0;
307
308        // [0] subscriberProcessIdentifier
309        let (tag, pos) = tags::decode_tag(data, offset)?;
310        let end = pos + tag.length as usize;
311        if end > data.len() {
312            return Err(Error::decoding(
313                pos,
314                "COVNotificationMultiple truncated at process-id",
315            ));
316        }
317        let subscriber_process_identifier = primitives::decode_unsigned(&data[pos..end])? as u32;
318        offset = end;
319
320        // [1] initiatingDeviceIdentifier
321        let (tag, pos) = tags::decode_tag(data, offset)?;
322        let end = pos + tag.length as usize;
323        if end > data.len() {
324            return Err(Error::decoding(
325                pos,
326                "COVNotificationMultiple truncated at device-id",
327            ));
328        }
329        let initiating_device_identifier = ObjectIdentifier::decode(&data[pos..end])?;
330        offset = end;
331
332        // [2] timeRemaining
333        let (tag, pos) = tags::decode_tag(data, offset)?;
334        let end = pos + tag.length as usize;
335        if end > data.len() {
336            return Err(Error::decoding(
337                pos,
338                "COVNotificationMultiple truncated at time-remaining",
339            ));
340        }
341        let time_remaining = primitives::decode_unsigned(&data[pos..end])? as u32;
342        offset = end;
343
344        // [3] timestamp
345        let (timestamp, new_off) = primitives::decode_timestamp(data, offset, 3)?;
346        offset = new_off;
347
348        // [4] listOfCovNotifications — opening tag 4
349        let (tag, tag_end) = tags::decode_tag(data, offset)?;
350        if !tag.is_opening_tag(4) {
351            return Err(Error::decoding(
352                offset,
353                "COVNotificationMultiple expected opening tag 4",
354            ));
355        }
356        offset = tag_end;
357
358        let mut items = Vec::new();
359        loop {
360            if offset >= data.len() {
361                return Err(Error::decoding(
362                    offset,
363                    "COVNotificationMultiple missing closing tag 4",
364                ));
365            }
366            if items.len() >= MAX_DECODED_ITEMS {
367                return Err(Error::decoding(offset, "too many notification items"));
368            }
369            let (tag, tag_end) = tags::decode_tag(data, offset)?;
370            if tag.is_closing_tag(4) {
371                offset = tag_end;
372                break;
373            }
374
375            // [0] monitoredObjectIdentifier
376            let end = tag_end + tag.length as usize;
377            if end > data.len() {
378                return Err(Error::decoding(
379                    tag_end,
380                    "COVNotificationMultiple truncated at monitored-id",
381                ));
382            }
383            let oid = ObjectIdentifier::decode(&data[tag_end..end])?;
384            offset = end;
385
386            // [1] listOfValues — opening tag 1
387            let (tag, tag_end) = tags::decode_tag(data, offset)?;
388            if !tag.is_opening_tag(1) {
389                return Err(Error::decoding(
390                    offset,
391                    "COVNotificationMultiple expected opening tag 1",
392                ));
393            }
394            offset = tag_end;
395
396            let mut values = Vec::new();
397            loop {
398                if offset >= data.len() {
399                    return Err(Error::decoding(
400                        offset,
401                        "COVNotificationMultiple missing closing tag 1",
402                    ));
403                }
404                if values.len() >= MAX_DECODED_ITEMS {
405                    return Err(Error::decoding(offset, "too many notification values"));
406                }
407                let (tag, tag_end) = tags::decode_tag(data, offset)?;
408                if tag.is_closing_tag(1) {
409                    offset = tag_end;
410                    break;
411                }
412
413                // [0] propertyIdentifier
414                let end = tag_end + tag.length as usize;
415                if end > data.len() {
416                    return Err(Error::decoding(
417                        tag_end,
418                        "COVNotificationMultiple truncated at property-id",
419                    ));
420                }
421                let prop_id = primitives::decode_unsigned(&data[tag_end..end])? as u32;
422                offset = end;
423
424                // [1] propertyArrayIndex OPTIONAL
425                let mut array_index = None;
426                if offset < data.len() {
427                    let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
428                    if let Some(content) = opt {
429                        array_index = Some(primitives::decode_unsigned(content)? as u32);
430                        offset = new_off;
431                    }
432                }
433
434                // [2] value (opening/closing)
435                let (tag, tag_end) = tags::decode_tag(data, offset)?;
436                if !tag.is_opening_tag(2) {
437                    return Err(Error::decoding(
438                        offset,
439                        "COVNotificationMultiple expected opening tag 2",
440                    ));
441                }
442                let (value_bytes, new_off) = tags::extract_context_value(data, tag_end, 2)?;
443                let value = value_bytes.to_vec();
444                offset = new_off;
445
446                // [3] timeOfChange OPTIONAL (opening/closing)
447                let mut time_of_change = None;
448                if offset < data.len() {
449                    let (peek, _) = tags::decode_tag(data, offset)?;
450                    if peek.is_opening_tag(3) {
451                        let (_, inner_start) = tags::decode_tag(data, offset)?;
452                        let (ts_bytes, new_off) =
453                            tags::extract_context_value(data, inner_start, 3)?;
454                        time_of_change = Some(ts_bytes.to_vec());
455                        offset = new_off;
456                    }
457                }
458
459                values.push(COVNotificationValue {
460                    property_identifier: PropertyIdentifier::from_raw(prop_id),
461                    property_array_index: array_index,
462                    value,
463                    time_of_change,
464                });
465            }
466
467            items.push(COVNotificationItem {
468                monitored_object_identifier: oid,
469                list_of_values: values,
470            });
471        }
472        let _ = offset;
473
474        Ok(Self {
475            subscriber_process_identifier,
476            initiating_device_identifier,
477            time_remaining,
478            timestamp,
479            list_of_cov_notifications: items,
480        })
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use bacnet_types::enums::ObjectType;
488    use bacnet_types::primitives::Time;
489
490    #[test]
491    fn subscribe_cov_property_multiple_round_trip() {
492        let req = SubscribeCOVPropertyMultipleRequest {
493            subscriber_process_identifier: 42,
494            max_notification_delay: Some(10),
495            issue_confirmed_notifications: Some(true),
496            list_of_cov_subscription_specifications: vec![COVSubscriptionSpecification {
497                monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
498                    .unwrap(),
499                list_of_cov_references: vec![
500                    COVReference {
501                        monitored_property: PropertyReference {
502                            property_identifier: PropertyIdentifier::PRESENT_VALUE,
503                            property_array_index: None,
504                        },
505                        cov_increment: Some(1.0),
506                        timestamped: true,
507                    },
508                    COVReference {
509                        monitored_property: PropertyReference {
510                            property_identifier: PropertyIdentifier::STATUS_FLAGS,
511                            property_array_index: None,
512                        },
513                        cov_increment: None,
514                        timestamped: false,
515                    },
516                ],
517            }],
518        };
519        let mut buf = BytesMut::new();
520        req.encode(&mut buf);
521        let decoded = SubscribeCOVPropertyMultipleRequest::decode(&buf).unwrap();
522        assert_eq!(req, decoded);
523    }
524
525    #[test]
526    fn subscribe_cov_property_multiple_minimal() {
527        let req = SubscribeCOVPropertyMultipleRequest {
528            subscriber_process_identifier: 1,
529            max_notification_delay: None,
530            issue_confirmed_notifications: None,
531            list_of_cov_subscription_specifications: vec![COVSubscriptionSpecification {
532                monitored_object_identifier: ObjectIdentifier::new(ObjectType::BINARY_INPUT, 5)
533                    .unwrap(),
534                list_of_cov_references: vec![COVReference {
535                    monitored_property: PropertyReference {
536                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
537                        property_array_index: None,
538                    },
539                    cov_increment: None,
540                    timestamped: false,
541                }],
542            }],
543        };
544        let mut buf = BytesMut::new();
545        req.encode(&mut buf);
546        let decoded = SubscribeCOVPropertyMultipleRequest::decode(&buf).unwrap();
547        assert_eq!(req, decoded);
548    }
549
550    #[test]
551    fn cov_notification_multiple_round_trip() {
552        let req = COVNotificationMultipleRequest {
553            subscriber_process_identifier: 1,
554            initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 100).unwrap(),
555            time_remaining: 60,
556            timestamp: BACnetTimeStamp::SequenceNumber(42),
557            list_of_cov_notifications: vec![COVNotificationItem {
558                monitored_object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1)
559                    .unwrap(),
560                list_of_values: vec![
561                    COVNotificationValue {
562                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
563                        property_array_index: None,
564                        value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
565                        time_of_change: None,
566                    },
567                    COVNotificationValue {
568                        property_identifier: PropertyIdentifier::STATUS_FLAGS,
569                        property_array_index: None,
570                        value: vec![0x82, 0x04, 0x00],
571                        time_of_change: Some(vec![0x19, 0x2A]), // raw timestamp bytes
572                    },
573                ],
574            }],
575        };
576        let mut buf = BytesMut::new();
577        req.encode(&mut buf);
578        let decoded = COVNotificationMultipleRequest::decode(&buf).unwrap();
579        assert_eq!(req, decoded);
580    }
581
582    #[test]
583    fn cov_notification_multiple_with_time_timestamp() {
584        let req = COVNotificationMultipleRequest {
585            subscriber_process_identifier: 5,
586            initiating_device_identifier: ObjectIdentifier::new(ObjectType::DEVICE, 200).unwrap(),
587            time_remaining: 0,
588            timestamp: BACnetTimeStamp::Time(Time {
589                hour: 12,
590                minute: 30,
591                second: 0,
592                hundredths: 0,
593            }),
594            list_of_cov_notifications: vec![COVNotificationItem {
595                monitored_object_identifier: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 3)
596                    .unwrap(),
597                list_of_values: vec![COVNotificationValue {
598                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
599                    property_array_index: None,
600                    value: vec![0x91, 0x01],
601                    time_of_change: None,
602                }],
603            }],
604        };
605        let mut buf = BytesMut::new();
606        req.encode(&mut buf);
607        let decoded = COVNotificationMultipleRequest::decode(&buf).unwrap();
608        assert_eq!(req, decoded);
609    }
610
611    #[test]
612    fn subscribe_cov_property_multiple_empty_input() {
613        assert!(SubscribeCOVPropertyMultipleRequest::decode(&[]).is_err());
614    }
615
616    #[test]
617    fn cov_notification_multiple_empty_input() {
618        assert!(COVNotificationMultipleRequest::decode(&[]).is_err());
619    }
620}