Skip to main content

bacnet_services/
wpm.rs

1//! WritePropertyMultiple service per ASHRAE 135-2020 Clause 15.10.
2
3use bacnet_encoding::primitives;
4use bacnet_encoding::tags;
5use bacnet_types::error::Error;
6use bacnet_types::primitives::ObjectIdentifier;
7use bytes::BytesMut;
8
9use crate::common::{BACnetPropertyValue, MAX_DECODED_ITEMS};
10
11// ---------------------------------------------------------------------------
12// WritePropertyMultipleRequest
13// ---------------------------------------------------------------------------
14
15/// A single object + list of property values to write.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct WriteAccessSpecification {
18    pub object_identifier: ObjectIdentifier,
19    pub list_of_properties: Vec<BACnetPropertyValue>,
20}
21
22/// WritePropertyMultiple-Request service parameters.
23///
24/// Uses SimpleACK (no ACK struct needed).
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct WritePropertyMultipleRequest {
27    pub list_of_write_access_specs: Vec<WriteAccessSpecification>,
28}
29
30impl WritePropertyMultipleRequest {
31    pub fn encode(&self, buf: &mut BytesMut) {
32        for spec in &self.list_of_write_access_specs {
33            primitives::encode_ctx_object_id(buf, 0, &spec.object_identifier);
34            tags::encode_opening_tag(buf, 1);
35            for prop_val in &spec.list_of_properties {
36                prop_val.encode(buf);
37            }
38            tags::encode_closing_tag(buf, 1);
39        }
40    }
41
42    pub fn decode(data: &[u8]) -> Result<Self, Error> {
43        let mut offset = 0;
44        let mut specs = Vec::new();
45
46        while offset < data.len() {
47            if specs.len() >= MAX_DECODED_ITEMS {
48                return Err(Error::decoding(
49                    offset,
50                    "WPM request exceeds max decoded items",
51                ));
52            }
53
54            // [0] object-identifier
55            let (tag, pos) = tags::decode_tag(data, offset)?;
56            let end = pos + tag.length as usize;
57            if end > data.len() {
58                return Err(Error::decoding(pos, "WPM request truncated at object-id"));
59            }
60            let object_identifier = ObjectIdentifier::decode(&data[pos..end])?;
61            offset = end;
62
63            // [1] list-of-properties (opening tag 1)
64            let (tag, tag_end) = tags::decode_tag(data, offset)?;
65            if !tag.is_opening_tag(1) {
66                return Err(Error::decoding(
67                    offset,
68                    "WPM request expected opening tag 1",
69                ));
70            }
71            offset = tag_end;
72
73            let mut props = Vec::new();
74            loop {
75                if offset >= data.len() {
76                    return Err(Error::decoding(offset, "WPM request missing closing tag 1"));
77                }
78                if props.len() >= MAX_DECODED_ITEMS {
79                    return Err(Error::decoding(offset, "WPM properties exceeds max"));
80                }
81                let (tag, tag_end) = tags::decode_tag(data, offset)?;
82                if tag.is_closing_tag(1) {
83                    offset = tag_end;
84                    break;
85                }
86                let (pv, new_offset) = BACnetPropertyValue::decode(data, offset)?;
87                props.push(pv);
88                offset = new_offset;
89            }
90
91            specs.push(WriteAccessSpecification {
92                object_identifier,
93                list_of_properties: props,
94            });
95        }
96
97        Ok(Self {
98            list_of_write_access_specs: specs,
99        })
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use bacnet_types::enums::{ObjectType, PropertyIdentifier};
107
108    #[test]
109    fn request_round_trip() {
110        let req = WritePropertyMultipleRequest {
111            list_of_write_access_specs: vec![WriteAccessSpecification {
112                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
113                list_of_properties: vec![BACnetPropertyValue {
114                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
115                    property_array_index: None,
116                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
117                    priority: Some(8),
118                }],
119            }],
120        };
121        let mut buf = BytesMut::new();
122        req.encode(&mut buf);
123        let decoded = WritePropertyMultipleRequest::decode(&buf).unwrap();
124        assert_eq!(req, decoded);
125    }
126
127    #[test]
128    fn multi_object_round_trip() {
129        let req = WritePropertyMultipleRequest {
130            list_of_write_access_specs: vec![
131                WriteAccessSpecification {
132                    object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
133                    list_of_properties: vec![BACnetPropertyValue {
134                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
135                        property_array_index: None,
136                        value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
137                        priority: None,
138                    }],
139                },
140                WriteAccessSpecification {
141                    object_identifier: ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, 2).unwrap(),
142                    list_of_properties: vec![BACnetPropertyValue {
143                        property_identifier: PropertyIdentifier::PRESENT_VALUE,
144                        property_array_index: None,
145                        value: vec![0x91, 0x01],
146                        priority: Some(8),
147                    }],
148                },
149            ],
150        };
151        let mut buf = BytesMut::new();
152        req.encode(&mut buf);
153        let decoded = WritePropertyMultipleRequest::decode(&buf).unwrap();
154        assert_eq!(req, decoded);
155    }
156
157    // -----------------------------------------------------------------------
158    // Malformed-input decode error tests
159    // -----------------------------------------------------------------------
160
161    #[test]
162    fn test_decode_wpm_request_truncated_1_byte() {
163        let req = WritePropertyMultipleRequest {
164            list_of_write_access_specs: vec![WriteAccessSpecification {
165                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
166                list_of_properties: vec![BACnetPropertyValue {
167                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
168                    property_array_index: None,
169                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
170                    priority: Some(8),
171                }],
172            }],
173        };
174        let mut buf = BytesMut::new();
175        req.encode(&mut buf);
176        assert!(WritePropertyMultipleRequest::decode(&buf[..1]).is_err());
177    }
178
179    #[test]
180    fn test_decode_wpm_request_truncated_3_bytes() {
181        let req = WritePropertyMultipleRequest {
182            list_of_write_access_specs: vec![WriteAccessSpecification {
183                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
184                list_of_properties: vec![BACnetPropertyValue {
185                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
186                    property_array_index: None,
187                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
188                    priority: Some(8),
189                }],
190            }],
191        };
192        let mut buf = BytesMut::new();
193        req.encode(&mut buf);
194        assert!(WritePropertyMultipleRequest::decode(&buf[..3]).is_err());
195    }
196
197    #[test]
198    fn test_decode_wpm_request_truncated_half() {
199        let req = WritePropertyMultipleRequest {
200            list_of_write_access_specs: vec![WriteAccessSpecification {
201                object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_OUTPUT, 1).unwrap(),
202                list_of_properties: vec![BACnetPropertyValue {
203                    property_identifier: PropertyIdentifier::PRESENT_VALUE,
204                    property_array_index: None,
205                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
206                    priority: Some(8),
207                }],
208            }],
209        };
210        let mut buf = BytesMut::new();
211        req.encode(&mut buf);
212        let half = buf.len() / 2;
213        assert!(WritePropertyMultipleRequest::decode(&buf[..half]).is_err());
214    }
215
216    #[test]
217    fn test_decode_wpm_request_invalid_tag() {
218        assert!(WritePropertyMultipleRequest::decode(&[0xFF, 0xFF, 0xFF]).is_err());
219    }
220}