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