Skip to main content

bacnet_objects/trend/
mod.rs

1//! TrendLog (type 20) and TrendLogMultiple (type 27) objects per ASHRAE 135-2020.
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use bacnet_types::bitstring::EventTransitionBits;
8use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetLogRecord};
9use bacnet_types::enums::{
10    ErrorClass, ErrorCode, LoggingType, NotifyType, ObjectType, PropertyIdentifier, Reliability,
11};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
14
15use crate::clock::ClockReader;
16use crate::common::{self, read_property_list_property};
17use crate::log_buffer::{
18    log_buffer_read_denied, LogBufferRecords, LogRecordBuffer, LogRecordIdentity,
19};
20use crate::log_lifecycle::LogLifecycle;
21use crate::log_reporting::{impl_buffer_ready_reporting, BufferReadyReporting};
22use crate::log_window::LogWindow;
23use crate::traits::BACnetObject;
24use acquisition::{Acquisition, Rules};
25
26mod acquisition;
27mod metadata;
28mod multiple;
29mod multiple_metadata;
30mod references;
31
32pub use acquisition::DEFAULT_LOG_INTERVAL;
33pub use multiple::TrendLogMultipleObject;
34pub use references::MAX_LOG_DEVICE_OBJECT_PROPERTIES;
35
36/// BACnet TrendLog object (type 20, Clause 12.25).
37///
38/// Ring buffer of timestamped values of the one property
39/// Log_DeviceObjectProperty names. The database's trend poller samples it
40/// every Log_Interval while Logging_Type is POLLED, at clock-aligned times
41/// when Align_Intervals asks for them, and once for each Trigger while it is
42/// TRIGGERED; the application may also call `add_record()`. Records are kept
43/// only while Enable is TRUE and the local time lies between Start_Time and
44/// Stop_Time. COV logging isn't carried out, so Logging_Type refuses it.
45pub struct TrendLogObject {
46    oid: ObjectIdentifier,
47    name: String,
48    description: String,
49    log_enable: bool,
50    stop_when_full: bool,
51    buffer_size: u32,
52    log_buffer: LogRecordBuffer,
53    reliability: Reliability,
54    log_device_object_property: Option<BACnetDeviceObjectPropertyReference>,
55    acquisition: Acquisition,
56    window: LogWindow,
57    reporting: BufferReadyReporting,
58    clock: Option<Arc<dyn ClockReader>>,
59}
60
61impl TrendLogObject {
62    /// Create a new Trend Log object with logging enabled; `buffer_size` is the record capacity.
63    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
64        let oid = ObjectIdentifier::new(ObjectType::TREND_LOG, instance)?;
65        Ok(Self {
66            oid,
67            name: name.into(),
68            description: String::new(),
69            log_enable: true,
70            stop_when_full: false,
71            buffer_size,
72            log_buffer: LogRecordBuffer::new(buffer_size),
73            reliability: Reliability::NO_FAULT_DETECTED,
74            log_device_object_property: None,
75            acquisition: Acquisition::new(Rules::TrendLog),
76            window: LogWindow::default(),
77            reporting: BufferReadyReporting::default(),
78            clock: None,
79        })
80    }
81
82    /// Add a BACnetLogRecord to the trend log buffer.
83    ///
84    /// Success does not guarantee a resident ordinary record: disabled logging
85    /// and a record outside the Start_Time / Stop_Time window are ignored,
86    /// zero-capacity logging may only count, and a stop-before-full
87    /// transition records status instead. Missing/invalid status clocks fail
88    /// atomically with DEVICE / OPERATIONAL_PROBLEM. A successful call serves
89    /// a pending Trigger, which reads FALSE again, even when the record is
90    /// ignored (Enable FALSE, or outside the window): the acquisition was
91    /// made.
92    pub fn add_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
93        self.lifecycle().try_add_ordinary(record)?;
94        self.acquisition.acquired();
95        Ok(())
96    }
97
98    /// Get the current buffer contents.
99    pub fn records(&self) -> &VecDeque<BACnetLogRecord> {
100        self.log_buffer.records()
101    }
102
103    /// Clear the buffer.
104    pub fn clear(&mut self) {
105        self.log_buffer.clear();
106    }
107
108    /// Set the description string.
109    pub fn set_description(&mut self, desc: impl Into<String>) {
110        self.description = desc.into();
111    }
112
113    /// Set Log_DeviceObjectProperty, the property the log samples, as local
114    /// configuration: the log buffer is left as it is.
115    ///
116    /// A reference may name another device; the poller then logs a failure
117    /// for each sample instead of reading it. `None`, or a reference whose
118    /// object or Device is at the reserved instance 4194303, leaves the log
119    /// without one (#1417). A Device member that isn't a Device identifier
120    /// fails with PROPERTY / VALUE_OUT_OF_RANGE and changes nothing.
121    pub fn set_log_device_object_property(
122        &mut self,
123        reference: Option<BACnetDeviceObjectPropertyReference>,
124    ) -> Result<(), Error> {
125        if let Some(reference) = &reference {
126            crate::device_reference::check_device_member(reference.device_identifier)?;
127        }
128        self.log_device_object_property = reference.and_then(crate::device_reference::set_or_unset);
129        Ok(())
130    }
131
132    /// A client's write of Log_DeviceObjectProperty (#1234): one reference,
133    /// the unset form clearing it (#1417). See [`references::check_written`]
134    /// for the refusals; Null is no reference, so INVALID_DATA_TYPE. A new
135    /// value purges the buffer, leaving a BUFFER_PURGED status record (Clause
136    /// 12.25.8); without a valid clock the purge fails with DEVICE /
137    /// OPERATIONAL_PROBLEM and nothing changes. Writing the value already held
138    /// changes nothing.
139    fn write_log_device_object_property(&mut self, value: PropertyValue) -> Result<(), Error> {
140        let reference = crate::device_reference::decode_reference(&value)?;
141        references::check_written(&reference)?;
142        let reference = crate::device_reference::set_or_unset(reference);
143        if reference != self.log_device_object_property {
144            self.lifecycle().purge()?;
145            self.log_device_object_property = reference;
146        }
147        Ok(())
148    }
149
150    /// Set Logging_Type, as a client's write does: POLLED or TRIGGERED. This
151    /// device has no COV acquisition yet (#1480), so COV, like any other
152    /// value, is PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED (Clause
153    /// 12.25.26) and changes nothing. POLLED with a zero Log_Interval sets
154    /// [`DEFAULT_LOG_INTERVAL`]; TRIGGERED sets Log_Interval to zero.
155    pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), Error> {
156        self.acquisition.set_logging_type(logging_type)
157    }
158
159    /// Set Log_Interval in hundredths of a second, as a client's write does.
160    /// While Logging_Type is TRIGGERED it is read-only and this is PROPERTY /
161    /// WRITE_ACCESS_DENIED. A POLLED log's nonzero interval set to zero would
162    /// switch it to COV logging (Clause 12.25.9), which is refused with
163    /// PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as COV itself is.
164    pub fn set_log_interval(&mut self, hundredths: u32) -> Result<(), Error> {
165        self.acquisition.set_log_interval(hundredths)
166    }
167
168    /// Set Start_Time, the local date and time from which records are kept,
169    /// as local configuration: nothing is recorded for the change itself,
170    /// but the log notes at once where the window stands, so a client's
171    /// write that then opens or shuts it is recorded. Every field
172    /// unspecified leaves the start open. Any other value has to name an
173    /// actual date and time, or it is PROPERTY / VALUE_OUT_OF_RANGE: the
174    /// weekday may stay unspecified, and unspecified seconds or hundredths
175    /// count as zero.
176    pub fn set_start_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
177        self.lifecycle()
178            .configure_window(PropertyIdentifier::START_TIME, (date, time))
179    }
180
181    /// Set Stop_Time, the local date and time from which records are no
182    /// longer kept, under the same rules as
183    /// [`set_start_time`](Self::set_start_time).
184    pub fn set_stop_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
185        self.lifecycle()
186            .configure_window(PropertyIdentifier::STOP_TIME, (date, time))
187    }
188
189    /// Set Align_Intervals: whether a POLLED log acquires at clock-aligned
190    /// times.
191    pub fn set_align_intervals(&mut self, align: bool) {
192        self.acquisition.set_align_intervals(align);
193    }
194
195    /// Set Interval_Offset, the delay after each aligned boundary, in
196    /// hundredths; it applies modulo Log_Interval.
197    pub fn set_interval_offset(&mut self, hundredths: u32) {
198        self.acquisition.set_interval_offset(hundredths);
199    }
200
201    /// Ask for one acquisition, as a local process writing Trigger TRUE does.
202    /// Only a TRIGGERED log takes it; any other is PROPERTY /
203    /// NOT_CONFIGURED_FOR_TRIGGERED_LOGGING.
204    pub fn trigger(&mut self) -> Result<(), Error> {
205        self.acquisition.trigger()
206    }
207
208    /// Set Notification_Threshold, the number of records that makes a
209    /// BUFFER_READY report; zero, the default, makes none.
210    pub fn set_notification_threshold(&mut self, threshold: u32) {
211        self.reporting.set_notification_threshold(threshold);
212    }
213
214    /// Set Notification_Class: the number of the Notification Class whose
215    /// recipients get the log's reports (0 by default).
216    pub fn set_notification_class(&mut self, class: u32) {
217        self.reporting.set_notification_class(class);
218    }
219
220    /// Set Event_Enable; a report goes out only while its TO_NORMAL flag is
221    /// set, as it is by default.
222    pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
223        self.reporting.set_event_enable(enable);
224    }
225
226    /// Set Notify_Type, sent with each report (EVENT by default).
227    pub fn set_notify_type(&mut self, notify_type: NotifyType) {
228        self.reporting.set_notify_type(notify_type);
229    }
230
231    fn lifecycle(&mut self) -> LogLifecycle<'_, BACnetLogRecord> {
232        LogLifecycle::new(
233            &mut self.log_buffer,
234            &mut self.log_enable,
235            &mut self.stop_when_full,
236            self.clock.as_ref(),
237            &mut self.window,
238            &mut self.reporting,
239        )
240    }
241}
242
243impl BACnetObject for TrendLogObject {
244    fn object_identifier(&self) -> ObjectIdentifier {
245        self.oid
246    }
247
248    fn object_name(&self) -> &str {
249        &self.name
250    }
251
252    fn read_property(
253        &self,
254        property: PropertyIdentifier,
255        array_index: Option<u32>,
256    ) -> Result<PropertyValue, Error> {
257        // Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
258        // Trigger, then Start_Time and Stop_Time.
259        if let Some(value) = self
260            .acquisition
261            .read(property)
262            .or_else(|| self.window.read(property))
263        {
264            return Ok(value);
265        }
266        let total = self.log_buffer.total_record_count();
267        if let Some(result) = self.reporting.read(property, array_index, total) {
268            return result;
269        }
270        match property {
271            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
272                Ok(PropertyValue::ObjectIdentifier(self.oid))
273            }
274            p if p == PropertyIdentifier::OBJECT_NAME => {
275                Ok(PropertyValue::CharacterString(self.name.clone()))
276            }
277            p if p == PropertyIdentifier::DESCRIPTION => {
278                Ok(PropertyValue::CharacterString(self.description.clone()))
279            }
280            p if p == PropertyIdentifier::OBJECT_TYPE => {
281                Ok(PropertyValue::Enumerated(ObjectType::TREND_LOG.to_raw()))
282            }
283            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
284            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
285                Ok(PropertyValue::Boolean(self.stop_when_full))
286            }
287            p if p == PropertyIdentifier::BUFFER_SIZE => {
288                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
289            }
290            p if p == PropertyIdentifier::RECORD_COUNT => {
291                Ok(PropertyValue::Unsigned(self.records().len() as u64))
292            }
293            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
294                Ok(PropertyValue::Unsigned(u64::from(total)))
295            }
296            // Clause 12.25.30 lets only IN_ALARM (from Event_State) and FAULT
297            // (from Reliability) move on a Trend Log; OVERRIDDEN and
298            // OUT_OF_SERVICE are always FALSE. Table 12-29 has no
299            // Out_Of_Service property (#985).
300            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
301                StatusFlags::empty(),
302                self.reliability,
303                false,
304                self.reporting.event_state(),
305            )),
306            p if p == PropertyIdentifier::RELIABILITY => {
307                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
308            }
309            // ReadRange pages it through `log_buffer_internal`.
310            p if p == PropertyIdentifier::LOG_BUFFER => Err(log_buffer_read_denied()),
311            // The Clause 21 encoding; while no reference is set, the empty
312            // element a Trend Log Multiple grows by (#1417).
313            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => {
314                Ok(crate::device_reference::optional_reference_value(
315                    self.log_device_object_property.as_ref(),
316                    ObjectType::ANALOG_INPUT,
317                ))
318            }
319            p if p == PropertyIdentifier::PROPERTY_LIST => {
320                read_property_list_property(&self.property_list(), array_index)
321            }
322            _ => Err(Error::Protocol {
323                class: ErrorClass::PROPERTY.to_raw() as u32,
324                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
325            }),
326        }
327    }
328
329    fn write_property(
330        &mut self,
331        property: PropertyIdentifier,
332        array_index: Option<u32>,
333        value: PropertyValue,
334        _priority: Option<u8>,
335    ) -> Result<(), Error> {
336        if property == PropertyIdentifier::LOG_ENABLE {
337            if let PropertyValue::Boolean(v) = value {
338                return self.lifecycle().write_enable(v);
339            }
340            return Err(Error::Protocol {
341                class: ErrorClass::PROPERTY.to_raw() as u32,
342                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
343            });
344        }
345        if let Some(result) = self.acquisition.write(property, &value) {
346            return result;
347        }
348        if let Some(result) = self.window.write(property, &value) {
349            // A change that opens or closes the window is recorded at once.
350            result?;
351            self.lifecycle().refresh_window();
352            return Ok(());
353        }
354        if property == PropertyIdentifier::STOP_WHEN_FULL {
355            if let PropertyValue::Boolean(v) = value {
356                return self.lifecycle().write_stop_when_full(v);
357            }
358            return Err(Error::Protocol {
359                class: ErrorClass::PROPERTY.to_raw() as u32,
360                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
361            });
362        }
363        if property == PropertyIdentifier::RECORD_COUNT {
364            if let PropertyValue::Unsigned(0) = value {
365                return self.lifecycle().purge();
366            }
367            return Err(Error::Protocol {
368                class: ErrorClass::PROPERTY.to_raw() as u32,
369                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
370            });
371        }
372        // Clause 12.25 Table 12-29 lists Reliability as plain O with no
373        // writability footnote, and unlike the intrinsic-reporting objects the
374        // Trend Log Reliability_Evaluation_Inhibit paragraph requires
375        // NO_FAULT_DETECTED while evaluation is inhibited, without an exception
376        // for a client-supplied Reliability value while Out_Of_Service is TRUE.
377        // Nothing in Clause 12.25 grants a
378        // network client this property: the log owns it (logging status and
379        // fault indication), so every write is refused.
380        if property == PropertyIdentifier::RELIABILITY {
381            return Err(Error::Protocol {
382                class: ErrorClass::PROPERTY.to_raw() as u32,
383                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
384            });
385        }
386        if property == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY {
387            return self.write_log_device_object_property(value);
388        }
389        let total = self.log_buffer.total_record_count();
390        if let Some(result) = self.reporting.write(property, array_index, &value, total) {
391            return result;
392        }
393        if let Some(result) = common::write_description(&mut self.description, property, &value) {
394            return result;
395        }
396        Err(crate::common::unhandled_write_error(
397            self.property_metadata().as_ref(),
398            property,
399            array_index,
400        ))
401    }
402
403    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
404        metadata::for_object(self)
405    }
406
407    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
408        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
409    }
410
411    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
412        self.clock = clock;
413    }
414
415    fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
416        Some(self.log_buffer.identities())
417    }
418
419    fn log_buffer_internal(&self) -> Option<&dyn LogBufferRecords> {
420        Some(&self.log_buffer)
421    }
422
423    fn add_trend_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
424        self.add_record(record)
425    }
426
427    fn refresh_log_window_internal(&mut self) -> bool {
428        self.lifecycle().refresh_window()
429    }
430
431    impl_buffer_ready_reporting!(reporting, log_buffer);
432}
433
434#[cfg(test)]
435mod log_record_tests;
436
437#[cfg(test)]
438mod multiple_options_tests;
439
440#[cfg(test)]
441mod options_tests;
442
443#[cfg(test)]
444mod reference_tests;
445
446#[cfg(test)]
447mod tests;