bacnet_objects/database/input_references.rs
1//! Judging a Pulse Converter's Input_Reference against the database (Clause
2//! 12.23.9, #1341).
3//!
4//! The object can't see the object its reference names, so the database
5//! looks: the reference is usable when it names another object here, at a
6//! property that reads as an Unsigned or an INTEGER. Anything else, a
7//! missing object or property included, is a CONFIGURATION_ERROR. A Pulse
8//! Converter's own properties are no input to it, an index goes only on an
9//! array, and index 0, an array's size, counts no pulses. A Priority_Array
10//! slot is judged by the datatype the object is commanded in, which its
11//! Relinquish_Default carries, so a slot that is relinquished for now (NULL)
12//! is usable but has nothing to count. An unset reference is no fault.
13//!
14//! The verdict is taken when the reference is written (the server calls
15//! [`ObjectDatabase::check_input_reference`] as the write commits), when the
16//! Pulse Converter is added, and whenever an object a reference names is
17//! added, replaced or removed (`ObjectDatabase::add` and `remove`).
18//! [`ObjectDatabase::count_pulse_inputs`] judges every converter again on
19//! each pass as it reads the property for the count (Clause 12.23.14), so a
20//! reference changed past the server's write paths, or a named property
21//! whose datatype changes, is caught there too.
22
23use bacnet_types::constructed::BACnetObjectPropertyReference;
24use bacnet_types::enums::{ObjectType, PropertyIdentifier};
25use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
26
27use super::ObjectDatabase;
28use crate::accumulator::InputReading;
29use crate::traits::BACnetObject;
30
31/// What a Pulse Converter's Input_Reference gives it.
32enum InputSource {
33 /// Nothing it can count from: a CONFIGURATION_ERROR.
34 Unusable,
35 /// A usable property with nothing to count right now: a relinquished
36 /// Priority_Array slot.
37 Idle,
38 /// A reading to count from.
39 Reading(InputReading),
40}
41
42impl ObjectDatabase {
43 /// Judge the Input_Reference of the Pulse Converter `oid` against this
44 /// database and hand it the verdict; returns whether its Reliability
45 /// changed, so the caller owes COV for it. Any other object, or one this
46 /// database doesn't hold, is left alone.
47 ///
48 /// The bundled server calls this as a write of Input_Reference commits,
49 /// over WriteProperty, WritePropertyMultiple or the local write path;
50 /// adding and removing objects calls it for the converters concerned.
51 pub fn check_input_reference(&mut self, oid: &ObjectIdentifier) -> bool {
52 let Some((usable, _)) = self.judge(oid) else {
53 return false;
54 };
55 self.objects
56 .get_mut(oid)
57 .is_some_and(|object| object.set_input_usable_internal(usable))
58 }
59
60 /// Judge again every Pulse Converter whose Input_Reference names
61 /// `changed`, and `changed` itself when it is one, after it was added,
62 /// replaced or removed; returns those whose Reliability changed.
63 pub(super) fn recheck_input_references(
64 &mut self,
65 changed: ObjectIdentifier,
66 ) -> Vec<ObjectIdentifier> {
67 let concerned: Vec<_> = self
68 .type_index
69 .get(&ObjectType::PULSE_CONVERTER)
70 .into_iter()
71 .flatten()
72 .copied()
73 .filter(|converter| {
74 *converter == changed
75 || self
76 .objects
77 .get(converter)
78 .and_then(|object| object.input_reference_internal().flatten())
79 .is_some_and(|reference| reference.object_identifier == changed)
80 })
81 .collect();
82 concerned
83 .into_iter()
84 .filter(|converter| self.check_input_reference(converter))
85 .collect()
86 }
87
88 /// Judge each Pulse Converter's Input_Reference again and hand over a
89 /// reading of the property it names, so the converter counts its
90 /// increase into Count (Clause 12.23.14); returns the converters whose
91 /// Reliability or Count changed, for COV. A converter with nothing to
92 /// read gets `None`, which drops its baseline.
93 ///
94 /// The bundled server runs this on every wake of its monotonic operation
95 /// task, so at least once a second. Reads go through
96 /// [`BACnetObject::read_property`], so they must stay bounded, as for
97 /// Averaging sampling.
98 pub fn count_pulse_inputs(&mut self) -> Vec<ObjectIdentifier> {
99 let converters = self.find_by_type(ObjectType::PULSE_CONVERTER);
100 let mut changed = Vec::new();
101 for oid in converters {
102 let Some((usable, reading)) = self.judge(&oid) else {
103 continue;
104 };
105 let Some(object) = self.objects.get_mut(&oid) else {
106 continue;
107 };
108 let judged = object.set_input_usable_internal(usable);
109 let counted = object.take_input_reading_internal(reading);
110 if judged || counted {
111 changed.push(oid);
112 }
113 }
114 changed
115 }
116
117 /// The verdict on the Pulse Converter `oid`'s Input_Reference and what
118 /// it reads now; `None` for an object that counts no referenced input.
119 fn judge(&self, oid: &ObjectIdentifier) -> Option<(bool, Option<InputReading>)> {
120 let reference = self.objects.get(oid)?.input_reference_internal()?;
121 Some(
122 match reference.map(|reference| self.input_source(*oid, reference)) {
123 None | Some(InputSource::Idle) => (true, None),
124 Some(InputSource::Unusable) => (false, None),
125 Some(InputSource::Reading(reading)) => (true, Some(reading)),
126 },
127 )
128 }
129
130 /// What the converter `converter` gets from `reference`; see the module
131 /// documentation for the rules.
132 fn input_source(
133 &self,
134 converter: ObjectIdentifier,
135 reference: &BACnetObjectPropertyReference,
136 ) -> InputSource {
137 if reference.object_identifier == converter {
138 return InputSource::Unusable;
139 }
140 let Some(target) = self.get(&reference.object_identifier) else {
141 return InputSource::Unusable;
142 };
143 let property = PropertyIdentifier::from_raw(reference.property_identifier);
144 let index = reference.property_array_index;
145 match index {
146 Some(0) => return InputSource::Unusable,
147 Some(_) if !target.is_array_property(property) => return InputSource::Unusable,
148 _ => {}
149 }
150 let slot = property == PropertyIdentifier::PRIORITY_ARRAY && index.is_some();
151 if slot
152 && integer(target.read_property(PropertyIdentifier::RELINQUISH_DEFAULT, None)).is_none()
153 {
154 return InputSource::Unusable;
155 }
156 let value = target.read_property(property, index);
157 if slot && matches!(value, Ok(PropertyValue::Null)) {
158 return InputSource::Idle;
159 }
160 match integer(value) {
161 Some(value) => InputSource::Reading(InputReading {
162 value,
163 wraps_after: wrap_bound(target, property, index),
164 }),
165 None => InputSource::Unusable,
166 }
167 }
168}
169
170/// An Unsigned or INTEGER read, widened.
171fn integer(read: Result<PropertyValue, bacnet_types::error::Error>) -> Option<i128> {
172 match read {
173 Ok(PropertyValue::Unsigned(value)) => Some(i128::from(value)),
174 Ok(PropertyValue::Signed(value)) => Some(i128::from(value)),
175 _ => None,
176 }
177}
178
179/// The value an Accumulator's Present_Value wraps after, its Max_Pres_Value
180/// (Clause 12.61.4); `None` for any other property.
181fn wrap_bound(
182 target: &dyn BACnetObject,
183 property: PropertyIdentifier,
184 index: Option<u32>,
185) -> Option<u64> {
186 let counter = target.object_identifier().object_type() == ObjectType::ACCUMULATOR
187 && property == PropertyIdentifier::PRESENT_VALUE
188 && index.is_none();
189 match target.read_property(PropertyIdentifier::MAX_PRES_VALUE, None) {
190 Ok(PropertyValue::Unsigned(max)) if counter => Some(max),
191 _ => None,
192 }
193}
194
195#[cfg(test)]
196#[path = "input_references_tests.rs"]
197mod tests;