Skip to main content

bacnet_encoding/constructed/
channel_value.rs

1//! BACnetChannelValue (Clause 21, as Addendum 135-2020ca extends it): the
2//! value a Channel object's Present_Value holds and a WriteGroup change-list
3//! entry carries.
4//!
5//! The production is an untagged CHOICE. Every alternative but three is a
6//! primitive with its own application tag. The three constructed ones each
7//! sit between an opening and a closing context tag: a BACnetLightingCommand
8//! in `[0]`, and the addendum's BACnetxyColor in `[1]` and
9//! BACnetColorCommand in `[2]`, each read with its own codec. The codec here
10//! only finds where one value ends and which alternative it is, so callers
11//! can keep the octets as they arrived.
12
13use bacnet_types::error::Error;
14
15use super::color_command::{self, decode_xy_color};
16use super::lighting_command;
17use super::tagged::expect_closing;
18use crate::primitives;
19use crate::tags::{self, TagClass};
20
21/// A constructed alternative of a BACnetChannelValue: which context tag
22/// frames it.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ConstructedChannelValue {
25    /// `[0]`: a BACnetLightingCommand.
26    LightingCommand,
27    /// `[1]`: a BACnetxyColor (Addendum 135-2020ca).
28    XyColor,
29    /// `[2]`: a BACnetColorCommand (Addendum 135-2020ca).
30    ColorCommand,
31}
32
33impl ConstructedChannelValue {
34    /// The alternative context tag `tag` frames, if any.
35    fn of_tag(tag: u8) -> Option<Self> {
36        match tag {
37            0 => Some(Self::LightingCommand),
38            1 => Some(Self::XyColor),
39            2 => Some(Self::ColorCommand),
40            _ => None,
41        }
42    }
43}
44
45/// Return the offset just past the single BACnetChannelValue starting at
46/// `offset`.
47///
48/// The value is one well-formed application-tagged primitive of any character
49/// set, or one of the constructed alternatives:
50///
51/// - `[0]` a BACnetLightingCommand. It must decode (see
52///   [`decode_lighting_command`]) with no field too wide for its type, and
53///   its priority, when present, must be 1 to 16; its levels, fade time and
54///   operation aren't range-checked.
55/// - `[1]` a BACnetxyColor: two application-tagged REALs (see
56///   [`decode_xy_color`]), of any value.
57/// - `[2]` a BACnetColorCommand. It must decode (see
58///   [`decode_color_command`]) with no field too wide for its type; its
59///   operation and fields aren't range-checked, as the colour object that
60///   takes it checks them.
61///
62/// Each must close right after its last field. Contents that run past the
63/// end of `data` fail with [`Error::BufferTooShort`]; any other malformed
64/// value with [`Error::Decoding`], including a field of the wrong length
65/// that the data also cuts short, and a field too wide for its type.
66///
67/// [`decode_lighting_command`]: super::decode_lighting_command
68/// [`decode_color_command`]: super::decode_color_command
69pub fn channel_value_end(data: &[u8], offset: usize) -> Result<usize, Error> {
70    if offset >= data.len() {
71        return Err(Error::decoding(offset, "missing channel value"));
72    }
73    let (tag, pos) = tags::decode_tag(data, offset)?;
74    if tag.class == TagClass::Application {
75        return primitives::validate_application_value(data, offset);
76    }
77    let kind = tag
78        .is_opening
79        .then(|| ConstructedChannelValue::of_tag(tag.number))
80        .flatten()
81        .ok_or_else(|| {
82            Error::decoding(
83                offset,
84                "a channel value is an application-tagged primitive, or a lighting command, xy \
85                 color or color command in context tag 0, 1 or 2",
86            )
87        })?;
88    match kind {
89        ConstructedChannelValue::LightingCommand => {
90            let (command, end, oversized) = lighting_command::decode_fields(data, pos)?;
91            let after = expect_closing(data, end, 0, "lighting command")?;
92            if let Some(field) = oversized {
93                return Err(Error::decoding(offset, format!("lighting command {field}")));
94            }
95            if let Some(priority) = command.priority.filter(|p| !(1..=16).contains(p)) {
96                return Err(Error::out_of_range(
97                    offset,
98                    format!("lighting command priority {priority} out of range 1-16"),
99                ));
100            }
101            Ok(after)
102        }
103        ConstructedChannelValue::XyColor => {
104            let (_, end) = decode_xy_color(data, pos)?;
105            expect_closing(data, end, 1, "xy color")
106        }
107        ConstructedChannelValue::ColorCommand => {
108            let (_, end, oversized) = color_command::decode_fields(data, pos)?;
109            let after = expect_closing(data, end, 2, "color command")?;
110            if let Some(field) = oversized {
111                return Err(Error::decoding(offset, format!("color command {field}")));
112            }
113            Ok(after)
114        }
115    }
116}
117
118/// Which constructed alternative `data` is, when it is exactly one
119/// well-formed constructed BACnetChannelValue; `None` for a primitive,
120/// anything malformed, or octets left over.
121pub fn constructed_channel_value(data: &[u8]) -> Option<ConstructedChannelValue> {
122    let (tag, _) = tags::decode_tag(data, 0).ok()?;
123    if tag.class != TagClass::Context || !tag.is_opening {
124        return None;
125    }
126    let kind = ConstructedChannelValue::of_tag(tag.number)?;
127    (channel_value_end(data, 0).ok()? == data.len()).then_some(kind)
128}