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}