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