Skip to main content

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}