Skip to main content

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;