Skip to main content

bacnet_objects/trend/
multiple.rs

1//! TrendLogMultiple (type 27, Clause 12.30).
2
3use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::sync::Arc;
6
7use bacnet_types::bitstring::EventTransitionBits;
8use bacnet_types::constructed::{BACnetDeviceObjectPropertyReference, BACnetLogMultipleRecord};
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 super::acquisition::{Acquisition, Rules};
16use super::multiple_metadata;
17use super::references::{self, MAX_LOG_DEVICE_OBJECT_PROPERTIES};
18use crate::clock::ClockReader;
19use crate::common::{self, read_property_list_property};
20use crate::device_reference;
21use crate::log_buffer::{
22    log_buffer_read_denied, LogBufferRecords, LogRecordBuffer, LogRecordIdentity,
23};
24use crate::log_lifecycle::LogLifecycle;
25use crate::log_reporting::{impl_buffer_ready_reporting, BufferReadyReporting};
26use crate::log_window::LogWindow;
27use crate::traits::BACnetObject;
28
29/// BACnet TrendLogMultiple object (type 27).
30///
31/// Multi-channel trending. Unlike TrendLog, which monitors a single property,
32/// TrendLogMultiple monitors a list of device-object-property references and
33/// logs one value per reference in each record. The database's trend poller
34/// samples every reference every Log_Interval while Logging_Type is POLLED,
35/// at clock-aligned times when Align_Intervals asks for them, and once for
36/// each Trigger while it is TRIGGERED. Records are kept only while Enable is
37/// TRUE and the local time lies between Start_Time and Stop_Time.
38pub struct TrendLogMultipleObject {
39    oid: ObjectIdentifier,
40    name: String,
41    description: String,
42    log_enable: bool,
43    stop_when_full: bool,
44    buffer_size: u32,
45    log_buffer: LogRecordBuffer<BACnetLogMultipleRecord>,
46    log_device_object_property: Vec<BACnetDeviceObjectPropertyReference>,
47    acquisition: Acquisition,
48    window: LogWindow,
49    reporting: BufferReadyReporting,
50    reliability: Reliability,
51    clock: Option<Arc<dyn ClockReader>>,
52}
53
54impl TrendLogMultipleObject {
55    /// Build a log with logging enabled that holds at most `buffer_size`
56    /// records.
57    pub fn new(instance: u32, name: impl Into<String>, buffer_size: u32) -> Result<Self, Error> {
58        let oid = ObjectIdentifier::new(ObjectType::TREND_LOG_MULTIPLE, instance)?;
59        Ok(Self {
60            oid,
61            name: name.into(),
62            description: String::new(),
63            log_enable: true,
64            stop_when_full: false,
65            buffer_size,
66            log_buffer: LogRecordBuffer::new(buffer_size),
67            log_device_object_property: Vec::new(),
68            acquisition: Acquisition::new(Rules::TrendLogMultiple),
69            window: LogWindow::default(),
70            reporting: BufferReadyReporting::default(),
71            reliability: Reliability::NO_FAULT_DETECTED,
72            clock: None,
73        })
74    }
75
76    /// Add a record to the log buffer.
77    ///
78    /// Success does not guarantee a resident ordinary record: disabled logging
79    /// and a record outside the Start_Time / Stop_Time window are ignored,
80    /// zero-capacity logging may only count, and a stop-before-full
81    /// transition records status instead. Missing/invalid status clocks fail
82    /// atomically with DEVICE / OPERATIONAL_PROBLEM. A successful call serves
83    /// a pending Trigger, which reads FALSE again, even when the record is
84    /// ignored (Enable FALSE, or outside the window): the acquisition was
85    /// made.
86    pub fn add_record(&mut self, record: BACnetLogMultipleRecord) -> Result<(), Error> {
87        self.lifecycle().try_add_ordinary(record)?;
88        self.acquisition.acquired();
89        Ok(())
90    }
91
92    /// Append a reference to Log_DeviceObjectProperty as local configuration:
93    /// the log buffer is left as it is.
94    ///
95    /// A reference may name another device; the poller then logs a failure
96    /// in its slot of each record instead of reading it. A Device member that
97    /// isn't a Device identifier fails with PROPERTY / VALUE_OUT_OF_RANGE, and
98    /// a reference past [`MAX_LOG_DEVICE_OBJECT_PROPERTIES`] with RESOURCES /
99    /// NO_SPACE_TO_WRITE_PROPERTY; either leaves the array unchanged.
100    pub fn add_property_reference(
101        &mut self,
102        reference: BACnetDeviceObjectPropertyReference,
103    ) -> Result<(), Error> {
104        device_reference::check_device_member(reference.device_identifier)?;
105        if self.log_device_object_property.len() >= MAX_LOG_DEVICE_OBJECT_PROPERTIES {
106            return Err(references::no_space_error());
107        }
108        self.log_device_object_property.push(reference);
109        Ok(())
110    }
111
112    /// A client's write of Log_DeviceObjectProperty (#1234).
113    ///
114    /// A whole write replaces the array, at any length up to
115    /// [`MAX_LOG_DEVICE_OBJECT_PROPERTIES`]; an indexed write replaces one
116    /// element. Writing index 0 resizes the array (Clause 12.1.5.1): a smaller
117    /// Unsigned drops the trailing elements, a larger one appends empty
118    /// elements, one past the cap is RESOURCES / NO_SPACE_TO_WRITE_PROPERTY and
119    /// another datatype PROPERTY / INVALID_DATA_TYPE. Each written reference
120    /// goes through [`references::check_written`], where an element naming
121    /// instance 4194303 is an empty one. A new value purges the buffer,
122    /// leaving a BUFFER_PURGED status record, which is the first of the two
123    /// actions Clause 12.30.11 offers; without a valid clock the purge fails
124    /// with DEVICE / OPERATIONAL_PROBLEM and nothing changes. Writing the value
125    /// already held changes nothing.
126    fn write_log_device_object_property(
127        &mut self,
128        array_index: Option<u32>,
129        value: PropertyValue,
130    ) -> Result<(), Error> {
131        let mut candidate = self.log_device_object_property.clone();
132        match array_index {
133            None => {
134                let written = device_reference::decode_references(&value)?;
135                if written.len() > MAX_LOG_DEVICE_OBJECT_PROPERTIES {
136                    return Err(references::no_space_error());
137                }
138                for reference in &written {
139                    references::check_written(reference)?;
140                }
141                candidate = written;
142            }
143            Some(0) => {
144                let PropertyValue::Unsigned(size) = value else {
145                    return Err(common::invalid_data_type_error());
146                };
147                let size = usize::try_from(size)
148                    .ok()
149                    .filter(|size| *size <= MAX_LOG_DEVICE_OBJECT_PROPERTIES)
150                    .ok_or_else(references::no_space_error)?;
151                candidate.resize_with(size, references::empty_element);
152            }
153            Some(index) => {
154                let slot = usize::try_from(index - 1)
155                    .ok()
156                    .and_then(|slot| candidate.get_mut(slot))
157                    .ok_or_else(common::invalid_array_index_error)?;
158                let reference = device_reference::decode_reference(&value)?;
159                references::check_written(&reference)?;
160                *slot = reference;
161            }
162        }
163        if candidate != self.log_device_object_property {
164            self.lifecycle().purge()?;
165            self.log_device_object_property = candidate;
166        }
167        Ok(())
168    }
169
170    /// Get the current buffer contents.
171    pub fn records(&self) -> &VecDeque<BACnetLogMultipleRecord> {
172        self.log_buffer.records()
173    }
174
175    /// Total_Record_Count, as
176    /// [`TrendLogObject::total_record_count`](super::TrendLogObject::total_record_count)
177    /// serves it.
178    pub fn total_record_count(&self) -> u32 {
179        self.log_buffer.total_record_count()
180    }
181
182    /// Restore the log buffer, or seed Total_Record_Count with no records,
183    /// before the log is added to an `ObjectDatabase`, under the rules of
184    /// [`TrendLogObject::restore_log_buffer`](super::TrendLogObject::restore_log_buffer)
185    /// (#1537). Records aren't checked against Log_DeviceObjectProperty,
186    /// which may have changed since they were taken. After a restart, follow
187    /// it with [`record_interruption`](Self::record_interruption), which
188    /// counts toward Total_Record_Count.
189    pub fn restore_log_buffer(
190        &mut self,
191        total_record_count: u32,
192        records: impl IntoIterator<Item = BACnetLogMultipleRecord>,
193    ) -> Result<(), Error> {
194        self.lifecycle()
195            .restore(total_record_count, records.into_iter().collect())
196    }
197
198    /// Append a LOG_INTERRUPTED status record, as
199    /// [`TrendLogObject::record_interruption`](super::TrendLogObject::record_interruption)
200    /// does, after restoring the log on a restart (#1537).
201    pub fn record_interruption(&mut self, date: Date, time: Time) -> Result<(), Error> {
202        self.lifecycle().record_interruption((date, time))
203    }
204
205    /// Clear the buffer.
206    pub fn clear(&mut self) {
207        self.log_buffer.clear();
208    }
209
210    /// Set the description string.
211    pub fn set_description(&mut self, desc: impl Into<String>) {
212        self.description = desc.into();
213    }
214
215    /// Set Logging_Type, as a client's write does: POLLED or TRIGGERED. COV
216    /// logging isn't allowed for this object type (Clause 12.30.12), so COV,
217    /// like any other value, is PROPERTY / VALUE_OUT_OF_RANGE and changes
218    /// nothing. POLLED with a zero Log_Interval sets
219    /// [`DEFAULT_LOG_INTERVAL`](super::DEFAULT_LOG_INTERVAL); TRIGGERED sets
220    /// 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. While Logging_Type is
226    /// TRIGGERED it is read-only and this is PROPERTY / WRITE_ACCESS_DENIED.
227    pub fn set_log_interval(&mut self, hundredths: u32) -> Result<(), Error> {
228        self.acquisition.set_log_interval(hundredths)
229    }
230
231    /// Set Start_Time, the local date and time from which records are kept,
232    /// as local configuration: nothing is recorded for the change itself,
233    /// but the log notes at once where the window stands, so a client's
234    /// write that then opens or shuts it is recorded. Every field
235    /// unspecified leaves the start open. Any other value has to name an
236    /// actual date and time, or it is PROPERTY / VALUE_OUT_OF_RANGE: the
237    /// weekday may stay unspecified, and unspecified seconds or hundredths
238    /// count as zero.
239    pub fn set_start_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
240        self.lifecycle()
241            .configure_window(PropertyIdentifier::START_TIME, (date, time))
242    }
243
244    /// Set Stop_Time, the local date and time from which records are no
245    /// longer kept, under the same rules as
246    /// [`set_start_time`](Self::set_start_time).
247    pub fn set_stop_time(&mut self, date: Date, time: Time) -> Result<(), Error> {
248        self.lifecycle()
249            .configure_window(PropertyIdentifier::STOP_TIME, (date, time))
250    }
251
252    /// Set Align_Intervals: whether a POLLED log acquires at clock-aligned
253    /// times.
254    pub fn set_align_intervals(&mut self, align: bool) {
255        self.acquisition.set_align_intervals(align);
256    }
257
258    /// Set Interval_Offset, the delay after each aligned boundary, in
259    /// hundredths; it applies modulo Log_Interval.
260    pub fn set_interval_offset(&mut self, hundredths: u32) {
261        self.acquisition.set_interval_offset(hundredths);
262    }
263
264    /// Ask for one acquisition, as a local process writing Trigger TRUE does.
265    /// Only a TRIGGERED log takes it; any other is PROPERTY /
266    /// NOT_CONFIGURED_FOR_TRIGGERED_LOGGING.
267    pub fn trigger(&mut self) -> Result<(), Error> {
268        self.acquisition.trigger()
269    }
270
271    pub(super) fn logging_type(&self) -> LoggingType {
272        self.acquisition.logging_type()
273    }
274
275    /// Set Notification_Threshold, the number of records that makes a
276    /// BUFFER_READY report; zero, the default, makes none.
277    pub fn set_notification_threshold(&mut self, threshold: u32) {
278        self.reporting.set_notification_threshold(threshold);
279    }
280
281    /// Set Notification_Class: the number of the Notification Class whose
282    /// recipients get the log's reports (0 by default).
283    pub fn set_notification_class(&mut self, class: u32) {
284        self.reporting.set_notification_class(class);
285    }
286
287    /// Set Event_Enable; a report goes out only while its TO_NORMAL flag is
288    /// set, as it is by default.
289    pub fn set_event_enable(&mut self, enable: EventTransitionBits) {
290        self.reporting.set_event_enable(enable);
291    }
292
293    /// Set Notify_Type, sent with each report (EVENT by default).
294    pub fn set_notify_type(&mut self, notify_type: NotifyType) {
295        self.reporting.set_notify_type(notify_type);
296    }
297
298    fn lifecycle(&mut self) -> LogLifecycle<'_, BACnetLogMultipleRecord> {
299        LogLifecycle::new(
300            &mut self.log_buffer,
301            &mut self.log_enable,
302            &mut self.stop_when_full,
303            self.clock.as_ref(),
304            &mut self.window,
305            &mut self.reporting,
306        )
307    }
308}
309
310impl BACnetObject for TrendLogMultipleObject {
311    fn object_identifier(&self) -> ObjectIdentifier {
312        self.oid
313    }
314
315    fn object_name(&self) -> &str {
316        &self.name
317    }
318
319    fn read_property(
320        &self,
321        property: PropertyIdentifier,
322        array_index: Option<u32>,
323    ) -> Result<PropertyValue, Error> {
324        // Logging_Type, Log_Interval, Align_Intervals, Interval_Offset and
325        // Trigger, then Start_Time and Stop_Time.
326        if let Some(value) = self
327            .acquisition
328            .read(property)
329            .or_else(|| self.window.read(property))
330        {
331            return Ok(value);
332        }
333        let total = self.log_buffer.total_record_count();
334        if let Some(result) = self.reporting.read(property, array_index, total) {
335            return result;
336        }
337        match property {
338            p if p == PropertyIdentifier::OBJECT_IDENTIFIER => {
339                Ok(PropertyValue::ObjectIdentifier(self.oid))
340            }
341            p if p == PropertyIdentifier::OBJECT_NAME => {
342                Ok(PropertyValue::CharacterString(self.name.clone()))
343            }
344            p if p == PropertyIdentifier::DESCRIPTION => {
345                Ok(PropertyValue::CharacterString(self.description.clone()))
346            }
347            p if p == PropertyIdentifier::OBJECT_TYPE => Ok(PropertyValue::Enumerated(
348                ObjectType::TREND_LOG_MULTIPLE.to_raw(),
349            )),
350            p if p == PropertyIdentifier::LOG_ENABLE => Ok(PropertyValue::Boolean(self.log_enable)),
351            p if p == PropertyIdentifier::STOP_WHEN_FULL => {
352                Ok(PropertyValue::Boolean(self.stop_when_full))
353            }
354            p if p == PropertyIdentifier::BUFFER_SIZE => {
355                Ok(PropertyValue::Unsigned(self.buffer_size as u64))
356            }
357            p if p == PropertyIdentifier::RECORD_COUNT => {
358                Ok(PropertyValue::Unsigned(self.records().len() as u64))
359            }
360            p if p == PropertyIdentifier::TOTAL_RECORD_COUNT => {
361                Ok(PropertyValue::Unsigned(u64::from(total)))
362            }
363            // Clause 12.30.5 lets only IN_ALARM (from Event_State) and FAULT
364            // (from Reliability) move on a Trend Log Multiple; OVERRIDDEN and
365            // OUT_OF_SERVICE are always FALSE. Table 12-35 has no
366            // Out_Of_Service property (#985).
367            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(common::compute_status_flags(
368                StatusFlags::empty(),
369                self.reliability,
370                false,
371                self.reporting.event_state(),
372            )),
373            p if p == PropertyIdentifier::RELIABILITY => {
374                Ok(PropertyValue::Enumerated(self.reliability.to_raw()))
375            }
376            // ReadRange pages it through `log_buffer_internal`.
377            p if p == PropertyIdentifier::LOG_BUFFER => Err(log_buffer_read_denied()),
378            // A BACnetARRAY: one Clause 21 encoding per element (#1234).
379            p if p == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY => common::read_array(
380                device_reference::reference_elements(&self.log_device_object_property),
381                array_index,
382            ),
383            p if p == PropertyIdentifier::PROPERTY_LIST => {
384                read_property_list_property(&self.property_list(), array_index)
385            }
386            _ => Err(Error::Protocol {
387                class: ErrorClass::PROPERTY.to_raw() as u32,
388                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
389            }),
390        }
391    }
392
393    fn write_property(
394        &mut self,
395        property: PropertyIdentifier,
396        array_index: Option<u32>,
397        value: PropertyValue,
398        _priority: Option<u8>,
399    ) -> Result<(), Error> {
400        if property == PropertyIdentifier::LOG_ENABLE {
401            if let PropertyValue::Boolean(v) = value {
402                return self.lifecycle().write_enable(v);
403            }
404            return Err(Error::Protocol {
405                class: ErrorClass::PROPERTY.to_raw() as u32,
406                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
407            });
408        }
409        if let Some(result) = self.acquisition.write(property, &value) {
410            return result;
411        }
412        if let Some(result) = self.window.write(property, &value) {
413            // A change that opens or closes the window is recorded at once.
414            result?;
415            self.lifecycle().refresh_window();
416            return Ok(());
417        }
418        if property == PropertyIdentifier::STOP_WHEN_FULL {
419            if let PropertyValue::Boolean(v) = value {
420                return self.lifecycle().write_stop_when_full(v);
421            }
422            return Err(Error::Protocol {
423                class: ErrorClass::PROPERTY.to_raw() as u32,
424                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
425            });
426        }
427        if property == PropertyIdentifier::RECORD_COUNT {
428            if let PropertyValue::Unsigned(0) = value {
429                return self.lifecycle().purge();
430            }
431            return Err(Error::Protocol {
432                class: ErrorClass::PROPERTY.to_raw() as u32,
433                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
434            });
435        }
436        if property == PropertyIdentifier::LOG_DEVICE_OBJECT_PROPERTY {
437            return self.write_log_device_object_property(array_index, value);
438        }
439        let total = self.log_buffer.total_record_count();
440        if let Some(result) = self.reporting.write(property, array_index, &value, total) {
441            return result;
442        }
443        if let Some(result) = common::write_description(&mut self.description, property, &value) {
444            return result;
445        }
446        Err(crate::common::unhandled_write_error(
447            self.property_metadata().as_ref(),
448            property,
449            array_index,
450        ))
451    }
452
453    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
454        multiple_metadata::for_object(self)
455    }
456
457    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
458        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
459    }
460
461    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
462        self.clock = clock;
463    }
464
465    fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
466        Some(self.log_buffer.identities())
467    }
468
469    fn log_buffer_internal(&self) -> Option<&dyn LogBufferRecords> {
470        Some(&self.log_buffer)
471    }
472
473    fn add_trend_multiple_record(&mut self, record: BACnetLogMultipleRecord) -> Result<(), Error> {
474        self.add_record(record)
475    }
476
477    fn refresh_log_window_internal(&mut self) -> bool {
478        self.lifecycle().refresh_window()
479    }
480
481    impl_buffer_ready_reporting!(reporting, log_buffer);
482}