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