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