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 (Clause 16.10.8)
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        offset = end;
84
85        // [1] writePriority
86        let (tag, pos) = tags::decode_tag(data, offset)?;
87        let end = pos + tag.length as usize;
88        if end > data.len() {
89            return Err(Error::decoding(
90                pos,
91                "WriteGroup truncated at write-priority",
92            ));
93        }
94        let write_priority = primitives::decode_unsigned(&data[pos..end])? as u8;
95        if !(1..=16).contains(&write_priority) {
96            return Err(Error::decoding(
97                pos,
98                format!("WriteGroup write-priority {write_priority} out of range 1-16"),
99            ));
100        }
101        offset = end;
102
103        // [2] changeList — opening tag 2
104        let (tag, tag_end) = tags::decode_tag(data, offset)?;
105        if !tag.is_opening_tag(2) {
106            return Err(Error::decoding(offset, "WriteGroup expected opening tag 2"));
107        }
108        offset = tag_end;
109
110        let mut change_list = Vec::new();
111        loop {
112            if offset >= data.len() {
113                return Err(Error::decoding(offset, "WriteGroup missing closing tag 2"));
114            }
115            if change_list.len() >= MAX_DECODED_ITEMS {
116                return Err(Error::decoding(offset, "WriteGroup change list too large"));
117            }
118            let (tag, tag_end) = tags::decode_tag(data, offset)?;
119            if tag.is_closing_tag(2) {
120                offset = tag_end;
121                break;
122            }
123
124            // [0] channel OPTIONAL — peek for context 0
125            let mut channel = None;
126            if tag.is_context(0) {
127                let end = tag_end + tag.length as usize;
128                if end > data.len() {
129                    return Err(Error::decoding(tag_end, "WriteGroup truncated at channel"));
130                }
131                channel = Some(ObjectIdentifier::decode(&data[tag_end..end])?);
132                offset = end;
133            } else {
134                offset = tag_end - (tag_end - offset); // stay at current position
135            }
136
137            // [1] overridePriority OPTIONAL
138            let mut override_priority = None;
139            if offset < data.len() {
140                let (opt, new_off) = tags::decode_optional_context(data, offset, 1)?;
141                if let Some(content) = opt {
142                    override_priority = Some(primitives::decode_unsigned(content)? as u8);
143                    offset = new_off;
144                }
145            }
146
147            // [2] value (opening/closing tag 2 — inner)
148            let (tag, tag_end) = tags::decode_tag(data, offset)?;
149            if !tag.is_opening_tag(2) {
150                return Err(Error::decoding(
151                    offset,
152                    "WriteGroup expected opening tag 2 for value",
153                ));
154            }
155            let (value_bytes, new_off) = tags::extract_context_value(data, tag_end, 2)?;
156            let value = value_bytes.to_vec();
157            offset = new_off;
158
159            change_list.push(GroupChannelValue {
160                channel,
161                override_priority,
162                value,
163            });
164        }
165
166        // [3] inhibitDelay OPTIONAL
167        let mut inhibit_delay = None;
168        if offset < data.len() {
169            let (opt, new_off) = tags::decode_optional_context(data, offset, 3)?;
170            if let Some(content) = opt {
171                inhibit_delay = Some(!content.is_empty() && content[0] != 0);
172                offset = new_off;
173            }
174        }
175        let _ = offset;
176
177        Ok(Self {
178            group_number,
179            write_priority,
180            change_list,
181            inhibit_delay,
182        })
183    }
184}
185
186// ---------------------------------------------------------------------------
187// Tests
188// ---------------------------------------------------------------------------
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use bacnet_types::enums::ObjectType;
194
195    #[test]
196    fn write_group_round_trip() {
197        let req = WriteGroupRequest {
198            group_number: 1,
199            write_priority: 8,
200            change_list: vec![
201                GroupChannelValue {
202                    channel: Some(ObjectIdentifier::new(ObjectType::CHANNEL, 1).unwrap()),
203                    override_priority: Some(10),
204                    value: vec![0x44, 0x42, 0x90, 0x00, 0x00],
205                },
206                GroupChannelValue {
207                    channel: None,
208                    override_priority: None,
209                    value: vec![0x91, 0x01],
210                },
211            ],
212            inhibit_delay: Some(true),
213        };
214        let mut buf = BytesMut::new();
215        req.encode(&mut buf);
216        let decoded = WriteGroupRequest::decode(&buf).unwrap();
217        assert_eq!(req, decoded);
218    }
219
220    #[test]
221    fn write_group_minimal() {
222        let req = WriteGroupRequest {
223            group_number: 100,
224            write_priority: 16,
225            change_list: vec![GroupChannelValue {
226                channel: None,
227                override_priority: None,
228                value: vec![0x10],
229            }],
230            inhibit_delay: None,
231        };
232        let mut buf = BytesMut::new();
233        req.encode(&mut buf);
234        let decoded = WriteGroupRequest::decode(&buf).unwrap();
235        assert_eq!(req, decoded);
236    }
237
238    #[test]
239    fn write_group_priority_validation() {
240        // Encode with valid priority, then corrupt it
241        let req = WriteGroupRequest {
242            group_number: 1,
243            write_priority: 8,
244            change_list: vec![GroupChannelValue {
245                channel: None,
246                override_priority: None,
247                value: vec![0x10],
248            }],
249            inhibit_delay: None,
250        };
251        let mut buf = BytesMut::new();
252        req.encode(&mut buf);
253        let mut data = buf.to_vec();
254        // The write_priority byte is after the group number encoding.
255        // group_number=1: ctx tag 0 (09 01), then write_priority=8: ctx tag 1 (19 08)
256        // Find and change the priority value to 0
257        for i in 0..data.len() - 1 {
258            if data[i] == 0x19 && data[i + 1] == 0x08 {
259                data[i + 1] = 0x00; // set to 0 (invalid)
260                break;
261            }
262        }
263        assert!(WriteGroupRequest::decode(&data).is_err());
264    }
265
266    #[test]
267    fn write_group_empty_input() {
268        assert!(WriteGroupRequest::decode(&[]).is_err());
269    }
270}