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}