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    /// Total_Record_Count: the records collected since the log was created
104    /// or restored, going from 2^32 - 1 on to 1. The newest record in
105    /// [`records`](Self::records) carries this number. Save the two to
106    /// [restore](Self::restore_log_buffer) the log later.
107    pub fn total_record_count(&self) -> u32 {
108        self.log_buffer.total_record_count()
109    }
110
111    /// Restore the log buffer, as a device restarting with saved records
112    /// does, or seed Total_Record_Count with no records, as a test that
113    /// needs the count near its wrap does (#1537).
114    ///
115    /// `records` become the resident records, oldest first. The newest is
116    /// numbered `total_record_count` and each one before it one less, going
117    /// from 1 back to 2^32 - 1, so a count below the number of records is a
118    /// count that has wrapped. The records are kept as given, whatever
119    /// Enable and the Start_Time / Stop_Time window say, and nothing is
120    /// recorded for the restore. BUFFER_READY counts from the restored
121    /// count, as when detection starts.
122    ///
123    /// Fails with [`Error::OutOfRange`], or a record's encoding error,
124    /// leaving the log unchanged, when there are more records than
125    /// Buffer_Size, when records come with a count of zero, when Stop_When_Full
126    /// and Enable are both TRUE and the records fill the buffer (such a log
127    /// stops before its last slot is taken), or when a record would not
128    /// encode.
129    ///
130    /// Restore a log before adding it to an `ObjectDatabase`. A running
131    /// server reaches its objects only through `BACnetObject`, which offers
132    /// no restore on purpose: renumbering a log that peers are reading would
133    /// change what their sequence numbers mean, with no BUFFER_PURGED record
134    /// to tell them, and the BUFFER_READY reports already sent would name
135    /// counts that no longer hold.
136    ///
137    /// A device restoring its saved records after a restart then calls
138    /// [`record_interruption`](Self::record_interruption) with the time it
139    /// came back: Clause 12.25.14 gives a log that status when a power
140    /// failure or reset broke its collection, so readers know samples may
141    /// be missing. That record counts toward Total_Record_Count like any
142    /// other, numbered one past the restored count.
143    pub fn restore_log_buffer(
144        &mut self,
145        total_record_count: u32,
146        records: impl IntoIterator<Item = BACnetLogRecord>,
147    ) -> Result<(), Error> {
148        self.lifecycle()
149            .restore(total_record_count, records.into_iter().collect())
150    }
151
152    /// Append a LOG_INTERRUPTED status record stamped `date` and `time`, as
153    /// a log restored after a restart does (#1537); see
154    /// [`restore_log_buffer`](Self::restore_log_buffer).
155    ///
156    /// The record goes in whatever Enable and the window say, counts toward
157    /// Total_Record_Count, and pushes out the oldest record of a full
158    /// buffer. It also carries LOG_DISABLED while collection is off; when it
159    /// fills a Stop_When_Full buffer it does so and turns Enable FALSE, as
160    /// the log's own status records do. Without a clock bound, as before the
161    /// log is added to a database, the time has to come from the caller: a
162    /// `date` and `time` that aren't an actual moment (every field given,
163    /// the weekday matching the date) fail with [`Error::OutOfRange`] and
164    /// change nothing.
165    pub fn record_interruption(&mut self, date: Date, time: Time) -> Result<(), Error> {
166        self.lifecycle().record_interruption((date, time))
167    }
168
169    /// Clear the buffer.
170    pub fn clear(&mut self) {
171        self.log_buffer.clear();
172    }
173
174    /// Set the description string.
175    pub fn set_description(&mut self, desc: impl Into<String>) {
176        self.description = desc.into();
177    }
178
179    /// Set Log_DeviceObjectProperty, the property the log samples, as local
180    /// configuration: the log buffer is left as it is.
181    ///
182    /// A reference may name another device; the poller then logs a failure
183    /// for each sample instead of reading it. `None`, or a reference whose
184    /// object or Device is at the reserved instance 4194303, leaves the log
185    /// without one (#1417). A Device member that isn't a Device identifier
186    /// fails with PROPERTY / VALUE_OUT_OF_RANGE and changes nothing.
187    pub fn set_log_device_object_property(
188        &mut self,
189        reference: Option<BACnetDeviceObjectPropertyReference>,
190    ) -> Result<(), Error> {
191        if let Some(reference) = &reference {
192            crate::device_reference::check_device_member(reference.device_identifier)?;
193        }
194        self.log_device_object_property = reference.and_then(crate::device_reference::set_or_unset);
195        Ok(())
196    }
197
198    /// A client's write of Log_DeviceObjectProperty (#1234): one reference,
199    /// the unset form clearing it (#1417). See [`references::check_written`]
200    /// for the refusals; Null is no reference, so INVALID_DATA_TYPE. A new
201    /// value purges the buffer, leaving a BUFFER_PURGED status record (Clause
202    /// 12.25.8); without a valid clock the purge fails with DEVICE /
203    /// OPERATIONAL_PROBLEM and nothing changes. Writing the value already held
204    /// changes nothing.
205    fn write_log_device_object_property(&mut self, value: PropertyValue) -> Result<(), Error> {
206        let reference = crate::device_reference::decode_reference(&value)?;
207        references::check_written(&reference)?;
208        let reference = crate::device_reference::set_or_unset(reference);
209        if reference != self.log_device_object_property {
210            self.lifecycle().purge()?;
211            self.log_device_object_property = reference;
212        }
213        Ok(())
214    }
215
216    /// Set Logging_Type, as a client's write does: POLLED or TRIGGERED. This
217    /// device has no COV acquisition yet (#1480), so COV, like any other
218    /// value, is PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED (Clause
219    /// 12.25.26) and changes nothing. POLLED with a zero Log_Interval sets
220    /// [`DEFAULT_LOG_INTERVAL`]; TRIGGERED sets Log_Interval to zero.
221    pub fn set_logging_type(&mut self, logging_type: LoggingType) -> Result<(), Error> {
222        self.acquisition.set_logging_type(logging_type)
223    }
224
225    /// Set Log_Interval in hundredths of a second, as a client's write does.
226    /// While Logging_Type is TRIGGERED it is read-only and this is PROPERTY /
227    /// WRITE_ACCESS_DENIED. A POLLED log's nonzero interval set to zero would
228    /// switch it to COV logging (Clause 12.25.9), which is refused with
229    /// PROPERTY / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as COV itself is.
230    pub fn set_log_interval(&mut self, hundredths: u32) -> Result<(), Error> {
231        self.acquisition.set_log_interval(hundredths)
232    }
233
234    /// Set Start_Time, the local date and time from which records are kept,
235    /// as local configuration: nothing is recorded for the change itself,
236    /// but the log notes at once where the window stands, so a client's
237    /// write that then opens or shuts it is recorded. Every field
238    /// unspecified leaves the start open. Any other value has to name an
239    /// actual date and time, or it is PROPERTY / VALUE_OUT_OF_RANGE: the
240    /// weekday may stay unspecified, and unspecified seconds or hundredths
241    /// count as zero.
242    pub fn set_start_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
243        self.lifecycle()
244            .configure_window(PropertyIdentifier::START_TIME, (date, time))
245    }
246
247    /// Set Stop_Time, the local date and time from which records are no
248    /// longer kept, under the same rules as
249    /// [`set_start_time`](Self::set_start_time).
250    pub fn set_stop_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
251        self.lifecycle()
252            .configure_window(PropertyIdentifier::STOP_TIME, (date, time))
253    }
254
255    /// Set Align_Intervals: whether a POLLED log acquires at clock-aligned
256    /// times.
257    pub fn set_align_intervals(&mut self, align: bool) {
258        self.acquisition.set_align_intervals(align);
259    }
260
261    /// Set Interval_Offset, the delay after each aligned boundary, in
262    /// hundredths; it applies modulo Log_Interval.
263    pub fn set_interval_offset(&mut self, hundredths: u32) {
264        self.acquisition.set_interval_offset(hundredths);
265    }
266
267    /// Ask for one acquisition, as a local process writing Trigger TRUE does.
268    /// Only a TRIGGERED log takes it; any other is PROPERTY /
269    /// NOT_CONFIGURED_FOR_TRIGGERED_LOGGING.
270    pub fn trigger(&mut self) -> Result<(), Error> {
271        self.acquisition.trigger()
272    }
273
274    /// Set Notification_Threshold, the number of records that makes a
275    /// BUFFER_READY report; zero, the default, makes none.
276    pub fn set_notification_threshold(&mut self, threshold: u32) {
277        self.reporting.set_notification_threshold(threshold);
278    }
279
280    /// Set Notification_Class: the number of the Notification Class whose
281    /// recipients get the log's reports (0 by default).
282    pub fn set_notification_class(&mut self, class: u32) {
283        self.reporting.set_notification_class(class);
284    }
285
286    /// Set Event_Enable; a report goes out only while its TO_NORMAL flag is
287    /// set, as it is by default.
288    pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
289        self.reporting.set_event_enable(enable);
290    }
291
292    /// Set Notify_Type, sent with each report (EVENT by default).
293    pub fn set_notify_type(&mut self, notify_type: NotifyType) {
294        self.reporting.set_notify_type(notify_type);
295    }
296
297    fn lifecycle(&mut self) -> LogLifecycle<'_, BACnetLogRecord> {
298        LogLifecycle::new(
299            &mut self.log_buffer,
300            &mut self.log_enable,
301            &mut self.stop_when_full,
302            self.clock.as_ref(),
303            &mut self.window,
304            &mut self.reporting,
305        )
306    }
307}
308
309impl BACnetObject for TrendLogObject {
310    fn object_identifier(&self) -> ObjectIdentifier {
311        self.oid
312    }
313
314    fn object_name(&self) -> &str {
315        &self.name
316    }
317
318    fn read_property(
319        &self,
320        property: PropertyIdentifier,
321        array_index: Option<u32>,
322    ) -> Result<PropertyValue, Error> {
323        // Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
324        // Trigger, then Start_Time and Stop_Time.
325        if let Some(value) = self
326            .acquisition
327            .read(property)
328            .or_else(|| self.window.read(property))
329        {
330            return Ok(value);
331        }
332        let total = self.log_buffer.total_record_count();
333        if let Some(result) = self.reporting.read(property, array_index, total) {
334            return result;
335        }
336        match property {
337            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
338                Ok(PropertyValue::ObjectIdentifier(self.oid))
339            }
340            p if p == PropertyIdentifier::OBJECT_NAME => {
341                Ok(PropertyValue::CharacterString(self.name.clone()))
342            }
343            p if p == PropertyIdentifier::DESCRIPTION => {
344                Ok(PropertyValue::CharacterString(self.description.clone()))
345            }
346            p if p == PropertyIdentifier::OBJECT_TYPE => {
347                Ok(PropertyValue::Enumerated(ObjectType::TREND_LOG.to_raw()))
348            }
349            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
350            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
351                Ok(PropertyValue::Boolean(self.stop_when_full))
352            }
353            p if p == PropertyIdentifier::BUFFER_SIZE => {
354                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
355            }
356            p if p == PropertyIdentifier::RECORD_COUNT => {
357                Ok(PropertyValue::Unsigned(self.records().len() as u64))
358            }
359            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
360                Ok(PropertyValue::Unsigned(u64::from(total)))
361            }
362            // Clause 12.25.30 lets only IN_ALARM (from Event_State) and FAULT
363            // (from Reliability) move on a Trend Log; OVERRIDDEN and
364            // OUT_OF_SERVICE are always FALSE. Table 12-29 has no
365            // Out_Of_Service property (#985).
366            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
367                StatusFlags::empty(),
368                self.reliability,
369                false,
370                self.reporting.event_state(),
371            )),
372            p if p == PropertyIdentifier::RELIABILITY => {
373                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
374            }
375            // ReadRange pages it through `log_buffer_internal`.
376            p if p == PropertyIdentifier::LOG_BUFFER => Err(log_buffer_read_denied()),
377            // The Clause 21 encoding; while no reference is set, the empty
378            // element a Trend Log Multiple grows by (#1417).
379            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => {
380                Ok(crate::device_reference::optional_reference_value(
381                    self.log_device_object_property.as_ref(),
382                    ObjectType::ANALOG_INPUT,
383                ))
384            }
385            p if p == PropertyIdentifier::PROPERTY_LIST => {
386                read_property_list_property(&self.property_list(), array_index)
387            }
388            _ => Err(Error::Protocol {
389                class: ErrorClass::PROPERTY.to_raw() as u32,
390                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
391            }),
392        }
393    }
394
395    fn write_property(
396        &mut self,
397        property: PropertyIdentifier,
398        array_index: Option<u32>,
399        value: PropertyValue,
400        _priority: Option<u8>,
401    ) -> Result<(), Error> {
402        if property == PropertyIdentifier::LOG_ENABLE {
403            if let PropertyValue::Boolean(v) = value {
404                return self.lifecycle().write_enable(v);
405            }
406            return Err(Error::Protocol {
407                class: ErrorClass::PROPERTY.to_raw() as u32,
408                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
409            });
410        }
411        if let Some(result) = self.acquisition.write(property, &value) {
412            return result;
413        }
414        if let Some(result) = self.window.write(property, &value) {
415            // A change that opens or closes the window is recorded at once.
416            result?;
417            self.lifecycle().refresh_window();
418            return Ok(());
419        }
420        if property == PropertyIdentifier::STOP_WHEN_FULL {
421            if let PropertyValue::Boolean(v) = value {
422                return self.lifecycle().write_stop_when_full(v);
423            }
424            return Err(Error::Protocol {
425                class: ErrorClass::PROPERTY.to_raw() as u32,
426                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
427            });
428        }
429        if property == PropertyIdentifier::RECORD_COUNT {
430            if let PropertyValue::Unsigned(0) = value {
431                return self.lifecycle().purge();
432            }
433            return Err(Error::Protocol {
434                class: ErrorClass::PROPERTY.to_raw() as u32,
435                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
436            });
437        }
438        // Clause 12.25 Table 12-29 lists Reliability as plain O with no
439        // writability footnote, and unlike the intrinsic-reporting objects the
440        // Trend Log Reliability_Evaluation_Inhibit paragraph requires
441        // NO_FAULT_DETECTED while evaluation is inhibited, without an exception
442        // for a client-supplied Reliability value while Out_Of_Service is TRUE.
443        // Nothing in Clause 12.25 grants a
444        // network client this property: the log owns it (logging status and
445        // fault indication), so every write is refused.
446        if property == PropertyIdentifier::RELIABILITY {
447            return Err(Error::Protocol {
448                class: ErrorClass::PROPERTY.to_raw() as u32,
449                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
450            });
451        }
452        if property == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY {
453            return self.write_log_device_object_property(value);
454        }
455        let total = self.log_buffer.total_record_count();
456        if let Some(result) = self.reporting.write(property, array_index, &value, total) {
457            return result;
458        }
459        if let Some(result) = common::write_description(&mut self.description, property, &value) {
460            return result;
461        }
462        Err(crate::common::unhandled_write_error(
463            self.property_metadata().as_ref(),
464            property,
465            array_index,
466        ))
467    }
468
469    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
470        metadata::for_object(self)
471    }
472
473    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
474        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
475    }
476
477    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
478        self.clock = clock;
479    }
480
481    fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
482        Some(self.log_buffer.identities())
483    }
484
485    fn log_buffer_internal(&self) -> Option<&dyn LogBufferRecords> {
486        Some(&self.log_buffer)
487    }
488
489    fn add_trend_record(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
490        self.add_record(record)
491    }
492
493    fn refresh_log_window_internal(&mut self) -> bool {
494        self.lifecycle().refresh_window()
495    }
496
497    impl_buffer_ready_reporting!(reporting, log_buffer);
498}
499
500#[cfg(test)]
501mod log_record_tests;
502
503#[cfg(test)]
504mod multiple_options_tests;
505
506#[cfg(test)]
507mod options_tests;
508
509#[cfg(test)]
510mod reference_tests;
511
512#[cfg(test)]
513mod tests;