Skip to main content

bacnet_services/
write_group.rs

1//! WriteGroup service per ASHRAE 135-2020 Clause 15.11.
2//!
3//! The wire form follows the WriteGroup-Request production in Clause 21.3.2 and the
4//! BACnetGroupChannelValue / BACnetChannelValue productions in Clause 21.6.
5
6#[cfg(not(feature = "std"))]
7use alloc::vec::Vec;
8
9use core::num::NonZeroU32;
10
11use bacnet_encoding::constructed::channel_value_end;
12use bacnet_encoding::primitives;
13use bacnet_encoding::tags;
14use bacnet_types::error::{DecodeError, DecodingKind, EncodeError, Error};
15use bytes::BytesMut;
16
17use crate::common::MAX_DECODED_ITEMS;
18use bacnet_encoding::constructed::tagged::{
19    decode_ctx_boolean, decode_ctx_primitive, next_is_context,
20};
21
22// ---------------------------------------------------------------------------
23// Validation helpers
24// ---------------------------------------------------------------------------
25
26/// Range of a write priority or overriding priority (Clauses 15.11.1.1.2 and 21.6).
27const PRIORITY_RANGE: core::ops::RangeInclusive<u64> = 1..=16;
28
29/// `label` names the priority, with a placeholder for its value.
30fn check_priority(label: &'static str, value: u8) -> Result<u8, Error> {
31    if PRIORITY_RANGE.contains(&u64::from(value)) {
32        Ok(value)
33    } else {
34        Err(EncodeError::new(label).with_value(value.into()).into())
35    }
36}
37
38/// `label` names the priority, with a placeholder for its value.
39fn decode_priority(label: &'static str, offset: usize, value: u64) -> Result<u8, Error> {
40    if PRIORITY_RANGE.contains(&value) {
41        Ok(value as u8)
42    } else {
43        Err(DecodeError::new(DecodingKind::InvalidEncoding, offset, label)
44            .with_value(value)
45            .into())
46    }
47}
48
49// ---------------------------------------------------------------------------
50// WriteGroupRequest
51// ---------------------------------------------------------------------------
52
53/// A single entry in the WriteGroup change list (BACnetGroupChannelValue, Clause 21.6).
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct GroupChannelValue {
56    /// \[0\] channel number, matched against a Channel object's Channel_Number.
57    pub channel: u16,
58    /// \[1\] overriding-priority OPTIONAL: priority 1-16 replacing the request's write priority
59    /// for this entry.
60    pub override_priority: Option<u8>,
61    /// The BACnetChannelValue, already encoded and carried without a wrapper tag: one
62    /// well-formed application-tagged primitive, or a constructed context-\[0\] lighting
63    /// command, context-\[1\] xy colour or context-\[2\] colour command whose fields are
64    /// in order with valid lengths. [`WriteGroupRequest::encode`] rejects anything else.
65    pub value: Vec<u8>,
66}
67
68/// WriteGroup-Request service parameters (Clause 15.11.1; encoding in Clauses 21.3.2 and 21.6).
69///
70/// Fields, in order:
71/// - group number, context \[0\], mandatory (Unsigned32);
72/// - write priority, context \[1\], mandatory (1-16);
73/// - change list, context \[2\] constructed, mandatory, holding one or more
74///   BACnetGroupChannelValue entries;
75/// - inhibit delay, context \[3\], optional (Boolean).
76///
77/// Each change-list entry is a channel number in context \[0\] (Unsigned16), an optional
78/// overriding priority in context \[1\] (1-16), and then the channel value itself with no
79/// wrapper tag (an untagged CHOICE).
80///
81/// For channel 5 holding REAL 72.0 the change-list entry is `09 05 44 42 90 00 00`.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct WriteGroupRequest {
84    /// Control group to write, matched against each Channel object's Control_Groups; group 0 is
85    /// reserved, so it cannot be represented.
86    pub group_number: NonZeroU32,
87    /// Priority (1-16) used for writes unless an entry overrides it; [`encode`](Self::encode)
88    /// rejects values outside that range.
89    pub write_priority: u8,
90    /// Channel values to apply, each addressed by channel number; must not be empty.
91    pub change_list: Vec<GroupChannelValue>,
92    /// When true, Channel objects that allow it skip their configured execution delay; `None` or
93    /// false leaves delays in force.
94    pub inhibit_delay: Option<bool>,
95}
96
97impl WriteGroupRequest {
98    /// Encode the request parameters into `buf`.
99    ///
100    /// Fails, leaving `buf` untouched, if the write priority or an overriding priority is outside
101    /// 1-16, the change list is empty, or an entry's value is not a single well-formed
102    /// BACnetChannelValue.
103    pub fn encode(&self, buf: &mut BytesMut) -> Result<(), Error> {
104        check_priority(
105            "WriteGroup write-priority {} out of range 1-16",
106            self.write_priority,
107        )?;
108        if self.change_list.is_empty() {
109            return Err(Error::encode("WriteGroup change list must contain at least one entry"));
110        }
111        for entry in &self.change_list {
112            if let Some(priority) = entry.override_priority {
113                check_priority(
114                    "WriteGroup override-priority {} out of range 1-16",
115                    priority,
116                )?;
117            }
118            let well_formed =
119                channel_value_end(&entry.value, 0).is_ok_and(|end| end == entry.value.len());
120            if !well_formed {
121                return Err(EncodeError::new(
122                    "WriteGroup value for channel {} is not a single BACnetChannelValue",
123                )
124                .with_value(entry.channel.into())
125                .into());
126            }
127        }
128
129        primitives::encode_ctx_unsigned(buf, 0, u64::from(self.group_number.get()));
130        primitives::encode_ctx_unsigned(buf, 1, u64::from(self.write_priority));
131        tags::encode_opening_tag(buf, 2);
132        for entry in &self.change_list {
133            primitives::encode_ctx_unsigned(buf, 0, u64::from(entry.channel));
134            if let Some(priority) = entry.override_priority {
135                primitives::encode_ctx_unsigned(buf, 1, u64::from(priority));
136            }
137            buf.extend_from_slice(&entry.value);
138        }
139        tags::encode_closing_tag(buf, 2);
140        if let Some(inhibit) = self.inhibit_delay {
141            primitives::encode_ctx_boolean(buf, 3, inhibit);
142        }
143        Ok(())
144    }
145
146    /// Decode the request from service-request octets.
147    ///
148    /// Fails on malformed, truncated or out-of-range input and on trailing data.
149    pub fn decode(data: &[u8]) -> Result<Self, Error> {
150        // [0] group-number
151        let (content, mut offset) = decode_ctx_primitive(data, 0, 0, "WriteGroup group-number")?;
152        let group_raw = primitives::decode_unsigned(content)?;
153        let group_number = u32::try_from(group_raw)
154            .ok()
155            .and_then(NonZeroU32::new)
156            .ok_or_else(|| {
157                DecodeError::new(
158                    DecodingKind::OutOfRange,
159                    0,
160                    "WriteGroup group number {} out of range 1-4294967295",
161                )
162                .with_value(group_raw)
163            })?;
164
165        // [1] write-priority
166        let priority_pos = offset;
167        let (content, end) = decode_ctx_primitive(data, offset, 1, "WriteGroup write-priority")?;
168        let write_priority = decode_priority(
169            "WriteGroup write-priority {} out of range 1-16",
170            priority_pos,
171            primitives::decode_unsigned(content)?,
172        )?;
173        offset = end;
174
175        // [2] change-list
176        if offset >= data.len() {
177            return Err(Error::decoding(offset, "WriteGroup missing change list"));
178        }
179        let (tag, tag_end) = tags::decode_tag(data, offset)?;
180        if !tag.is_opening_tag(2) {
181            return Err(Error::decoding(offset, "WriteGroup expected opening tag 2"));
182        }
183        offset = tag_end;
184
185        let mut change_list = Vec::new();
186        loop {
187            if offset >= data.len() {
188                return Err(Error::decoding(offset, "WriteGroup missing closing tag 2"));
189            }
190            let (tag, tag_end) = tags::decode_tag(data, offset)?;
191            if tag.is_closing_tag(2) {
192                offset = tag_end;
193                break;
194            }
195            if change_list.len() >= MAX_DECODED_ITEMS {
196                return Err(Error::decoding(offset, "WriteGroup change list too large"));
197            }
198
199            // [0] channel
200            let (content, end) = decode_ctx_primitive(data, offset, 0, "WriteGroup channel")?;
201            let channel_raw = primitives::decode_unsigned(content)?;
202            let channel = u16::try_from(channel_raw).map_err(|_| {
203                DecodeError::new(
204                    DecodingKind::OutOfRange,
205                    offset,
206                    "WriteGroup channel {} exceeds 65535",
207                )
208                .with_value(channel_raw)
209            })?;
210            offset = end;
211
212            // [1] overriding-priority OPTIONAL
213            let mut override_priority = None;
214            if next_is_context(data, offset, 1)? {
215                let (content, end) =
216                    decode_ctx_primitive(data, offset, 1, "WriteGroup override-priority")?;
217                override_priority = Some(decode_priority(
218                    "WriteGroup override-priority {} out of range 1-16",
219                    offset,
220                    primitives::decode_unsigned(content)?,
221                )?);
222                offset = end;
223            }
224
225            // BACnetChannelValue (untagged CHOICE)
226            let end = channel_value_end(data, offset)?;
227            change_list.push(GroupChannelValue {
228                channel,
229                override_priority,
230                value: data[offset..end].to_vec(),
231            });
232            offset = end;
233        }
234        if change_list.is_empty() {
235            return Err(Error::decoding(
236                offset,
237                "WriteGroup change list must contain at least one entry",
238            ));
239        }
240
241        // [3] inhibit-delay OPTIONAL
242        let mut inhibit_delay = None;
243        if next_is_context(data, offset, 3)? {
244            let (inhibit, end) = decode_ctx_boolean(data, offset, 3, "WriteGroup inhibit-delay")?;
245            inhibit_delay = Some(inhibit);
246            offset = end;
247        }
248        if offset != data.len() {
249            return Err(Error::decoding(offset, "WriteGroup has trailing data"));
250        }
251
252        Ok(Self {
253            group_number,
254            write_priority,
255            change_list,
256            inhibit_delay,
257        })
258    }
259}
260
261#[cfg(test)]
262#[path = "write_group_tests.rs"]
263mod tests;