Skip to main content

bacnet_objects/channel/
coercion.rs

1//! Coercing a Channel's value to each member's datatype (Clause 12.53.5.1,
2//! Table 12-63 and Coercion Rules 1 to 6 in Clauses 12.53.5.2 to 12.53.5.7).
3//!
4//! The table is keyed by the datatype written to Present_Value and the
5//! datatype of the member property. A pair it marks as needing no coercion
6//! passes the value on; a pair it marks as an invalid datatype, or a number
7//! outside a rule's limits, is a coercion failure: the member isn't written
8//! and the distribution ends FAILED (Clause 12.53.5.8).
9//!
10//! Choices this module makes where the clause leaves room:
11//!
12//! - A member's datatype is that of the value its property holds now (see
13//!   [`MemberDatatype::of`]); a NULL or constructed value counts as unknown,
14//!   which takes any value but a constructed one, as the table's first
15//!   column does. The exceptions are the properties whose datatype is one of
16//!   the constructed alternatives: Lighting_Command, Color_Command, and the
17//!   xy colours of a Color object (Addendum 135-2020ca's rows and columns of
18//!   the table, #1474). Each takes only its own alternative.
19//! - Where the table passes a value on between two different datatypes
20//!   (Unsigned and ENUMERATED, Unsigned and BACnetObjectIdentifier), the
21//!   number is kept and only the datatype changes. A number the target can't
22//!   hold is a failure.
23//! - Rule 3 bounds the Unsigned or ENUMERATED value itself at 2147483647,
24//!   whichever numeric target it goes to (INTEGER, REAL or Double); a larger
25//!   one fails. Unsigned to ENUMERATED and back is the pass-through above,
26//!   not Rule 3.
27//! - A REAL or Double going to an integer type keeps its integer part
28//!   (truncation toward zero) once the rule's range check passes.
29//! - Rules 5 and 6 print the INTEGER range as -2147483000 to 214783000. The
30//!   upper bound is read as 2147483000: one digit is missing, it would sit
31//!   below the Unsigned bound of the same rules, and it then mirrors the
32//!   lower bound.
33//! - Rule 6's REAL bound of about 3.4 x 10^38 is taken as the largest finite
34//!   REAL, `f32::MAX` (3.4028235 x 10^38), so every finite REAL is in range
35//!   and anything larger fails.
36//! - NaN and the infinities are inside no stated range, so they fail every
37//!   conversion that has one: REAL or Double to an integer type, and Double
38//!   to REAL. A REAL going to a Double has no range to check and passes as
39//!   it is, as does a value going to its own datatype. Rule 1 turns NaN into
40//!   TRUE, since it isn't zero.
41//! - The seven-significant-digit REAL limit in Rules 3 and 4 is read the
42//!   same way in both: the value is rounded to the nearest REAL, which is
43//!   what that precision means, and the rounding never fails the write. Only
44//!   the range bounds fail it.
45
46use bacnet_encoding::constructed::{
47    constructed_channel_value, decode_xy_color, ConstructedChannelValue,
48};
49use bacnet_types::enums::{ObjectType, PropertyIdentifier};
50use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
51
52/// The datatype of a Channel member's property, as Table 12-63's columns
53/// name them.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum MemberDatatype {
56    /// Not known: any primitive value passes on unchanged.
57    Unknown,
58    /// BOOLEAN.
59    Boolean,
60    /// Unsigned.
61    Unsigned,
62    /// INTEGER.
63    Integer,
64    /// REAL.
65    Real,
66    /// Double.
67    Double,
68    /// OCTET STRING.
69    OctetString,
70    /// CharacterString.
71    CharacterString,
72    /// BIT STRING.
73    BitString,
74    /// ENUMERATED.
75    Enumerated,
76    /// Date.
77    Date,
78    /// Time.
79    Time,
80    /// BACnetObjectIdentifier.
81    ObjectIdentifier,
82    /// BACnetLightingCommand.
83    LightingCommand,
84    /// BACnetColorCommand (Addendum 135-2020ca).
85    ColorCommand,
86    /// BACnetxyColor (Addendum 135-2020ca).
87    XyColor,
88}
89
90impl MemberDatatype {
91    /// The datatype of member property `property` of an object of type
92    /// `object_type`, whose value is `current` (`None` when it can't be
93    /// read).
94    ///
95    /// A few properties have a constructed datatype whatever form the object
96    /// keeps them in: Lighting_Command is a BACnetLightingCommand,
97    /// Color_Command a BACnetColorCommand, and Default_Color and a Color
98    /// object's Present_Value and Tracking_Value a BACnetxyColor. Any other
99    /// property takes the datatype of the primitive it holds; NULL,
100    /// constructed values and unreadable properties are
101    /// [`MemberDatatype::Unknown`].
102    pub fn of(
103        object_type: ObjectType,
104        property: PropertyIdentifier,
105        current: Option<&PropertyValue>,
106    ) -> Self {
107        use PropertyIdentifier as P;
108        match property {
109            P::LIGHTING_COMMAND => return Self::LightingCommand,
110            P::COLOR_COMMAND => return Self::ColorCommand,
111            P::DEFAULT_COLOR => return Self::XyColor,
112            P::PRESENT_VALUE | P::TRACKING_VALUE if object_type == ObjectType::COLOR => {
113                return Self::XyColor
114            }
115            _ => {}
116        }
117        match current {
118            Some(PropertyValue::Boolean(_)) => Self::Boolean,
119            Some(PropertyValue::Unsigned(_)) => Self::Unsigned,
120            Some(PropertyValue::Signed(_)) => Self::Integer,
121            Some(PropertyValue::Real(_)) => Self::Real,
122            Some(PropertyValue::Double(_)) => Self::Double,
123            Some(PropertyValue::OctetString(_)) => Self::OctetString,
124            Some(PropertyValue::CharacterString(_)) => Self::CharacterString,
125            Some(PropertyValue::BitString { .. }) => Self::BitString,
126            Some(PropertyValue::Enumerated(_)) => Self::Enumerated,
127            Some(PropertyValue::Date(_)) => Self::Date,
128            Some(PropertyValue::Time(_)) => Self::Time,
129            Some(PropertyValue::ObjectIdentifier(_)) => Self::ObjectIdentifier,
130            _ => Self::Unknown,
131        }
132    }
133}
134
135/// A channel value that can't go to a member: the table names an invalid
136/// datatype, or a number is outside its rule's limits.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub struct CoercionFailure;
139
140/// A number a channel value carries, by the coercion rule its datatype
141/// follows.
142#[derive(Clone, Copy)]
143enum Number {
144    /// BOOLEAN, as 0 or 1 (Rule 2).
145    Boolean(bool),
146    /// Unsigned or ENUMERATED (Rule 3).
147    Unsigned(u64),
148    /// INTEGER (Rule 4).
149    Integer(i64),
150    /// REAL (Rule 5).
151    Real(f32),
152    /// Double (Rule 6).
153    Double(f64),
154}
155
156/// Rules 5 and 6 bound a REAL or Double going to an Unsigned or ENUMERATED
157/// at this value, and to an INTEGER at it either way.
158const FLOAT_TO_INTEGER_LIMIT: f64 = 2_147_483_000.0;
159
160impl Number {
161    fn of(value: &PropertyValue) -> Option<Self> {
162        Some(match *value {
163            PropertyValue::Boolean(value) => Self::Boolean(value),
164            PropertyValue::Unsigned(value) => Self::Unsigned(value),
165            PropertyValue::Enumerated(value) => Self::Unsigned(value.into()),
166            PropertyValue::Signed(value) => Self::Integer(value.into()),
167            PropertyValue::Real(value) => Self::Real(value),
168            PropertyValue::Double(value) => Self::Double(value),
169            _ => return None,
170        })
171    }
172
173    /// Rule 1: zero is FALSE and anything else TRUE.
174    fn to_boolean(self) -> bool {
175        match self {
176            Self::Boolean(value) => value,
177            Self::Unsigned(value) => value != 0,
178            Self::Integer(value) => value != 0,
179            Self::Real(value) => value != 0.0,
180            Self::Double(value) => value != 0.0,
181        }
182    }
183
184    /// The value as an Unsigned or ENUMERATED, if the rule allows it.
185    fn to_unsigned(self) -> Option<u64> {
186        match self {
187            Self::Boolean(value) => Some(value.into()),
188            Self::Unsigned(value) => Some(value),
189            // Rule 4's upper bound, 2147483647, is the top of the INTEGER
190            // range, so only the sign can fail.
191            Self::Integer(value) => u64::try_from(value).ok(),
192            Self::Real(value) => float_to_unsigned(value.into()),
193            Self::Double(value) => float_to_unsigned(value),
194        }
195    }
196
197    /// The value as an INTEGER, if the rule allows it.
198    fn to_integer(self) -> Option<i32> {
199        match self {
200            Self::Boolean(value) => Some(value.into()),
201            Self::Unsigned(value) => rule_3_bound(value).map(|value| value as i32),
202            Self::Integer(value) => i32::try_from(value).ok(),
203            Self::Real(value) => float_to_integer(value.into()),
204            Self::Double(value) => float_to_integer(value),
205        }
206    }
207
208    /// The value as a REAL, if the rule allows it.
209    fn to_real(self) -> Option<f32> {
210        match self {
211            Self::Boolean(value) => Some(u8::from(value).into()),
212            // Rules 3 and 4: rounding to a REAL's precision isn't a failure,
213            // but an Unsigned past 2147483647 is (Rule 3).
214            Self::Unsigned(value) => rule_3_bound(value).map(|value| value as f32),
215            Self::Integer(value) => Some(value as f32),
216            Self::Real(value) => Some(value),
217            // Rule 6: a magnitude past the REAL range fails.
218            // Rule 6: the REAL bound is `f32::MAX`; NaN is inside no bound.
219            Self::Double(value) => (value.abs() <= f64::from(f32::MAX)).then_some(value as f32),
220        }
221    }
222
223    /// The value as a Double, if the rule allows it.
224    fn to_double(self) -> Option<f64> {
225        match self {
226            Self::Boolean(value) => Some(u8::from(value).into()),
227            Self::Unsigned(value) => rule_3_bound(value).map(f64::from),
228            Self::Integer(value) => Some(value as f64),
229            Self::Real(value) => Some(value.into()),
230            Self::Double(value) => Some(value),
231        }
232    }
233}
234
235/// Rule 3 bounds an Unsigned or ENUMERATED going to INTEGER, REAL or Double
236/// at 2147483647, the top of the INTEGER range.
237fn rule_3_bound(value: u64) -> Option<u32> {
238    i32::try_from(value).ok().map(|value| value as u32)
239}
240
241fn float_to_unsigned(value: f64) -> Option<u64> {
242    (0.0..=FLOAT_TO_INTEGER_LIMIT)
243        .contains(&value)
244        .then(|| value.trunc() as u64)
245}
246
247fn float_to_integer(value: f64) -> Option<i32> {
248    (-FLOAT_TO_INTEGER_LIMIT..=FLOAT_TO_INTEGER_LIMIT)
249        .contains(&value)
250        .then(|| value.trunc() as i32)
251}
252
253/// The value a member of datatype `target` gets when a Channel holding
254/// `value` distributes it (Table 12-63).
255///
256/// A constructed channel value, held as its framed octets, reaches only a
257/// member of its own datatype, the way a WriteProperty of that member's
258/// property would carry it: a lighting or colour command as the SEQUENCE
259/// inside its tag, and an xy colour as its two REALs.
260pub fn coerce_channel_value(
261    value: &PropertyValue,
262    target: MemberDatatype,
263) -> Result<PropertyValue, CoercionFailure> {
264    use ConstructedChannelValue as C;
265    use MemberDatatype as D;
266
267    if let PropertyValue::ApplicationData(octets) = value {
268        // A constructed value goes to a member of its own datatype and
269        // nowhere else, not even an unknown one.
270        let inner = || octets[1..octets.len() - 1].to_vec();
271        return match (constructed_channel_value(octets), target) {
272            (Some(C::LightingCommand), D::LightingCommand)
273            | (Some(C::ColorCommand), D::ColorCommand) => {
274                Ok(PropertyValue::ApplicationData(inner()))
275            }
276            (Some(C::XyColor), D::XyColor) => {
277                let (color, _) = decode_xy_color(octets, 1).map_err(|_| CoercionFailure)?;
278                Ok(PropertyValue::List(vec![
279                    PropertyValue::Real(color.x),
280                    PropertyValue::Real(color.y),
281                ]))
282            }
283            _ => Err(CoercionFailure),
284        };
285    }
286    match target {
287        D::LightingCommand | D::ColorCommand | D::XyColor => return Err(CoercionFailure),
288        D::Unknown => return Ok(value.clone()),
289        _ => {}
290    }
291    if *value == PropertyValue::Null {
292        return Ok(PropertyValue::Null);
293    }
294    if let Some(number) = Number::of(value) {
295        let coerced = match target {
296            D::Boolean => Some(PropertyValue::Boolean(number.to_boolean())),
297            D::Unsigned => number.to_unsigned().map(PropertyValue::Unsigned),
298            D::Integer => number.to_integer().map(PropertyValue::Signed),
299            D::Real => number.to_real().map(PropertyValue::Real),
300            D::Double => number.to_double().map(PropertyValue::Double),
301            D::Enumerated => number
302                .to_unsigned()
303                .and_then(|raw| u32::try_from(raw).ok())
304                .map(PropertyValue::Enumerated),
305            // The table passes an Unsigned to an object identifier as the
306            // same 32-bit number.
307            D::ObjectIdentifier => match *value {
308                PropertyValue::Unsigned(raw) => u32::try_from(raw)
309                    .ok()
310                    .and_then(|raw| ObjectIdentifier::decode(&raw.to_be_bytes()).ok())
311                    .map(PropertyValue::ObjectIdentifier),
312                _ => None,
313            },
314            _ => None,
315        };
316        return coerced.ok_or(CoercionFailure);
317    }
318    let passes = match (value, target) {
319        (PropertyValue::OctetString(_), D::OctetString)
320        | (PropertyValue::CharacterString(_), D::CharacterString)
321        | (PropertyValue::BitString { .. }, D::BitString)
322        | (PropertyValue::Date(_), D::Date)
323        | (PropertyValue::Time(_), D::Time)
324        | (PropertyValue::ObjectIdentifier(_), D::ObjectIdentifier) => true,
325        (PropertyValue::ObjectIdentifier(oid), D::Unsigned) => {
326            return Ok(PropertyValue::Unsigned(
327                u32::from_be_bytes(oid.encode()).into(),
328            ));
329        }
330        _ => false,
331    };
332    if passes {
333        Ok(value.clone())
334    } else {
335        Err(CoercionFailure)
336    }
337}