Skip to main content

bacnet_encoding/constructed/
action_list.rs

1//! `BACnetActionList` and `BACnetActionCommand` (ASHRAE 135-2020 Clause 21),
2//! the element of a Command object's Action array (Clause 12.10.8).
3//!
4//! An action list frames its commands in an opening/closing context tag `[0]`
5//! pair and puts them back to back inside it. A command is nine context-tagged
6//! members numbered in order: device `[0]` (optional), object `[1]`, property
7//! `[2]`, array index `[3]` (optional), the value framed in `[4]`, priority
8//! `[5]` and post delay `[6]` (both optional), then the quit-on-failure `[7]`
9//! and write-successful `[8]` BOOLEANs. An Action array read whole
10//! concatenates its lists with no further frame.
11
12#[cfg(not(feature = "std"))]
13use alloc::vec::Vec;
14
15use bacnet_types::constructed::{BACnetActionCommand, BACnetActionList};
16use bacnet_types::enums::PropertyIdentifier;
17use bacnet_types::error::{DecodeError, DecodingKind, Error, RangeError};
18use bytes::BytesMut;
19
20use super::tagged::{
21    decode_ctx_boolean, decode_ctx_object_id, decode_ctx_unsigned, decode_framed_value,
22    decode_optional_ctx, expect_opening,
23};
24use super::MAX_FRAMED_ITEMS;
25use crate::{primitives, tags};
26
27const WHAT: &str = "BACnetActionCommand";
28
29/// Encode one bare `BACnetActionCommand`, appending to `buf`.
30///
31/// Fails, leaving `buf` as it was, when the priority is outside 1..=16 or the
32/// value can't be encoded (see [`primitives::encode_property_value`]).
33pub fn encode_action_command(
34    buf: &mut BytesMut,
35    command: &BACnetActionCommand,
36) -> Result<(), Error> {
37    if let Some(priority) = command.priority {
38        if !(1..=16).contains(&priority) {
39            return Err(
40                RangeError::new("BACnetActionCommand: priority {} is outside 1..=16")
41                    .with_value(priority.into())
42                    .into(),
43            );
44        }
45    }
46    let start = buf.len();
47    if let Some(device) = &command.device_identifier {
48        primitives::encode_ctx_object_id(buf, 0, device);
49    }
50    primitives::encode_ctx_object_id(buf, 1, &command.object_identifier);
51    primitives::encode_ctx_enumerated(buf, 2, command.property_identifier.to_raw());
52    if let Some(index) = command.property_array_index {
53        primitives::encode_ctx_unsigned(buf, 3, u64::from(index));
54    }
55    tags::encode_opening_tag(buf, 4);
56    if let Err(error) = primitives::encode_property_value(buf, &command.property_value) {
57        buf.truncate(start);
58        return Err(error);
59    }
60    tags::encode_closing_tag(buf, 4);
61    if let Some(priority) = command.priority {
62        primitives::encode_ctx_unsigned(buf, 5, u64::from(priority));
63    }
64    if let Some(delay) = command.post_delay {
65        primitives::encode_ctx_unsigned(buf, 6, u64::from(delay));
66    }
67    primitives::encode_ctx_boolean(buf, 7, command.quit_on_failure);
68    primitives::encode_ctx_boolean(buf, 8, command.write_successful);
69    Ok(())
70}
71
72/// Decode one bare `BACnetActionCommand` at `offset`; returns it and the
73/// offset past its write-successful flag.
74///
75/// The value inside `[4]` decodes as one application value when it holds one
76/// element and as a [`PropertyValue::List`] otherwise (empty included). A
77/// context-tagged element in it decodes to [`PropertyValue::ApplicationData`],
78/// so the value re-encodes to the same octets.
79///
80/// [`PropertyValue::List`]: bacnet_types::primitives::PropertyValue::List
81/// [`PropertyValue::ApplicationData`]: bacnet_types::primitives::PropertyValue::ApplicationData
82pub fn decode_action_command(
83    data: &[u8],
84    offset: usize,
85) -> Result<(BACnetActionCommand, usize), Error> {
86    let (device_identifier, offset) =
87        decode_optional_ctx(data, offset, 0, WHAT, decode_ctx_object_id)?;
88    let (object_identifier, offset) = decode_ctx_object_id(data, offset, 1, WHAT)?;
89    let (property, offset) = decode_ctx_unsigned::<u32>(data, offset, 2, WHAT)?;
90    let property_identifier = PropertyIdentifier::from_raw(property);
91    let (property_array_index, offset) =
92        decode_optional_ctx(data, offset, 3, WHAT, decode_ctx_unsigned::<u32>)?;
93
94    let content = expect_opening(data, offset, 4, WHAT)?;
95    let (property_value, offset) = decode_framed_value(data, content, 4, WHAT)?;
96
97    let (priority, next) = decode_optional_ctx(data, offset, 5, WHAT, decode_ctx_unsigned::<u32>)?;
98    let priority = priority
99        .map(|value| {
100            u8::try_from(value)
101                .ok()
102                .filter(|p| (1..=16).contains(p))
103                .ok_or_else(|| {
104                    DecodeError::new(
105                        DecodingKind::InvalidEncoding,
106                        offset,
107                        "priority {} is outside 1..=16",
108                    )
109                    .within(WHAT)
110                    .with_value(value.into())
111                })
112        })
113        .transpose()?;
114    let (post_delay, offset) =
115        decode_optional_ctx(data, next, 6, WHAT, decode_ctx_unsigned::<u32>)?;
116    let (quit_on_failure, offset) = decode_ctx_boolean(data, offset, 7, WHAT)?;
117    let (write_successful, offset) = decode_ctx_boolean(data, offset, 8, WHAT)?;
118    Ok((
119        BACnetActionCommand {
120            device_identifier,
121            object_identifier,
122            property_identifier,
123            property_array_index,
124            property_value,
125            priority,
126            post_delay,
127            quit_on_failure,
128            write_successful,
129        },
130        offset,
131    ))
132}
133
134/// Encode one `BACnetActionList`, its `[0]` frame included, appending to
135/// `buf`. Fails, leaving `buf` as it was, when a command fails to encode.
136pub fn encode_action_list(buf: &mut BytesMut, list: &BACnetActionList) -> Result<(), Error> {
137    let start = buf.len();
138    tags::encode_opening_tag(buf, 0);
139    for command in &list.commands {
140        if let Err(error) = encode_action_command(buf, command) {
141            buf.truncate(start);
142            return Err(error);
143        }
144    }
145    tags::encode_closing_tag(buf, 0);
146    Ok(())
147}
148
149/// Decode one `BACnetActionList` at `offset`; returns it and the offset past
150/// its closing `[0]` tag, so an Action array is walked by calling this at
151/// each element's start.
152pub fn decode_action_list(data: &[u8], offset: usize) -> Result<(BACnetActionList, usize), Error> {
153    let content = expect_opening(data, offset, 0, "BACnetActionList")?;
154    let (inner, end) = tags::extract_context_value(data, content, 0)?;
155    let body = &data[..content + inner.len()];
156    let mut commands = Vec::new();
157    let mut offset = content;
158    while offset < body.len() {
159        if commands.len() >= MAX_FRAMED_ITEMS {
160            return Err(Error::overflow(
161                offset,
162                "BACnetActionList: commands exceed item limit",
163            ));
164        }
165        let (command, next) = decode_action_command(body, offset)?;
166        commands.push(command);
167        offset = next;
168    }
169    Ok((BACnetActionList { commands }, end))
170}