use core::future::Future;
use bacnet_encoding::constructed::tagged::{decode_app_unsigned, expect_end};
use bacnet_services::read_range::{
LogRecords, RangeSpec, ReadRangeReply, ReadRangeRequest, ReadRangeValidation,
ReadRangeViolation,
};
use bacnet_transport::port::TransportPort;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{Date, ObjectIdentifier, Time};
use crate::client::BACnetClient;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LogCursor {
Oldest,
Sequence(u64),
Position(u64),
Time(Date, Time),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LogGap {
pub expected: u64,
pub first: u64,
pub skipped: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct LogPage {
pub records: LogRecords,
pub first_sequence_number: Option<u64>,
pub result_flags: (bool, bool, bool),
pub gap: Option<LogGap>,
pub violations: Vec<ReadRangeViolation>,
pub next: LogCursor,
pub done: bool,
pub wrapped: bool,
}
pub trait LogRequester {
fn read_unsigned(
&self,
mac: &[u8],
log: ObjectIdentifier,
property: PropertyIdentifier,
) -> impl Future<Output = Result<u64, Error>> + Send;
fn read_range_lenient(
&self,
mac: &[u8],
request: &ReadRangeRequest,
) -> impl Future<Output = Result<ReadRangeReply, Error>> + Send;
}
impl<T: TransportPort + 'static> LogRequester for BACnetClient<T> {
async fn read_unsigned(
&self,
mac: &[u8],
log: ObjectIdentifier,
property: PropertyIdentifier,
) -> Result<u64, Error> {
let ack = self.read_property(mac, log, property, None).await?;
unsigned_value(&ack.property_value)
}
async fn read_range_lenient(
&self,
mac: &[u8],
request: &ReadRangeRequest,
) -> Result<ReadRangeReply, Error> {
self.read_range_with(mac, request, ReadRangeValidation::Lenient)
.await
}
}
pub fn unsigned_value(value: &[u8]) -> Result<u64, Error> {
const WHAT: &str = "log record count";
let (count, end) = decode_app_unsigned::<u64>(value, 0, WHAT)?;
expect_end(value, end, end, WHAT)?;
Ok(count)
}
impl<T: TransportPort + 'static> BACnetClient<T> {
pub async fn read_log_page(
&self,
mac: &[u8],
log: ObjectIdentifier,
cursor: LogCursor,
page_size: u16,
) -> Result<LogPage, Error> {
read_log_page(self, mac, log, cursor, page_size).await
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SequenceSpace {
max: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Window {
Empty { next: u64 },
Records { oldest: u64, newest: u64 },
}
impl SequenceSpace {
pub(crate) fn of(object_type: ObjectType) -> Result<Self, Error> {
match object_type {
ObjectType::AUDIT_LOG => Ok(Self { max: u64::MAX }),
ObjectType::TREND_LOG | ObjectType::EVENT_LOG | ObjectType::TREND_LOG_MULTIPLE => {
Ok(Self {
max: u64::from(u32::MAX),
})
}
other => Err(Error::Encoding(format!(
"a paged log read needs a Trend Log, Event Log, Trend Log Multiple or \
Audit Log, not {other}"
))),
}
}
pub(crate) fn advance(self, sequence: u64, n: u64) -> u64 {
if sequence == 0 {
return n;
}
let size = u128::from(self.max);
((u128::from(sequence - 1) + u128::from(n)) % size + 1) as u64
}
fn retreat(self, sequence: u64, back: u64) -> u64 {
if sequence == 0 {
let size = u128::from(self.max) + 1;
return ((size - u128::from(back) % size) % size) as u64;
}
let size = u128::from(self.max);
((u128::from(sequence - 1) + size - u128::from(back) % size) % size + 1) as u64
}
pub(crate) fn distance(self, from: u64, to: u64) -> i128 {
let size = i128::from(self.max);
let at = |sequence: u64| i128::from(sequence.max(1));
let ahead = (at(to) - at(from)).rem_euclid(size);
if ahead > size / 2 {
ahead - size
} else {
ahead
}
}
pub(crate) fn window(self, record_count: u64, total: u64) -> Result<Window, Error> {
if record_count > self.max || total > self.max {
return Err(Error::OutOfRange(format!(
"log counts {record_count} and {total} exceed the log's sequence range"
)));
}
if record_count == 0 {
return Ok(Window::Empty {
next: self.advance(total, 1),
});
}
Ok(Window::Records {
oldest: self.retreat(total, record_count - 1),
newest: total,
})
}
}
fn page_count(page_size: u16) -> Result<i32, Error> {
match i16::try_from(page_size) {
Ok(count @ 1..) => Ok(i32::from(count)),
_ => Err(Error::Encoding(format!(
"log page size must be 1..=32767, got {page_size}"
))),
}
}
fn request(log: ObjectIdentifier, cursor: LogCursor, count: i32) -> ReadRangeRequest {
let range = match cursor {
LogCursor::Oldest => unreachable!("Oldest resolves to a sequence number first"),
LogCursor::Sequence(reference_seq) => RangeSpec::BySequenceNumber {
reference_seq,
count,
},
LogCursor::Position(reference_index) => RangeSpec::ByPosition {
reference_index,
count,
},
LogCursor::Time(date, time) => RangeSpec::ByTime {
reference_time: (date, time),
count,
},
};
ReadRangeRequest {
object_identifier: log,
property_identifier: PropertyIdentifier::LOG_BUFFER,
property_array_index: None,
range: Some(range),
}
}
const RETRIES: usize = 3;
async fn window<R: LogRequester + ?Sized>(
requester: &R,
mac: &[u8],
log: ObjectIdentifier,
space: SequenceSpace,
) -> Result<Window, Error> {
let total = || requester.read_unsigned(mac, log, PropertyIdentifier::TOTAL_RECORD_COUNT);
let mut before = total().await?;
let mut attempts = 0;
loop {
let record_count = requester
.read_unsigned(mac, log, PropertyIdentifier::RECORD_COUNT)
.await?;
let after = total().await?;
attempts += 1;
if after == before || attempts > RETRIES {
return space.window(record_count, before);
}
before = after;
}
}
pub async fn read_log_page<R: LogRequester + ?Sized>(
requester: &R,
mac: &[u8],
log: ObjectIdentifier,
cursor: LogCursor,
page_size: u16,
) -> Result<LogPage, Error> {
let space = SequenceSpace::of(log.object_type())?;
let count = page_count(page_size)?;
let cursor = match cursor {
LogCursor::Oldest => match window(requester, mac, log, space).await? {
Window::Empty { next } => return Ok(empty_page(log, LogCursor::Sequence(next))),
Window::Records { oldest, .. } => LogCursor::Sequence(oldest),
},
cursor => cursor,
};
let mut reply = requester
.read_range_lenient(mac, &request(log, cursor, count))
.await?;
let LogCursor::Sequence(expected) = cursor else {
return page(space, cursor, reply, None);
};
let mut asked = expected;
let mut asked_while_held = false;
let mut restarts = 0;
loop {
if reply.ack.item_count != 0 {
return page(
space,
LogCursor::Sequence(asked),
reply,
(asked != expected).then_some(expected),
);
}
if let Some(rule) = fatal(&reply.violations, LogCursor::Sequence(asked)) {
return Err(Error::ReadRangeViolation(rule));
}
if restarts == RETRIES {
break;
}
restarts += 1;
asked = match window(requester, mac, log, space).await? {
Window::Records { oldest, newest }
if space.distance(asked, space.advance(newest, 1)) != 0 =>
{
let held = space.distance(oldest, asked) >= 0 && space.distance(asked, newest) >= 0;
if held && asked_while_held {
break;
}
asked_while_held = true;
if held { asked } else { oldest }
}
_ => return page(space, LogCursor::Sequence(asked), reply, None),
};
reply = requester
.read_range_lenient(mac, &request(log, LogCursor::Sequence(asked), count))
.await?;
}
Err(Error::LogNotAdvancing {
requested: asked,
returned: None,
})
}
fn empty_page(log: ObjectIdentifier, next: LogCursor) -> LogPage {
LogPage {
records: LogRecords::empty_for(log.object_type())
.expect("a log type was checked before reading"),
first_sequence_number: None,
result_flags: (false, false, false),
gap: None,
violations: Vec::new(),
next,
done: true,
wrapped: false,
}
}
fn fatal(violations: &[ReadRangeViolation], cursor: LogCursor) -> Option<ReadRangeViolation> {
let sequenced = !matches!(cursor, LogCursor::Position(_));
violations
.iter()
.copied()
.find(|violation| match violation {
ReadRangeViolation::MissingFirstSequenceNumber => sequenced,
ReadRangeViolation::ZeroFirstSequenceNumber
| ReadRangeViolation::UnexpectedFirstSequenceNumber
| ReadRangeViolation::MoreItemsPastEnd
| ReadRangeViolation::ItemCountExceedsRequest => false,
_ => true,
})
}
pub(crate) fn page(
space: SequenceSpace,
cursor: LogCursor,
reply: ReadRangeReply,
restarted: Option<u64>,
) -> Result<LogPage, Error> {
let ReadRangeReply { ack, violations } = reply;
if let Some(rule) = fatal(&violations, cursor) {
return Err(Error::ReadRangeViolation(rule));
}
let records = match ack.log_records() {
Some(records) => records?,
None => {
return Err(Error::ReadRangeViolation(
ReadRangeViolation::PropertyMismatch,
));
}
};
let returned = u64::from(ack.item_count);
let done = returned == 0 || ack.result_flags.1;
let mut gap = None;
let mut wrapped = false;
let (first_sequence_number, next) = match (cursor, ack.first_sequence_number) {
(LogCursor::Position(position), _) => {
(None, LogCursor::Position(position.saturating_add(returned)))
}
(cursor, None) => (None, cursor),
(cursor, Some(first)) => {
if let LogCursor::Sequence(asked) = cursor {
let ahead = space.distance(asked, first);
if ahead < 0 {
return Err(Error::LogNotAdvancing {
requested: asked,
returned: Some(first),
});
}
let expected = restarted.unwrap_or(asked);
let skipped = space.distance(expected, first);
if skipped != 0 {
gap = Some(LogGap {
expected,
first,
skipped: u64::try_from(skipped).ok(),
});
}
}
wrapped =
first != 0 && u128::from(first) + u128::from(returned) > u128::from(space.max);
(
Some(first),
LogCursor::Sequence(space.advance(first, returned)),
)
}
};
Ok(LogPage {
records,
first_sequence_number,
result_flags: ack.result_flags,
gap,
violations,
next,
done,
wrapped,
})
}
#[cfg(test)]
#[path = "log_reader_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "log_reader_device_tests.rs"]
mod device_tests;