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 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;