Skip to main content

bacnet_objects/accumulator/
pulse_converter.rs

1//! Pulse Converter (type 24) object, per ASHRAE 135-2020 §12.23 Table 12-27.
2//!
3//! Count holds the input pulses. In service, Present_Value is Count times
4//! Scale_Factor, worked out on each read; Out_Of_Service decouples it and
5//! makes it writable. A write of Adjust_Value takes the whole quotient of the
6//! value over Scale_Factor off Count, keeping the old Count in
7//! Count_Before_Change and the time in Count_Change_Time. `add_pulses`
8//! accumulates input and stamps Update_Time.
9//!
10//! Reliability reports whether Input_Reference names a property the object
11//! can count from (Clause 12.23.9): CONFIGURATION_ERROR, and with it the
12//! FAULT flag, while it names one that isn't Unsigned or INTEGER, or one the
13//! object can't reach. Only the database can look at the named object, so it
14//! judges the reference (`ObjectDatabase::check_input_reference`, #1341) and
15//! hands the verdict over through `set_input_usable_internal`. An unset
16//! reference is no fault: the object then stands for the physical input
17//! itself (Clause 12.23.6). While Out_Of_Service is TRUE, Reliability is
18//! decoupled from the verdict and takes a client's value, for simulation
19//! (Clause 12.23.10); the verdict is kept, and the return to service applies
20//! it.
21//!
22//! A running server also counts from the property (Clause 12.23.14 leaves
23//! the means local): at least once a second the database reads it
24//! (`ObjectDatabase::count_pulse_inputs`), judging the reference again as it
25//! does, and adds each increase over the last reading to Count, as
26//! `add_pulses` would. The first reading after the reference is set, changed
27//! or becomes readable only sets the baseline, so pointing the reference at
28//! a property holding a larger value counts nothing. A reading below the last
29//! one is a wrap when the source is an Accumulator's Present_Value, which
30//! counts modulo Max_Pres_Value + 1 (Clause 12.61.4): the pulses up to the
31//! maximum, round to zero and on to the reading are counted. From any other
32//! source such a reading, a reset or a wrap whose bound is unknown, only
33//! sets the baseline again.
34
35use bacnet_types::constructed::BACnetObjectPropertyReference;
36use bacnet_types::enums::{
37    EngineeringUnits, EventState, ObjectType, PropertyIdentifier, Reliability,
38};
39use bacnet_types::error::Error;
40use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
41use std::borrow::Cow;
42use std::sync::Arc;
43
44use crate::clock::{stamp_datetime, ClockReader, UNSPECIFIED_DATETIME};
45use crate::common::{self, read_common_properties};
46use crate::reference::{self, ReferenceFrame};
47use crate::traits::BACnetObject;
48
49mod metadata;
50
51/// One reading of the property a Pulse Converter counts from (#1341), which
52/// the database hands over through
53/// [`BACnetObject::take_input_reading_internal`].
54#[doc(hidden)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct InputReading {
57    /// The property's value, an Unsigned or INTEGER widened to `i128`.
58    pub value: i128,
59    /// The largest value the source holds before it wraps to zero, for a
60    /// source that counts modulo a bound: an Accumulator's Present_Value and
61    /// its Max_Pres_Value (Clause 12.61.4). `None` for any other source.
62    pub wraps_after: Option<u64>,
63}
64
65/// BACnet Pulse Converter object — converts accumulated pulses to an analog value.
66pub struct PulseConverterObject {
67    oid: ObjectIdentifier,
68    name: String,
69    description: String,
70    /// Present_Value while Out_Of_Service is TRUE. In service, Present_Value
71    /// is derived from Count and this copy is unused.
72    decoupled_present_value: f32,
73    units: EngineeringUnits,
74    scale_factor: f32,
75    adjust_value: f32,
76    /// Count; `count` times `scale_factor` always fits a finite REAL.
77    count: u64,
78    update_time: (Date, Time),
79    count_change_time: (Date, Time),
80    count_before_change: u64,
81    cov_increment: f32,
82    input_reference: Option<BACnetObjectPropertyReference>,
83    /// The database's last verdict on Input_Reference: TRUE when it names a
84    /// property this object can't count from.
85    input_unusable: bool,
86    /// The last reading of the property Input_Reference names, which the
87    /// next reading's increase is counted from.
88    last_input: Option<i128>,
89    status_flags: StatusFlags,
90    event_state: EventState,
91    out_of_service: bool,
92    reliability: Reliability,
93    /// Database-owned clock used to stamp Update_Time and Count_Change_Time.
94    clock: Option<Arc<dyn ClockReader>>,
95}
96
97impl PulseConverterObject {
98    /// Create a new Pulse Converter object with default values.
99    pub fn new(
100        instance: u32,
101        name: impl Into<String>,
102        units: EngineeringUnits,
103    ) -> Result<Self, Error> {
104        let oid = ObjectIdentifier::new(ObjectType::PULSE_CONVERTER, instance)?;
105        Ok(Self {
106            oid,
107            name: name.into(),
108            description: String::new(),
109            decoupled_present_value: 0.0,
110            units,
111            scale_factor: 1.0,
112            adjust_value: 0.0,
113            count: 0,
114            update_time: UNSPECIFIED_DATETIME,
115            count_change_time: UNSPECIFIED_DATETIME,
116            count_before_change: 0,
117            cov_increment: 0.0,
118            input_reference: None,
119            input_unusable: false,
120            last_input: None,
121            status_flags: StatusFlags::empty(),
122            event_state: EventState::NORMAL,
123            out_of_service: false,
124            reliability: Reliability::NO_FAULT_DETECTED,
125            clock: None,
126        })
127    }
128
129    /// Set the description string.
130    pub fn set_description(&mut self, desc: impl Into<String>) {
131        self.description = desc.into();
132    }
133
134    /// Set Input_Reference, the property the input is counted from. A
135    /// reference to the reserved instance 4194303 clears it, as a client's
136    /// write of the unset form does (#1417).
137    ///
138    /// A database the object is in judges the reference when it is added
139    /// (see the module documentation); a client's write is judged as it
140    /// commits.
141    pub fn set_input_reference(&mut self, r: BACnetObjectPropertyReference) {
142        self.store_input_reference(reference::set_or_unset(r));
143    }
144
145    /// Store Input_Reference. An unset one is no fault, so clearing the
146    /// reference clears a CONFIGURATION_ERROR at once; a new reference keeps
147    /// the last verdict until the database judges it.
148    fn store_input_reference(&mut self, reference: Option<BACnetObjectPropertyReference>) {
149        if reference.is_none() {
150            self.set_input_usable_internal(true);
151        }
152        // Another property starts its own baseline.
153        if reference != self.input_reference {
154            self.last_input = None;
155        }
156        self.input_reference = reference;
157    }
158
159    /// Reliability as the input reference decides it, out of service aside.
160    fn evaluated_reliability(&self) -> Reliability {
161        if self.input_unusable {
162            Reliability::CONFIGURATION_ERROR
163        } else {
164            Reliability::NO_FAULT_DETECTED
165        }
166    }
167
168    /// The current Count.
169    pub fn count(&self) -> u64 {
170        self.count
171    }
172
173    /// Accumulate `pulses` input pulses into Count (Clause 12.23.14).
174    ///
175    /// Count is read-only over the network, so this is how the application
176    /// feeds it from a physical input; a running server feeds it from the
177    /// property Input_Reference names (see the module documentation). A
178    /// change stamps Update_Time from the Device
179    /// clock (unspecified fields without one). Count keeps accumulating while
180    /// Out_Of_Service is TRUE; only Present_Value stops following it. A sum
181    /// past the largest Unsigned this object holds, or one that would scale
182    /// past the largest REAL, is refused with VALUE_OUT_OF_RANGE and nothing
183    /// changes. Zero pulses change nothing.
184    pub fn add_pulses(&mut self, pulses: u64) -> Result<(), Error> {
185        if pulses == 0 {
186            return Ok(());
187        }
188        let count = self
189            .count
190            .checked_add(pulses)
191            .ok_or_else(common::value_out_of_range_error)?;
192        scaled(count, self.scale_factor)?;
193        self.count = count;
194        self.update_time = stamp_datetime(self.clock.as_deref());
195        Ok(())
196    }
197
198    /// Present_Value: Count times Scale_Factor in service, or the decoupled
199    /// copy while Out_Of_Service is TRUE.
200    fn present_value(&self) -> f32 {
201        if self.out_of_service {
202            self.decoupled_present_value
203        } else {
204            // Every change to Count or Scale_Factor checked this product.
205            scaled(self.count, self.scale_factor).unwrap_or_default()
206        }
207    }
208
209    /// Apply a write of Adjust_Value as one step (Clause 12.23.13): store it,
210    /// keep Count in Count_Before_Change, take the truncated quotient of the
211    /// value over Scale_Factor off Count, and stamp Count_Change_Time.
212    ///
213    /// A Count that would leave the Unsigned range, a quotient that is not a
214    /// number (a zero Scale_Factor), or a new Count that no longer scales to
215    /// a finite REAL is refused with VALUE_OUT_OF_RANGE before anything
216    /// changes.
217    fn adjust(&mut self, adjust_value: f32) -> Result<(), Error> {
218        common::reject_non_finite(adjust_value)?;
219        let quotient = (f64::from(adjust_value) / f64::from(self.scale_factor)).trunc();
220        // Any quotient this large moves Count out of the u64 range anyway.
221        if !quotient.is_finite() || quotient.abs() > u64::MAX as f64 {
222            return Err(common::value_out_of_range_error());
223        }
224        let count = u64::try_from(i128::from(self.count) - quotient as i128)
225            .map_err(|_| common::value_out_of_range_error())?;
226        scaled(count, self.scale_factor)?;
227        self.adjust_value = adjust_value;
228        self.count_before_change = self.count;
229        self.count = count;
230        self.count_change_time = stamp_datetime(self.clock.as_deref());
231        Ok(())
232    }
233}
234
235/// Count times Scale_Factor as a REAL, refused with VALUE_OUT_OF_RANGE when
236/// the product is too large for one.
237fn scaled(count: u64, scale_factor: f32) -> Result<f32, Error> {
238    let value = (count as f64 * f64::from(scale_factor)) as f32;
239    if value.is_finite() {
240        Ok(value)
241    } else {
242        Err(common::value_out_of_range_error())
243    }
244}
245
246fn datetime_value((date, time): (Date, Time)) -> PropertyValue {
247    PropertyValue::List(vec![PropertyValue::Date(date), PropertyValue::Time(time)])
248}
249
250impl BACnetObject for PulseConverterObject {
251    fn object_identifier(&self) -> ObjectIdentifier {
252        self.oid
253    }
254
255    fn object_name(&self) -> &str {
256        &self.name
257    }
258
259    fn read_property(
260        &self,
261        property: PropertyIdentifier,
262        array_index: Option<u32>,
263    ) -> Result<PropertyValue, Error> {
264        if let Some(result) = read_common_properties!(self, property, array_index) {
265            return result;
266        }
267        match property {
268            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
269                ObjectType::PULSE_CONVERTER.to_raw(),
270            )),
271            p if p == PropertyIdentifier::PRESENT_VALUE => {
272                Ok(PropertyValue::Real(self.present_value()))
273            }
274            p if p == PropertyIdentifier::UNITS => {
275                Ok(PropertyValue::Enumerated(self.units.to_raw()))
276            }
277            p if p == PropertyIdentifier::SCALE_FACTOR => {
278                Ok(PropertyValue::Real(self.scale_factor))
279            }
280            p if p == PropertyIdentifier::ADJUST_VALUE => {
281                Ok(PropertyValue::Real(self.adjust_value))
282            }
283            p if p == PropertyIdentifier::COUNT => Ok(PropertyValue::Unsigned(self.count)),
284            p if p == PropertyIdentifier::UPDATE_TIME => Ok(datetime_value(self.update_time)),
285            p if p == PropertyIdentifier::COUNT_CHANGE_TIME => {
286                Ok(datetime_value(self.count_change_time))
287            }
288            p if p == PropertyIdentifier::COUNT_BEFORE_CHANGE => {
289                Ok(PropertyValue::Unsigned(self.count_before_change))
290            }
291            p if p == PropertyIdentifier::COV_INCREMENT => {
292                Ok(PropertyValue::Real(self.cov_increment))
293            }
294            // Periodic COV notifications are not implemented, and zero is the
295            // value that says so (Clause 13.1).
296            p if p == PropertyIdentifier::COV_PERIOD => Ok(PropertyValue::Unsigned(0)),
297            // The reference's Clause 21 encoding (#1312), or while unset an
298            // Accumulator's Present_Value, the usual input (Clause 12.23.6),
299            // at the reserved instance (#1417).
300            p if p == PropertyIdentifier::INPUT_REFERENCE => {
301                Ok(reference::object_property_reference_value(
302                    self.input_reference.as_ref(),
303                    ObjectType::ACCUMULATOR,
304                ))
305            }
306            p if p == PropertyIdentifier::EVENT_STATE => {
307                Ok(PropertyValue::Enumerated(self.event_state.to_raw()))
308            }
309            _ => Err(common::unknown_property_error()),
310        }
311    }
312
313    fn write_property(
314        &mut self,
315        property: PropertyIdentifier,
316        _array_index: Option<u32>,
317        value: PropertyValue,
318        _priority: Option<u8>,
319    ) -> Result<(), Error> {
320        let in_service_value = self.present_value();
321        let was_out_of_service = self.out_of_service;
322        if let Some(result) =
323            common::write_out_of_service(&mut self.out_of_service, property, &value)
324        {
325            // Going out of service freezes Present_Value where it was; writes
326            // then land on the decoupled copy.
327            if self.out_of_service && !was_out_of_service {
328                self.decoupled_present_value = in_service_value;
329            }
330            // Back in service, Reliability follows the input reference
331            // again, whatever a client simulated meanwhile.
332            if was_out_of_service && !self.out_of_service {
333                self.reliability = self.evaluated_reliability();
334            }
335            return result;
336        }
337        if let Some(result) = common::write_description(&mut self.description, property, &value) {
338            return result;
339        }
340        if let Some(result) = common::write_cov_increment(&mut self.cov_increment, property, &value)
341        {
342            return result;
343        }
344        match property {
345            p if p == PropertyIdentifier::PRESENT_VALUE => {
346                if !self.out_of_service {
347                    return Err(common::write_access_denied_error());
348                }
349                if let PropertyValue::Real(v) = value {
350                    common::reject_non_finite(v)?;
351                    self.decoupled_present_value = v;
352                    Ok(())
353                } else {
354                    Err(common::invalid_data_type_error())
355                }
356            }
357            p if p == PropertyIdentifier::SCALE_FACTOR => {
358                if let PropertyValue::Real(v) = value {
359                    common::reject_non_finite(v)?;
360                    scaled(self.count, v)?;
361                    self.scale_factor = v;
362                    Ok(())
363                } else {
364                    Err(common::invalid_data_type_error())
365                }
366            }
367            p if p == PropertyIdentifier::ADJUST_VALUE => {
368                if let PropertyValue::Real(v) = value {
369                    self.adjust(v)
370                } else {
371                    Err(common::invalid_data_type_error())
372                }
373            }
374            // Reliability can leave NO_FAULT_DETECTED (the input reference's
375            // CONFIGURATION_ERROR), so it takes a client's value while out of
376            // service (Clause 12.23.10) and is read-only otherwise.
377            p if p == PropertyIdentifier::RELIABILITY => {
378                if !self.out_of_service {
379                    return Err(common::write_access_denied_error());
380                }
381                let PropertyValue::Enumerated(raw) = value else {
382                    return Err(common::invalid_data_type_error());
383                };
384                let reliability = Reliability::from_raw(raw);
385                if !common::is_reliability_value_valid(reliability) {
386                    return Err(common::value_out_of_range_error());
387                }
388                self.reliability = reliability;
389                Ok(())
390            }
391            // Input_Reference is BACnetObjectPropertyReference (Table
392            // 12-27): its context-tagged members, the unset form clearing it.
393            p if p == PropertyIdentifier::INPUT_REFERENCE => {
394                let reference = reference::decode_reference_write(&value, ReferenceFrame::Bare)?;
395                self.store_input_reference(reference);
396                Ok(())
397            }
398            _ => Err(crate::common::unhandled_write_error(
399                self.property_metadata().as_ref(),
400                property,
401                _array_index,
402            )),
403        }
404    }
405
406    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
407        metadata::for_pulse_converter_object(self)
408    }
409
410    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
411        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
412    }
413
414    fn supports_cov(&self) -> bool {
415        true
416    }
417
418    fn cov_increment(&self) -> Option<f64> {
419        Some(f64::from(self.cov_increment))
420    }
421
422    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
423        self.clock = clock;
424    }
425
426    fn input_reference_internal(&self) -> Option<Option<&BACnetObjectPropertyReference>> {
427        Some(self.input_reference.as_ref())
428    }
429
430    fn set_input_usable_internal(&mut self, usable: bool) -> bool {
431        self.input_unusable = !usable;
432        if self.out_of_service {
433            return false;
434        }
435        let reliability = self.evaluated_reliability();
436        std::mem::replace(&mut self.reliability, reliability) != reliability
437    }
438
439    fn take_input_reading_internal(&mut self, reading: Option<InputReading>) -> bool {
440        let Some(reading) = reading else {
441            self.last_input = None;
442            return false;
443        };
444        let Some(last) = self.last_input.replace(reading.value) else {
445            return false;
446        };
447        let pulses = if reading.value >= last {
448            reading.value - last
449        } else {
450            match reading.wraps_after.map(i128::from) {
451                // Up to the maximum, one more round to zero, then on to the
452                // reading.
453                Some(max) if last <= max => max - last + 1 + reading.value,
454                _ => return false,
455            }
456        };
457        if pulses == 0 {
458            return false;
459        }
460        self.add_pulses(u64::try_from(pulses).unwrap_or(u64::MAX))
461            .is_ok()
462    }
463}
464
465#[cfg(test)]
466#[path = "../pulse_converter_policy_tests.rs"]
467mod pulse_converter_policy_tests;
468
469#[cfg(test)]
470mod tests;