use std::future::Future;
use std::ops::ControlFlow;
use bacnet_encoding::constructed::decode_device_object_property_reference;
use bacnet_types::enums::{ObjectType, PropertyIdentifier};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
use super::{Datum, StateTexts, ValueFormat, holds_state, is_binary, is_multistate};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LoggedProperty {
pub object: ObjectIdentifier,
pub property: PropertyIdentifier,
pub index: Option<u32>,
pub device: Option<u32>,
}
impl LoggedProperty {
pub fn holds_states(&self) -> bool {
let object_type = self.object.object_type();
(is_binary(object_type) || is_multistate(object_type)) && holds_state(self.property)
}
fn on(&self, log_device: Option<u32>) -> bool {
self.device.is_none_or(|device| log_device == Some(device))
}
}
pub const MAX_STATE_TEXT_OBJECTS: usize = 8;
static NO_TEXTS: StateTexts = StateTexts {
active: None,
inactive: None,
states: Vec::new(),
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LogColumns {
columns: Vec<Option<LoggedProperty>>,
raw: Vec<StateTexts>,
texts: Vec<StateTexts>,
}
impl LogColumns {
pub fn applies(object_type: ObjectType) -> bool {
object_type == ObjectType::TREND_LOG || object_type == ObjectType::TREND_LOG_MULTIPLE
}
pub fn decode(data: &[u8]) -> Self {
let mut columns = Vec::new();
let mut offset = 0;
while offset < data.len() {
let Ok((reference, next)) = decode_device_object_property_reference(data, offset)
else {
return Self::default();
};
columns.push((!reference.is_unset()).then(|| {
LoggedProperty {
object: reference.object_identifier,
property: PropertyIdentifier::from_raw(reference.property_identifier),
index: reference.property_array_index,
device: reference
.device_identifier
.map(|device| device.instance_number())
.filter(|device| *device != ObjectIdentifier::WILDCARD_INSTANCE),
}
}));
offset = next;
}
columns.into_iter().collect()
}
pub fn column(&self, column: usize) -> Option<LoggedProperty> {
self.columns.get(column).copied().flatten()
}
pub fn is_empty(&self) -> bool {
self.columns.iter().all(Option::is_none)
}
pub fn state_texts(&self, column: usize) -> &StateTexts {
self.texts.get(column).unwrap_or(&NO_TEXTS)
}
pub fn raw_state_texts(&self, column: usize) -> &StateTexts {
self.raw.get(column).unwrap_or(&NO_TEXTS)
}
fn state_objects(&self, log_device: Option<u32>) -> Vec<ObjectIdentifier> {
let mut objects = Vec::new();
for column in self.columns.iter().flatten() {
if column.holds_states() && column.on(log_device) && !objects.contains(&column.object) {
if objects.len() == MAX_STATE_TEXT_OBJECTS {
break;
}
objects.push(column.object);
}
}
objects
}
fn set_state_texts(
&mut self,
log_device: Option<u32>,
object: ObjectIdentifier,
texts: &StateTexts,
) {
let cleaned = texts.cleaned();
for (index, column) in self.columns.iter().enumerate() {
if column.is_some_and(|column| {
column.object == object && column.holds_states() && column.on(log_device)
}) {
if self.texts.len() <= index {
self.texts.resize(index + 1, StateTexts::default());
self.raw.resize(index + 1, StateTexts::default());
}
self.texts[index] = cleaned.clone();
self.raw[index] = texts.clone();
}
}
}
pub async fn read_state_texts<F, Fut>(&mut self, log_device: Option<u32>, mut read: F)
where
F: FnMut(ObjectIdentifier) -> Fut,
Fut: Future<Output = ControlFlow<(), StateTexts>>,
{
for object in self.state_objects(log_device) {
match read(object).await {
ControlFlow::Continue(texts) => self.set_state_texts(log_device, object, &texts),
ControlFlow::Break(()) => break,
}
}
}
}
impl FromIterator<Option<LoggedProperty>> for LogColumns {
fn from_iter<I: IntoIterator<Item = Option<LoggedProperty>>>(columns: I) -> Self {
Self {
columns: columns.into_iter().collect(),
raw: Vec::new(),
texts: Vec::new(),
}
}
}
const UNNAMED: PropertyIdentifier = PropertyIdentifier::LOG_BUFFER;
impl ValueFormat<'_> {
fn for_column(&self, column: Option<LoggedProperty>) -> (Self, PropertyIdentifier) {
match column {
Some(column) => (
Self {
object_type: column.object.object_type(),
..*self
},
column.property,
),
None => (*self, UNNAMED),
}
}
pub fn logged(&self, column: Option<LoggedProperty>, value: &PropertyValue) -> String {
let (format, property) = self.for_column(column);
let text = format.element(property, value);
match value {
PropertyValue::Enumerated(raw) if text == raw.to_string() => format!("enum {raw}"),
_ => text,
}
}
pub fn logged_data(&self, column: Option<LoggedProperty>, value: &PropertyValue) -> Datum {
let (format, property) = self.for_column(column);
format.element_data(property, value)
}
pub fn logged_octets(&self, column: Option<LoggedProperty>, octets: &[u8]) -> String {
let (format, property) = self.for_column(column);
format.value_at(property, column.and_then(|column| column.index), octets)
}
pub fn logged_octets_data(&self, column: Option<LoggedProperty>, octets: &[u8]) -> Datum {
let (format, property) = self.for_column(column);
format.data_at(property, column.and_then(|column| column.index), octets)
}
}
#[cfg(test)]
#[path = "logged_tests.rs"]
mod tests;