bacnet_encoding/constructed/color_command.rs
1//! Codecs for the colour values of Addendum 135-2020ca (Clause 21):
2//! `BACnetxyColor`, a Color object's Present_Value, Tracking_Value and
3//! Default_Color, and `BACnetColorCommand`, the Color_Command of a Color or
4//! Color Temperature object.
5//!
6//! An xy colour is two application-tagged REALs, x then y, with nothing
7//! around them. The command is a SEQUENCE with no frame of its own: the
8//! operation under primitive context tag 0, then any of five optional fields
9//! in ascending tag order. The target colour sits between an opening and a
10//! closing tag 1; the target colour temperature, fade time, ramp rate and
11//! step increment are Unsigneds under tags 2 to 5.
12
13use bacnet_types::constructed::{BACnetColorCommand, BACnetXyColor};
14use bacnet_types::enums::ColorOperation;
15use bacnet_types::error::Error;
16use bytes::BytesMut;
17
18use super::members::unsigned_field;
19use super::tagged::{
20 decode_app_fixed, decode_optional_ctx, expect_closing, expect_end, expect_opening,
21 next_is_opening,
22};
23use crate::primitives;
24use crate::tags::{self, app_tag};
25
26/// The production name the command's decode errors carry.
27const WHAT: &str = "color command";
28
29/// The production name an xy colour's decode errors carry.
30const XY: &str = "xy color";
31
32/// Encode one `BACnetxyColor`: x, then y, each an application-tagged REAL.
33pub fn encode_xy_color(buf: &mut BytesMut, value: &BACnetXyColor) {
34 primitives::encode_app_real(buf, value.x);
35 primitives::encode_app_real(buf, value.y);
36}
37
38/// Read one application-tagged REAL at `offset` for `what`.
39fn app_real(data: &[u8], offset: usize, what: &str) -> Result<(f32, usize), Error> {
40 let (octets, end) = decode_app_fixed(data, offset, app_tag::REAL, 4, what)?;
41 Ok((primitives::decode_real(octets)?, end))
42}
43
44/// Decode one `BACnetxyColor` at `offset`: two application-tagged REALs.
45///
46/// Returns the colour and the offset just past y. Any REAL decodes, NaN and
47/// values outside 0.0 to 1.0 included; the receiver checks the range.
48/// Contents that run past the end of `data` are [`Error::BufferTooShort`];
49/// a missing coordinate, another tag, or a REAL that isn't four octets long
50/// is [`Error::Decoding`].
51pub fn decode_xy_color(data: &[u8], offset: usize) -> Result<(BACnetXyColor, usize), Error> {
52 let (x, offset) = app_real(data, offset, XY)?;
53 let (y, offset) = app_real(data, offset, XY)?;
54 Ok((BACnetXyColor { x, y }, offset))
55}
56
57/// Encode one unframed `BACnetColorCommand` SEQUENCE: the operation, then
58/// each field that is present.
59pub fn encode_color_command(buf: &mut BytesMut, value: &BACnetColorCommand) {
60 primitives::encode_ctx_enumerated(buf, 0, value.operation.to_raw());
61 if let Some(color) = &value.target_color {
62 tags::encode_opening_tag(buf, 1);
63 encode_xy_color(buf, color);
64 tags::encode_closing_tag(buf, 1);
65 }
66 let unsigneds = [
67 value.target_color_temperature,
68 value.fade_time,
69 value.ramp_rate,
70 value.step_increment,
71 ];
72 for (tag, field) in (2..).zip(unsigneds) {
73 if let Some(field) = field {
74 primitives::encode_ctx_unsigned(buf, tag, field.into());
75 }
76 }
77}
78
79/// A decoded command, the offset just past its last field, and the first
80/// field too wide for its type, if any.
81pub(super) type Fields = (BACnetColorCommand, usize, Option<&'static str>);
82
83/// Read the target colour framed in tag 1 at `offset`, if one opens there.
84fn target_color(data: &[u8], offset: usize) -> Result<(Option<BACnetXyColor>, usize), Error> {
85 if !next_is_opening(data, offset, 1)? {
86 return Ok((None, offset));
87 }
88 let content = expect_opening(data, offset, 1, WHAT)?;
89 let (color, end) = decode_xy_color(data, content)?;
90 let end = expect_closing(data, end, 1, WHAT)?;
91 Ok((Some(color), end))
92}
93
94/// Read the fields of one unframed `BACnetColorCommand` at `offset`,
95/// stopping at the first tag that isn't the next optional field. A field
96/// too wide for its type is returned rather than refused, so the caller can
97/// check the rest of its input first.
98pub(super) fn decode_fields(data: &[u8], offset: usize) -> Result<Fields, Error> {
99 let mut oversized = None;
100 let (operation, offset) = unsigned_field::<u32>(
101 data,
102 offset,
103 0,
104 WHAT,
105 "operation [0] exceeds 32 bits",
106 &mut oversized,
107 )?;
108 let (target_color, offset) = target_color(data, offset)?;
109 let mut unsigned = |offset: usize, tag: u8, too_wide: &'static str| {
110 decode_optional_ctx(data, offset, tag, WHAT, |data, at, tag, _| {
111 unsigned_field::<u32>(data, at, tag, WHAT, too_wide, &mut oversized)
112 })
113 };
114 let (target_color_temperature, offset) =
115 unsigned(offset, 2, "target-color-temperature [2] exceeds 32 bits")?;
116 let (fade_time, offset) = unsigned(offset, 3, "fade-time [3] exceeds 32 bits")?;
117 let (ramp_rate, offset) = unsigned(offset, 4, "ramp-rate [4] exceeds 32 bits")?;
118 let (step_increment, offset) = unsigned(offset, 5, "step-increment [5] exceeds 32 bits")?;
119 let command = BACnetColorCommand {
120 operation: ColorOperation::from_raw(operation),
121 target_color,
122 target_color_temperature,
123 fade_time,
124 ramp_rate,
125 step_increment,
126 };
127 Ok((command, offset, oversized))
128}
129
130/// The [`Error::OutOfRange`] for a field too wide for its type.
131fn too_wide(field: &'static str) -> Error {
132 Error::OutOfRange(format!("{WHAT} {field}"))
133}
134
135/// Decode one unframed `BACnetColorCommand` SEQUENCE at `offset`.
136///
137/// Returns the command and the offset just past its last field. Decoding
138/// stops at the first tag that isn't the next optional field, so a caller
139/// that expects exactly one command must check that the returned offset
140/// reaches the end of its data, or the closing tag of its frame. A caller
141/// whose whole input must be one command uses [`decode_color_command_value`]
142/// instead, which checks that before it reports a field too wide for its
143/// type.
144///
145/// Field values aren't range-checked here; the receiver does that. Failures
146/// come in three kinds:
147///
148/// - contents that run past the end of `data`: [`Error::BufferTooShort`];
149/// - any other malformed field (a missing operation, an Unsigned with no
150/// contents octets, or one of more than four that opens with a zero octet,
151/// or a target colour that isn't two four-octet application REALs closed
152/// by tag 1, a frame cut off at a tag boundary included): [`Error::Decoding`];
153/// - a well-formed command whose operation or an Unsigned field needs more
154/// than 32 bits: [`Error::OutOfRange`], naming the first such field. A
155/// malformed field anywhere takes precedence.
156///
157/// Up to four contents octets, an Unsigned or ENUMERATED may open with zero
158/// octets; [`encode_color_command`] always writes the shortest form.
159pub fn decode_color_command(
160 data: &[u8],
161 offset: usize,
162) -> Result<(BACnetColorCommand, usize), Error> {
163 let (command, end, oversized) = decode_fields(data, offset)?;
164 match oversized {
165 Some(field) => Err(too_wide(field)),
166 None => Ok((command, end)),
167 }
168}
169
170/// Decode a `BACnetColorCommand` that fills `data`, as a WriteProperty of
171/// Color_Command carries it.
172///
173/// Fails as [`decode_color_command`] does, and with [`Error::Decoding`] when
174/// octets follow the last field. A field too wide for its type is
175/// [`Error::OutOfRange`] only when the rest of `data` is one well-formed
176/// command, so a broken input is always reported as broken.
177pub fn decode_color_command_value(data: &[u8]) -> Result<BACnetColorCommand, Error> {
178 let (command, end, oversized) = decode_fields(data, 0)?;
179 expect_end(data, end, end, WHAT)?;
180 match oversized {
181 Some(field) => Err(too_wide(field)),
182 None => Ok(command),
183 }
184}