Skip to main content

bacnet_services/
write_group.rs

1//! WriteGroup service per ASHRAE 135-2020 Clause 16.10.8.
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::MAX_DECODED_ITEMS;
10
11// ---------------------------------------------------------------------------
12// WriteGroupRequest
13// ---------------------------------------------------------------------------
14
15/// A single entry in the WriteGroup change list.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct GroupChannelValue {
18    /// [0] channel OPTIONAL
19    pub channel: Option<ObjectIdentifier>,
20    /// [1] overridePriority OPTIONAL
21    pub override_priority: Option<u8>,
22    /// [2] value — raw application-tagged bytes
23    pub value: Vec<u8>,
24}
25
26/// WriteGroup-Request service parameters.
27///
28/// ```text
29/// WriteGroupRequest ::= SEQUENCE {
30///     groupNumber    [0] Unsigned32,
31///     writePriority  [1] Unsigned (1-16),
32///     changeList     [2] SEQUENCE OF { ... },
33///     inhibitDelay   [3] BOOLEAN OPTIONAL
34/// }
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct WriteGroupRequest {
38    pub group_number: u32,
39    pub write_priority: u8,
40    pub change_list: Vec<GroupChannelValue>,
41    pub inhibit_delay: Option<bool>,
42}
43
44impl WriteGroupRequest {
45    pub fn encode(&self, buf: &mut BytesMut) {
46        // [0] groupNumber
47        primitives::encode_ctx_unsigned(buf, 0, self.group_number as u64);
48        // [1] writePriority
49        primitives::encode_ctx_unsigned(buf, 1, self.write_priority as u64);
50        // [2] changeList
51        tags::encode_opening_tag(buf, 2);
52        for entry in &self.change_list {
53            // [0] channel OPTIONAL
54            if let Some(ref ch) = entry.channel {
55                primitives::encode_ctx_object_id(buf, 0, ch);
56            }
57            // [1] overridePriority OPTIONAL
58            if let Some(prio) = entry.override_priority {
59                primitives::encode_ctx_unsigned(buf, 1, prio as u64);
60            }
61            // [2] value (opening/closing)
62            tags::encode_opening_tag(buf, 2);
63            buf.extend_from_slice(&entry.value);
64            tags::encode_closing_tag(buf, 2);
65        }
66        tags::encode_closing_tag(buf, 2);
67        // [3] inhibitDelay OPTIONAL
68        if let Some(v) = self.inhibit_delay {
69            primitives::encode_ctx_boolean(buf, 3, v);
70        }
71    }
72
73    pub fn decode(data: &[u8]) -> Result<Self, Error> {
74        let mut offset = 0;
75
76        // [0] groupNumber
77        let (tag, pos) = tags::decode_tag(data, offset)?;
78        let end = pos + tag.length as usize;
79        if end > data.len() {
80            return Err(Error::decoding(pos, "WriteGroup truncated at group-number"));
81        }
82        let group_number = primitives::decode_unsigned(&data[pos..end])? as u32;
83        if group_number == 0 {
84            return Err(Error::Encoding(
85                "WriteGroup group number 0 is reserved".into(),
86            ));
87        }
88        offset = end;
89
90        // [1] writePriority
91        let (tag, pos) = tags::decode_tag(data, offset)?;
92        let end = pos + tag.length as usize;
93        if end > data.len() {
94            return Err(Error::decoding(
95                pos,
96                "WriteGroup truncated at write-priority",
97            ));
98        }
99        let write_priority = primitives::decode_unsigned(&data[pos..end])? as u8;
100        if !(1..=16).contains(&write_priority) {
101            return Err(Error::decoding(
102                pos,
103                format!("WriteGroup write-priority {write_priority} out of range 1-16"),
104            ));
105        }
106        offset = end;
107
108        // [2] changeList — opening tag 2
109        let (tag, tag_end) = tags::decode_tag(data, offset)?;
110        if !tag.is_opening_tag(2) {
111            return Err(Error::decoding(offset, "WriteGroup expected opening tag 2"));
112        }
113        offset = tag_end;
114
115        let mut change_list = Vec::new();
116        loop {
117            if offset >= data.len() {
118                return Err(Error::decoding(offset, "WriteGroup missing closing tag 2"));
119            }
120            if change_list.len() >= MAX_DECODED_ITEMS {
121                return Err(Error::decoding(offset, "WriteGroup change list too large"));
122            }
123            let (tag, tag_end) = tags::decode_tag(data, offset)?;
124            if tag.is_closing_tag(2) {
125                offset = tag_end;
126                break;
127            }
128
129            // [0] channel OPTIONAL — peek for context 0
130            let mut channel = None;
131            if tag.is_context(0) {
132                let end = tag_end + tag.length as usize;
133                if end > data.len() {
134                    return Err(Error::decoding(tag_end, "WriteGroup truncated at channel"));
135                }
136                channel = Some(ObjectIdentifier::decode(&data[tag_end..end])?);
137                offset = end;
138            } else {
139                offset = tag_end - (tag_end - offset); // stay at current position
140            }
141
142            // [1] overridePriority OPTIONAL
143            let mut override_priority = None;
144            if offset < data.len() {
145                let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
146                if let Some(content) = opt {
147                    override_priority = Some(primitives::decode_unsigned(content)? as u8);
148                    offset = new_off;
149                }
150            }
151
152            // [2] value (opening/closing tag 2 — inner)
153            let (tag, tag_end) = tags::decode_tag(data, offset)?;
154            if !tag.is_opening_tag(2) {
155                return Err(Error::decoding(
156                    offset,
157                    "WriteGroup expected opening tag 2 for value",
158                ));
159            }
160            let (value_bytes, new_off) = tags::extract_context_value(data, tag_end, 2)?;
161            let value = value_bytes.to_vec();
162            offset = new_off;
163
164            change_list.push(GroupChannelValue {
165                channel,
166                override_priority,
167                value,
168            });
169        }
170
171        // [3] inhibitDelay OPTIONAL
172        let mut inhibit_delay = None;
173        if offset < data.len() {
174            let (opt, new_off) = tags::decode_optional_context(data, offset, 3)?;
175            if let Some(content) = opt {
176                inhibit_delay = Some(!content.is_empty() && content[0] != 0);
177                offset = new_off;
178            }
179        }
180        let _ = offset;
181
182        Ok(Self {
183            group_number,
184            write_priority,
185            change_list,
186            inhibit_delay,
187        })
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use bacnet_types::enums::ObjectType;
195
196    #[test]
197    fn write_group_round_trip() {
198        let req = WriteGroupRequest {
199            group_number: 1,
200            write_priority: 8,
201            change_list: vec![
202                GroupChannelValue {
203                    channel: Some(ObjectIdentifier::new(ObjectType::CHANNEL, 1).unwrap()),
204                    override_priority: Some(10),
205                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
206                },
207                GroupChannelValue {
208                    channel: None,
209                    override_priority: None,
210                    value: vec![0x91, 0x01],
211                },
212            ],
213            inhibit_delay: Some(true),
214        };
215        let mut buf = BytesMut::new();
216        req.encode(&mut buf);
217        let decoded = WriteGroupRequest::decode(&buf).unwrap();
218        assert_eq!(req, decoded);
219    }
220
221    #[test]
222    fn write_group_minimal() {
223        let req = WriteGroupRequest {
224            group_number: 100,
225            write_priority: 16,
226            change_list: vec![GroupChannelValue {
227                channel: None,
228                override_priority: None,
229                value: vec![0x10],
230            }],
231            inhibit_delay: None,
232        };
233        let mut buf = BytesMut::new();
234        req.encode(&mut buf);
235        let decoded = WriteGroupRequest::decode(&buf).unwrap();
236        assert_eq!(req, decoded);
237    }
238
239    #[test]
240    fn write_group_priority_validation() {
241        // Encode with valid priority, then corrupt it
242        let req = WriteGroupRequest {
243            group_number: 1,
244            write_priority: 8,
245            change_list: vec![GroupChannelValue {
246                channel: None,
247                override_priority: None,
248                value: vec![0x10],
249            }],
250            inhibit_delay: None,
251        };
252        let mut buf = BytesMut::new();
253        req.encode(&mut buf);
254        let mut data = buf.to_vec();
255        // The write_priority byte is after the group number encoding.
256        // group_number=1: ctx tag 0 (09 01), then write_priority=8: ctx tag 1 (19 08)
257        // Find and change the priority value to 0
258        for i in 0..data.len() - 1 {
259            if data[i] == 0x19 && data[i + 1] == 0x08 {
260                data[i + 1] = 0x00; // set to 0 (invalid)
261                break;
262            }
263        }
264        assert!(WriteGroupRequest::decode(&data).is_err());
265    }
266
267    #[test]
268    fn write_group_empty_input() {
269        assert!(WriteGroupRequest::decode(&[]).is_err());
270    }
271}