1use std::collections::VecDeque;
4
5use bacnet_types::constructed::{BACnetLogRecord, LogDatum};
6use bacnet_types::primitives::{Date, PropertyValue, Time};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct LogRecordIdentity {
16 sequence_number: u32,
17 date: Date,
18 time: Time,
19}
20
21impl LogRecordIdentity {
22 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 pub fn sequence_number(&self) -> u32 {
33 self.sequence_number
34 }
35
36 pub fn date(&self) -> Date {
38 self.date
39 }
40
41 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}