Skip to main content

bacnet_objects/
log_reporting.rs

1//! BUFFER_READY intrinsic reporting for the log objects (Clause 13.3.7).
2//!
3//! A Trend Log, Event Log or Trend Log Multiple that reports intrinsically
4//! tells the recipients of its Notification Class each time
5//! Notification_Threshold more records have been collected since its last
6//! report, so they can fetch the new ones with ReadRange. The three tables
7//! (12-29, 12-31 and 12-35) give each log the same event rows, and the
8//! crate's `BufferReadyReporting` keeps them for any of the three: it runs
9//! the algorithm on the log's Total_Record_Count, its pMonitoredValue, and
10//! serves and takes the rows itself. [`BufferReadyReport`] carries one
11//! report's counts to the server.
12//!
13//! Event_State never leaves NORMAL. Each report is a NORMAL to NORMAL
14//! transition, so it stamps the TO_NORMAL slots of Event_Time_Stamps,
15//! Event_Message_Texts and Acked_Transitions, and Event_Enable's TO_NORMAL
16//! flag decides whether it goes out.
17//!
18//! An object adopts it in three steps: hold one and lend it to its
19//! `LogLifecycle`, which restarts Records_Since_Notification at a purge;
20//! offer each read and write to its `read` and `write`; and invoke
21//! `impl_buffer_ready_reporting!` with the field and the log buffer.
22
23use bacnet_types::bitstring::EventTransitionBits;
24use bacnet_types::constructed::BACnetObjectPropertyReference;
25use bacnet_types::enums::{
26    ErrorClass, ErrorCode, EventState, EventType, NotifyType, PropertyIdentifier as P,
27};
28use bacnet_types::error::Error;
29use bacnet_types::primitives::{BACnetTimeStamp, PropertyValue};
30
31use crate::common;
32use crate::event::history::{EventHistory, EventTransitionState};
33use crate::event::options::REPORTING_OPTION_METADATA as OPTIONS;
34use crate::event::{
35    EnrollmentSummaryCapability, EventStateChange, EventTransitionCommit,
36    EventTransitionCommitError, TransitionOutcome,
37};
38use crate::property_metadata::{
39    PropertyConformance::Optional,
40    PropertyMetadata,
41    PropertyPresenceCondition::{IntrinsicReportingOptional, IntrinsicReportingRequired},
42    PropertyWriteCapability::{Always, ReadOnly},
43};
44
45/// The counts one BUFFER_READY report carries (Clause 13.3.7), as the
46/// server projects them into the notification's event values.
47#[doc(hidden)]
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct BufferReadyReport {
50    /// Previous_Notification: Last_Notify_Record before this report, the
51    /// Total_Record_Count at the report before or when detection started.
52    pub previous_notification: u32,
53    /// Current_Notification: the Total_Record_Count this report was made at.
54    pub current_notification: u32,
55}
56
57/// A row footnote 3 of Table 12-31 (4 of Tables 12-29 and 12-35) requires
58/// of a log that reports intrinsically.
59const fn required(
60    property: P,
61    write: crate::property_metadata::PropertyWriteCapability,
62) -> PropertyMetadata {
63    PropertyMetadata::new(property, Optional, Some(IntrinsicReportingRequired), write)
64}
65
66/// A row footnote 5 of Table 12-31 (7 of 12-29, 6 of 12-35) only lets such
67/// a log have.
68const fn permitted(
69    property: P,
70    write: crate::property_metadata::PropertyWriteCapability,
71) -> PropertyMetadata {
72    PropertyMetadata::new(property, Optional, Some(IntrinsicReportingOptional), write)
73}
74
75/// The event rows a log that reports intrinsically adds, in the order the
76/// three log tables list them: the ones the tables require of a log with
77/// intrinsic reporting, and Event_Message_Texts, Event_Message_Texts_Config
78/// and the Event_Algorithm_Inhibit pair, which they only permit (#1329).
79pub(crate) const BUFFER_READY_METADATA: [PropertyMetadata; 13] = [
80    required(P::NOTIFICATION_THRESHOLD, Always),
81    required(P::RECORDS_SINCE_NOTIFICATION, ReadOnly),
82    required(P::LAST_NOTIFY_RECORD, ReadOnly),
83    required(P::NOTIFICATION_CLASS, Always),
84    required(P::EVENT_ENABLE, Always),
85    required(P::ACKED_TRANSITIONS, ReadOnly),
86    required(P::NOTIFY_TYPE, Always),
87    required(P::EVENT_TIME_STAMPS, ReadOnly),
88    permitted(P::EVENT_MESSAGE_TEXTS, ReadOnly),
89    OPTIONS[0],
90    required(P::EVENT_DETECTION_ENABLE, Always),
91    OPTIONS[1],
92    OPTIONS[2],
93];
94
95/// The records collected from Total_Record_Count `from` to `to`. The count
96/// wraps from 2^32 - 1 to 1, never passing zero, so a smaller `to` has gone
97/// round once: the second condition of the algorithm (Clause 13.3.7).
98pub(crate) fn records_between(from: u32, to: u32) -> u32 {
99    if to >= from {
100        to - from
101    } else {
102        // At most 2^32 - 2: `to` is at least 1 once the count has wrapped.
103        (u64::from(to) + u64::from(u32::MAX) - u64::from(from)) as u32
104    }
105}
106
107/// The event state, configuration and history of one log reporting
108/// BUFFER_READY.
109#[derive(Debug, Clone)]
110pub(crate) struct BufferReadyReporting {
111    /// Notification_Threshold, pThreshold: the records that make a report.
112    /// Zero, the default, makes none.
113    notification_threshold: u32,
114    /// Last_Notify_Record, pPreviousCount: Total_Record_Count at the last
115    /// report, or when detection started.
116    last_notify_record: u32,
117    /// Total_Record_Count when Records_Since_Notification last restarted:
118    /// at the last report, purge or start of detection.
119    count_restarted_at: u32,
120    notification_class: u32,
121    event_enable: EventTransitionBits,
122    notify_type: NotifyType,
123    /// Always NORMAL: the algorithm knows no other state.
124    event_state: EventState,
125    acked_transitions: EventTransitionBits,
126    history: EventHistory,
127    /// Event_Detection_Enable: FALSE suspends the algorithm (Clause 13.2.2.1).
128    event_detection_enable: bool,
129    /// The counts of the last committed report, for its event values.
130    report: Option<BufferReadyReport>,
131}
132
133impl Default for BufferReadyReporting {
134    /// Detection on with no threshold, so nothing is reported until one is
135    /// set; Notification_Class 0, every transition enabled and acknowledged,
136    /// and reports sent as events rather than alarms.
137    fn default() -> Self {
138        Self {
139            notification_threshold: 0,
140            last_notify_record: 0,
141            count_restarted_at: 0,
142            notification_class: 0,
143            event_enable: EventTransitionBits::all(),
144            notify_type: NotifyType::EVENT,
145            event_state: EventState::NORMAL,
146            acked_transitions: EventTransitionBits::all(),
147            history: EventHistory::default(),
148            event_detection_enable: true,
149            report: None,
150        }
151    }
152}
153
154impl BufferReadyReporting {
155    /// Event_State as served: always NORMAL.
156    pub(crate) fn event_state(&self) -> EventState {
157        self.event_state
158    }
159
160    pub(crate) fn set_notification_threshold(&mut self, threshold: u32) {
161        self.notification_threshold = threshold;
162    }
163
164    pub(crate) fn set_notification_class(&mut self, class: u32) {
165        self.notification_class = class;
166    }
167
168    pub(crate) fn set_event_enable(&mut self, enable: EventTransitionBits) {
169        self.event_enable = enable;
170    }
171
172    pub(crate) fn set_notify_type(&mut self, notify_type: NotifyType) {
173        self.notify_type = notify_type;
174    }
175
176    /// Restart Records_Since_Notification at Total_Record_Count `total`, as a
177    /// purge does before it records BUFFER_PURGED (Clause 12.27.14). The
178    /// algorithm's own count, from Last_Notify_Record, goes on.
179    pub(crate) fn restart_count(&mut self, total: u32) {
180        self.count_restarted_at = total;
181    }
182
183    /// Count afresh from Total_Record_Count `total`, as a log restored or
184    /// seeded at that count does (#1537): Last_Notify_Record moves to it and
185    /// Records_Since_Notification reads zero, as when detection starts, so
186    /// the jump to `total` is never taken for records collected.
187    pub(crate) fn restart_from(&mut self, total: u32) {
188        self.last_notify_record = total;
189        self.count_restarted_at = total;
190    }
191
192    /// Serve one of the event rows for a log whose Total_Record_Count is
193    /// `total`, or `None` for any other property.
194    pub(crate) fn read(
195        &self,
196        property: P,
197        array_index: Option<u32>,
198        total: u32,
199    ) -> Option<Result<PropertyValue, Error>> {
200        let unsigned = |value: u32| Some(Ok(PropertyValue::Unsigned(u64::from(value))));
201        let transitions = |bits: EventTransitionBits| {
202            Some(Ok(PropertyValue::BitString {
203                unused_bits: 5,
204                data: vec![bits.to_bacnet()],
205            }))
206        };
207        match property {
208            P::EVENT_STATE => Some(Ok(PropertyValue::Enumerated(self.event_state.to_raw()))),
209            P::EVENT_DETECTION_ENABLE => {
210                Some(Ok(PropertyValue::Boolean(self.event_detection_enable)))
211            }
212            P::NOTIFICATION_THRESHOLD => unsigned(self.notification_threshold),
213            P::RECORDS_SINCE_NOTIFICATION => {
214                unsigned(records_between(self.count_restarted_at, total))
215            }
216            P::LAST_NOTIFY_RECORD => unsigned(self.last_notify_record),
217            P::NOTIFICATION_CLASS => unsigned(self.notification_class),
218            P::EVENT_ENABLE => transitions(self.event_enable),
219            P::ACKED_TRANSITIONS => transitions(self.acked_transitions),
220            P::NOTIFY_TYPE => Some(Ok(PropertyValue::Enumerated(self.notify_type.to_raw()))),
221            _ => self.history.read(property, array_index),
222        }
223    }
224
225    /// Take a network write of one of the writable event rows for a log
226    /// whose Total_Record_Count is `total`, or `None` for any other property.
227    /// Acked_Transitions is refused with WRITE_ACCESS_DENIED, as on every
228    /// intrinsic-reporting object; the other read-only rows are left to the
229    /// object's metadata. Event_Message_Texts_Config and the
230    /// Event_Algorithm_Inhibit pair go to the history's
231    /// [`ReportingOptions`](crate::event::options::ReportingOptions).
232    ///
233    /// Event_Detection_Enable FALSE puts Event_State, Acked_Transitions,
234    /// Event_Time_Stamps and Event_Message_Texts back to their starting
235    /// values (Clause 12.27.26). TRUE again starts the algorithm afresh,
236    /// counting from the current Total_Record_Count.
237    pub(crate) fn write(
238        &mut self,
239        property: P,
240        array_index: Option<u32>,
241        value: &PropertyValue,
242        total: u32,
243    ) -> Option<Result<(), Error>> {
244        if let Some(result) =
245            self.history
246                .write(property, array_index, value, self.event_detection_enable)
247        {
248            return Some(result);
249        }
250        let result = match property {
251            P::EVENT_DETECTION_ENABLE => boolean(value).map(|enable| {
252                if !enable {
253                    self.event_state = EventState::NORMAL;
254                    self.acked_transitions = EventTransitionBits::all();
255                    self.history.reset();
256                } else if !self.event_detection_enable {
257                    self.last_notify_record = total;
258                    self.count_restarted_at = total;
259                }
260                self.event_detection_enable = enable;
261            }),
262            P::NOTIFICATION_THRESHOLD => {
263                unsigned32(value).map(|threshold| self.notification_threshold = threshold)
264            }
265            P::NOTIFICATION_CLASS => unsigned32(value).map(|class| self.notification_class = class),
266            P::EVENT_ENABLE => match value {
267                PropertyValue::BitString { unused_bits, data } => {
268                    common::check_fixed_width_bit_string(*unused_bits, data, 3).map(|byte| {
269                        self.event_enable = EventTransitionBits::from_bacnet(&[byte]);
270                    })
271                }
272                _ => Err(common::invalid_data_type_error()),
273            },
274            P::NOTIFY_TYPE => match *value {
275                PropertyValue::Enumerated(raw) => {
276                    let notify_type = NotifyType::from_raw(raw);
277                    if NotifyType::ALL_NAMED.iter().any(|&(_, n)| n == notify_type) {
278                        self.notify_type = notify_type;
279                        Ok(())
280                    } else {
281                        Err(common::value_out_of_range_error())
282                    }
283                }
284                _ => Err(common::invalid_data_type_error()),
285            },
286            P::ACKED_TRANSITIONS => Err(common::write_access_denied_error()),
287            _ => return None,
288        };
289        Some(result)
290    }
291
292    /// The algorithm (Clause 13.3.7) for a log whose Total_Record_Count is
293    /// `total`: a NORMAL to NORMAL transition once Notification_Threshold or
294    /// more records have been collected since Last_Notify_Record, counting
295    /// across the wrap. No threshold, detection off, or a TRUE
296    /// Event_Algorithm_Inhibit, which stops a NORMAL state's re-entry
297    /// (Clause 13.2.2.1), proposes nothing; the records keep counting, so a
298    /// report falls due once the inhibit clears. Proposing changes nothing;
299    /// the commit does.
300    pub(crate) fn propose(&self, total: u32) -> Option<TransitionOutcome> {
301        let due = self.event_detection_enable
302            && !self.history.options.inhibited()
303            && self.notification_threshold > 0
304            && records_between(self.last_notify_record, total) >= self.notification_threshold;
305        due.then(|| TransitionOutcome {
306            change: EventStateChange {
307                from: EventState::NORMAL,
308                to: EventState::NORMAL,
309            },
310            event_type: EventType::BUFFER_READY,
311            distribute: self.event_enable.contains(EventTransitionBits::TO_NORMAL),
312        })
313    }
314
315    /// Commit one report made at Total_Record_Count `total` through the
316    /// shared kernel, then move Last_Notify_Record to `total` and restart
317    /// Records_Since_Notification. Any target other than NORMAL is refused
318    /// as unsupported: the algorithm has no other state.
319    pub(crate) fn commit(
320        &mut self,
321        commit: EventTransitionCommit,
322        total: u32,
323    ) -> Result<(), EventTransitionCommitError> {
324        if commit.change.to != EventState::NORMAL {
325            return Err(EventTransitionCommitError::Unsupported);
326        }
327        EventTransitionState::new(
328            &mut self.event_state,
329            &mut self.acked_transitions,
330            &mut self.history,
331        )
332        .commit(commit)?;
333        self.report = Some(BufferReadyReport {
334            previous_notification: self.last_notify_record,
335            current_notification: total,
336        });
337        self.last_notify_record = total;
338        self.count_restarted_at = total;
339        Ok(())
340    }
341
342    /// The counts of the last committed report.
343    pub(crate) fn report(&self) -> Option<BufferReadyReport> {
344        self.report
345    }
346
347    /// Acknowledge the latest report stamped `timestamp`; with detection off
348    /// there is nothing to acknowledge (NO_ALARM_CONFIGURED).
349    pub(crate) fn acknowledge(
350        &mut self,
351        event_state: EventState,
352        timestamp: &BACnetTimeStamp,
353    ) -> Result<Option<EventStateChange>, Error> {
354        if !self.event_detection_enable {
355            return Err(common::protocol_error(
356                ErrorClass::OBJECT,
357                ErrorCode::NO_ALARM_CONFIGURED,
358            ));
359        }
360        self.history.acknowledge_correlated_detailed(
361            &mut self.acked_transitions,
362            event_state,
363            timestamp,
364        )
365    }
366
367    /// The property Event_Algorithm_Inhibit follows, if any.
368    pub(crate) fn inhibit_reference(&self) -> Option<BACnetObjectPropertyReference> {
369        self.history.options.inhibit_reference().cloned()
370    }
371
372    /// Take the referenced value as Event_Algorithm_Inhibit; returns whether
373    /// it changed.
374    pub(crate) fn follow_inhibit(&mut self, inhibit: bool) -> bool {
375        self.history.options.follow(inhibit)
376    }
377
378    /// What GetEnrollmentSummary reports for the log.
379    pub(crate) fn enrollment_summary(&self) -> EnrollmentSummaryCapability {
380        EnrollmentSummaryCapability {
381            event_type: EventType::BUFFER_READY,
382            last_transition: self.history.last_transition(),
383        }
384    }
385}
386
387fn boolean(value: &PropertyValue) -> Result<bool, Error> {
388    match *value {
389        PropertyValue::Boolean(value) => Ok(value),
390        _ => Err(common::invalid_data_type_error()),
391    }
392}
393
394fn unsigned32(value: &PropertyValue) -> Result<u32, Error> {
395    match *value {
396        PropertyValue::Unsigned(value) => common::u64_to_u32(value),
397        _ => Err(common::invalid_data_type_error()),
398    }
399}
400
401/// Implement the `BACnetObject` intrinsic-reporting hooks of a log that
402/// holds a [`BufferReadyReporting`] in `$reporting` and its records in the
403/// [`LogRecordBuffer`](crate::log_buffer::LogRecordBuffer) `$buffer`.
404macro_rules! impl_buffer_ready_reporting {
405    ($reporting:ident, $buffer:ident) => {
406        fn enrollment_summary_capability_internal(
407            &self,
408        ) -> Option<$crate::event::EnrollmentSummaryCapability> {
409            Some(self.$reporting.enrollment_summary())
410        }
411
412        fn evaluate_intrinsic_reporting(&mut self) -> Option<$crate::event::TransitionOutcome> {
413            self.$reporting.propose(self.$buffer.total_record_count())
414        }
415
416        fn tick_intrinsic_reporting(&mut self) -> Option<$crate::event::TransitionOutcome> {
417            self.$reporting.propose(self.$buffer.total_record_count())
418        }
419
420        fn commit_event_transition_internal(
421            &mut self,
422            commit: $crate::event::EventTransitionCommit,
423        ) -> Result<(), $crate::event::EventTransitionCommitError> {
424            let total = self.$buffer.total_record_count();
425            self.$reporting.commit(commit, total)
426        }
427
428        fn acknowledge_alarm_correlated_internal(
429            &mut self,
430            event_state: bacnet_types::enums::EventState,
431            timestamp: &bacnet_types::primitives::BACnetTimeStamp,
432        ) -> Result<(), bacnet_types::error::Error> {
433            self.$reporting
434                .acknowledge(event_state, timestamp)
435                .map(|_| ())
436        }
437
438        fn acknowledge_alarm_correlated_detailed_internal(
439            &mut self,
440            event_state: bacnet_types::enums::EventState,
441            timestamp: &bacnet_types::primitives::BACnetTimeStamp,
442        ) -> Result<Option<$crate::event::EventStateChange>, bacnet_types::error::Error> {
443            self.$reporting.acknowledge(event_state, timestamp)
444        }
445
446        fn buffer_ready_report_internal(&self) -> Option<$crate::log_reporting::BufferReadyReport> {
447            self.$reporting.report()
448        }
449
450        fn event_algorithm_inhibit_reference_internal(
451            &self,
452        ) -> Option<bacnet_types::constructed::BACnetObjectPropertyReference> {
453            self.$reporting.inhibit_reference()
454        }
455
456        fn follow_event_algorithm_inhibit_internal(&mut self, inhibit: bool) -> bool {
457            self.$reporting.follow_inhibit(inhibit)
458        }
459    };
460}
461
462pub(crate) use impl_buffer_ready_reporting;
463
464#[cfg(test)]
465#[path = "log_reporting_tests.rs"]
466mod tests;