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}