Skip to main content

bacnet_objects/
log_buffer.rs

1//! Shared resident storage and identity projection for BACnet log objects.
2
3use std::collections::VecDeque;
4
5use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
6use bacnet_types::primitives::{Date, PropertyValue, Time};
7
8/// Stable object-owned identity for one resident log record.
9///
10/// Identity views returned by [`crate::traits::BACnetObject::log_record_identities_internal`]
11/// are ordered oldest-to-newest and align element-for-element with the owning
12/// object's resident records and `LOG_BUFFER` projection. Sequence numbers are
13/// always nonzero; after `u32::MAX`, the next accepted record uses 1.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct LogRecordIdentity {
16    sequence_number: u32,
17    date: Date,
18    time: Time,
19}
20
21impl LogRecordIdentity {
22    /// Construct an identity, rejecting zero as an invalid record sequence.
23    pub fn new(sequence_number: u32, date: Date, time: Time) -> Option<Self> {
24        (sequence_number != 0).then_some(Self {
25            sequence_number,
26            date,
27            time,
28        })
29    }
30
31    /// Return this record's nonzero Unsigned32 sequence number.
32    pub fn sequence_number(&self) -> u32 {
33        self.sequence_number
34    }
35
36    /// Return the date owned by this record.
37    pub fn date(&self) -> Date {
38        self.date
39    }
40
41    /// Return the time owned by this record.
42    pub fn time(&self) -> Time {
43        self.time
44    }
45}
46
47#[derive(Debug, Clone, Copy)]
48pub(crate) enum LogRecordProfile {
49    Event,
50    Trend,
51    TrendMultiple,
52}
53
54#[derive(Clone)]
55pub(crate) struct LogRecordBuffer {
56    capacity: u32,
57    records: VecDeque<BACnetLogRecord>,
58    total_record_count: u32,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(crate) enum OrdinaryAdmission {
63    IgnoredDisabled,
64    Inserted,
65    CountOnly,
66    StopBeforeFull,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub(crate) enum ForcedAdmission {
71    Inserted,
72    CountOnly,
73}
74
75impl LogRecordBuffer {
76    pub(crate) fn new(capacity: u32) -> Self {
77        Self {
78            capacity,
79            records: VecDeque::new(),
80            total_record_count: 0,
81        }
82    }
83
84    pub(crate) fn admit_ordinary(
85        &mut self,
86        record: BACnetLogRecord,
87        enabled: bool,
88        stop_when_full: bool,
89    ) -> OrdinaryAdmission {
90        if !enabled {
91            return OrdinaryAdmission::IgnoredDisabled;
92        }
93        if stop_when_full && self.next_record_would_fill() {
94            return OrdinaryAdmission::StopBeforeFull;
95        }
96
97        match self.insert_counted(record) {
98            ForcedAdmission::Inserted => OrdinaryAdmission::Inserted,
99            ForcedAdmission::CountOnly => OrdinaryAdmission::CountOnly,
100        }
101    }
102
103    pub(crate) fn insert_forced(&mut self, record: BACnetLogRecord) -> ForcedAdmission {
104        self.insert_counted(record)
105    }
106
107    pub(crate) fn is_full(&self) -> bool {
108        self.records.len() >= self.capacity as usize
109    }
110
111    pub(crate) fn next_record_would_fill_positive_capacity(&self) -> bool {
112        self.capacity > 0 && self.next_record_would_fill()
113    }
114
115    pub(crate) fn records(&self) -> &VecDeque<BACnetLogRecord> {
116        &self.records
117    }
118
119    pub(crate) fn total_record_count(&self) -> u32 {
120        self.total_record_count
121    }
122
123    pub(crate) fn capacity(&self) -> u32 {
124        self.capacity
125    }
126
127    pub(crate) fn clear(&mut self) {
128        self.records.clear();
129    }
130
131    pub(crate) fn identities(&self) -> Vec<LogRecordIdentity> {
132        if self.records.is_empty() {
133            return Vec::new();
134        }
135
136        debug_assert_ne!(self.total_record_count, 0);
137        let mut sequence_number = self.total_record_count;
138        for _ in 1..self.records.len() {
139            sequence_number = previous_sequence(sequence_number);
140        }
141
142        self.records
143            .iter()
144            .map(|record| {
145                let identity = LogRecordIdentity {
146                    sequence_number,
147                    date: record.date,
148                    time: record.time,
149                };
150                sequence_number = next_sequence(sequence_number);
151                identity
152            })
153            .collect()
154    }
155
156    pub(crate) fn project(&self, profile: LogRecordProfile) -> PropertyValue {
157        PropertyValue::List(
158            self.records
159                .iter()
160                .map(|record| project_record(record, profile))
161                .collect(),
162        )
163    }
164
165    fn next_record_would_fill(&self) -> bool {
166        self.capacity == 0 || self.records.len().saturating_add(1) >= self.capacity as usize
167    }
168
169    fn insert_counted(&mut self, record: BACnetLogRecord) -> ForcedAdmission {
170        self.total_record_count = next_sequence(self.total_record_count);
171        if self.capacity == 0 {
172            return ForcedAdmission::CountOnly;
173        }
174        if self.is_full() {
175            self.records.pop_front();
176        }
177        self.records.push_back(record);
178        ForcedAdmission::Inserted
179    }
180
181    #[cfg(test)]
182    pub(crate) fn set_total_record_count_for_test(&mut self, total_record_count: u32) {
183        debug_assert!(self.records.is_empty());
184        self.total_record_count = total_record_count;
185    }
186}
187
188fn next_sequence(sequence_number: u32) -> u32 {
189    if sequence_number == u32::MAX {
190        1
191    } else {
192        sequence_number + 1
193    }
194}
195
196fn previous_sequence(sequence_number: u32) -> u32 {
197    if sequence_number == 1 {
198        u32::MAX
199    } else {
200        sequence_number - 1
201    }
202}
203
204fn project_record(record: &BACnetLogRecord, profile: LogRecordProfile) -> PropertyValue {
205    let mut fields = vec![
206        PropertyValue::Date(record.date),
207        PropertyValue::Time(record.time),
208        project_datum(&record.log_datum),
209    ];
210    if let (LogRecordProfile::Trend, Some(status_flags)) = (profile, record.status_flags) {
211        fields.push(PropertyValue::BitString {
212            unused_bits: 4,
213            data: vec![status_flags << 4],
214        });
215    }
216    PropertyValue::List(fields)
217}
218
219fn project_datum(datum: &LogDatum) -> PropertyValue {
220    match datum {
221        LogDatum::LogStatus(value) => PropertyValue::BitString {
222            unused_bits: 5,
223            data: vec![(value & 0b111) << 5],
224        },
225        LogDatum::BooleanValue(value) => PropertyValue::Boolean(*value),
226        LogDatum::RealValue(value) => PropertyValue::Real(*value),
227        LogDatum::EnumValue(value) => PropertyValue::Enumerated(*value),
228        LogDatum::UnsignedValue(value) => PropertyValue::Unsigned(*value),
229        LogDatum::SignedValue(value) => PropertyValue::Signed(*value as i32),
230        LogDatum::BitstringValue { unused_bits, data } => PropertyValue::BitString {
231            unused_bits: *unused_bits,
232            data: data.clone(),
233        },
234        LogDatum::NullValue => PropertyValue::Null,
235        LogDatum::Failure {
236            error_class,
237            error_code,
238        } => PropertyValue::List(vec![
239            PropertyValue::Unsigned(*error_class as u64),
240            PropertyValue::Unsigned(*error_code as u64),
241        ]),
242        LogDatum::TimeChange(value) => PropertyValue::Real(*value),
243        LogDatum::AnyValue(bytes) => PropertyValue::OctetString(bytes.clone()),
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
251    use bacnet_types::primitives::{Date, Time};
252
253    fn record(hour: u8) -> BACnetLogRecord {
254        BACnetLogRecord {
255            date: Date {
256                year: 126,
257                month: 8,
258                day: 31,
259                day_of_week: 1,
260            },
261            time: Time {
262                hour,
263                minute: 0,
264                second: 0,
265                hundredths: 0,
266            },
267            log_datum: LogDatum::UnsignedValue(hour as u64),
268            status_flags: None,
269        }
270    }
271
272    #[test]
273    fn log_buffer_assigns_one_and_wraps_max_without_zero() {
274        let mut buffer = LogRecordBuffer::new(2);
275        assert_eq!(buffer.total_record_count(), 0);
276        assert!(LogRecordIdentity::new(0, record(0).date, record(0).time).is_none());
277
278        assert_eq!(
279            buffer.admit_ordinary(record(1), true, false),
280            OrdinaryAdmission::Inserted
281        );
282        assert_eq!(buffer.identities()[0].sequence_number(), 1);
283
284        buffer.clear();
285        buffer.set_total_record_count_for_test(u32::MAX);
286        assert_eq!(
287            buffer.admit_ordinary(record(2), true, false),
288            OrdinaryAdmission::Inserted
289        );
290        assert_eq!(buffer.total_record_count(), 1);
291        assert_eq!(buffer.identities()[0].sequence_number(), 1);
292    }
293
294    #[test]
295    fn log_buffer_rejections_do_not_consume_sequence() {
296        let mut buffer = LogRecordBuffer::new(1);
297        assert_eq!(
298            buffer.admit_ordinary(record(1), false, false),
299            OrdinaryAdmission::IgnoredDisabled
300        );
301        assert_eq!(buffer.total_record_count(), 0);
302
303        assert_eq!(
304            buffer.admit_ordinary(record(2), true, true),
305            OrdinaryAdmission::StopBeforeFull
306        );
307        assert_eq!(buffer.total_record_count(), 0);
308        assert!(buffer.identities().is_empty());
309    }
310
311    #[test]
312    fn log_buffer_zero_capacity_counts_without_retaining() {
313        let mut ring = LogRecordBuffer::new(0);
314        assert_eq!(
315            ring.admit_ordinary(record(1), true, false),
316            OrdinaryAdmission::CountOnly
317        );
318        assert_eq!(
319            ring.admit_ordinary(record(2), true, false),
320            OrdinaryAdmission::CountOnly
321        );
322        assert!(ring.records().is_empty());
323        assert_eq!(ring.total_record_count(), 2);
324
325        let mut stop_when_full = LogRecordBuffer::new(0);
326        assert_eq!(
327            stop_when_full.admit_ordinary(record(1), true, true),
328            OrdinaryAdmission::StopBeforeFull
329        );
330        assert!(stop_when_full.records().is_empty());
331        assert_eq!(stop_when_full.total_record_count(), 0);
332    }
333
334    #[test]
335    fn log_buffer_fifo_eviction_keeps_survivor_identity_and_alignment() {
336        let mut buffer = LogRecordBuffer::new(2);
337        assert_eq!(
338            buffer.admit_ordinary(record(1), true, false),
339            OrdinaryAdmission::Inserted
340        );
341        assert_eq!(
342            buffer.admit_ordinary(record(2), true, false),
343            OrdinaryAdmission::Inserted
344        );
345        let survivor = buffer.identities()[1];
346        assert_eq!(
347            buffer.admit_ordinary(record(3), true, false),
348            OrdinaryAdmission::Inserted
349        );
350
351        let identities = buffer.identities();
352        assert_eq!(
353            identities,
354            vec![
355                survivor,
356                LogRecordIdentity::new(3, record(3).date, record(3).time).unwrap()
357            ]
358        );
359        assert_eq!(identities[0].sequence_number(), 2);
360        assert_ne!(identities[0].sequence_number(), 1);
361        for (record, identity) in buffer.records().iter().zip(&identities) {
362            assert_eq!(identity.date(), record.date);
363            assert_eq!(identity.time(), record.time);
364        }
365    }
366
367    #[test]
368    fn log_buffer_wrap_and_eviction_keep_modular_fifo_alignment() {
369        let mut buffer = LogRecordBuffer::new(3);
370        buffer.set_total_record_count_for_test(u32::MAX - 1);
371        assert_eq!(
372            buffer.admit_ordinary(record(1), true, false),
373            OrdinaryAdmission::Inserted
374        );
375        assert_eq!(
376            buffer.admit_ordinary(record(2), true, false),
377            OrdinaryAdmission::Inserted
378        );
379        assert_eq!(
380            buffer.admit_ordinary(record(3), true, false),
381            OrdinaryAdmission::Inserted
382        );
383        assert_eq!(
384            buffer
385                .identities()
386                .iter()
387                .map(LogRecordIdentity::sequence_number)
388                .collect::<Vec<_>>(),
389            vec![u32::MAX, 1, 2]
390        );
391
392        assert_eq!(
393            buffer.admit_ordinary(record(4), true, false),
394            OrdinaryAdmission::Inserted
395        );
396        assert_eq!(
397            buffer
398                .identities()
399                .iter()
400                .map(LogRecordIdentity::sequence_number)
401                .collect::<Vec<_>>(),
402            vec![1, 2, 3]
403        );
404        assert_eq!(
405            buffer
406                .records()
407                .iter()
408                .map(|record| record.time.hour)
409                .collect::<Vec<_>>(),
410            vec![2, 3, 4]
411        );
412    }
413
414    #[test]
415    fn log_buffer_clear_preserves_counter_and_constructor_resets_it() {
416        let mut buffer = LogRecordBuffer::new(2);
417        assert_eq!(
418            buffer.admit_ordinary(record(1), true, false),
419            OrdinaryAdmission::Inserted
420        );
421        assert_eq!(
422            buffer.admit_ordinary(record(2), true, false),
423            OrdinaryAdmission::Inserted
424        );
425        buffer.clear();
426
427        assert!(buffer.records().is_empty());
428        assert!(buffer.identities().is_empty());
429        assert_eq!(buffer.total_record_count(), 2);
430        assert_eq!(
431            buffer.admit_ordinary(record(3), true, false),
432            OrdinaryAdmission::Inserted
433        );
434        assert_eq!(buffer.identities()[0].sequence_number(), 3);
435        assert_eq!(LogRecordBuffer::new(2).total_record_count(), 0);
436    }
437
438    #[test]
439    fn log_buffer_duplicate_timestamps_keep_distinct_sequences() {
440        let mut buffer = LogRecordBuffer::new(2);
441        let duplicate = record(4);
442        assert_eq!(
443            buffer.admit_ordinary(duplicate.clone(), true, false),
444            OrdinaryAdmission::Inserted
445        );
446        assert_eq!(
447            buffer.admit_ordinary(duplicate, true, false),
448            OrdinaryAdmission::Inserted
449        );
450
451        let identities = buffer.identities();
452        assert_eq!(identities[0].date(), identities[1].date());
453        assert_eq!(identities[0].time(), identities[1].time());
454        assert_eq!(identities[0].sequence_number(), 1);
455        assert_eq!(identities[1].sequence_number(), 2);
456    }
457
458    #[test]
459    fn log_buffer_clone_restore_preserves_payloads_and_derived_identities() {
460        let mut buffer = LogRecordBuffer::new(2);
461        assert_eq!(
462            buffer.admit_ordinary(record(1), true, false),
463            OrdinaryAdmission::Inserted
464        );
465        assert_eq!(
466            buffer.admit_ordinary(record(2), true, false),
467            OrdinaryAdmission::Inserted
468        );
469        let records = buffer.records().clone();
470        let identities = buffer.identities();
471        let total = buffer.total_record_count();
472
473        let snapshot = buffer.clone();
474        buffer.clear();
475        buffer = snapshot;
476
477        assert_eq!(buffer.records(), &records);
478        assert_eq!(buffer.identities(), identities);
479        assert_eq!(buffer.total_record_count(), total);
480    }
481}