pub mod alarms;
mod clean;
mod commands;
mod constructed;
mod data;
mod event_parameter;
mod fields;
pub mod log_records;
mod logged;
pub mod notification;
mod primitive;
mod recipients;
mod tagged;
mod units;
mod wire;
pub use clean::{MAX_NAME_CHARS, clean_name, clean_text};
pub(crate) use constructed::timestamp_text;
pub use data::{Datum, Fields, PLAIN_ELEMENTS};
pub use logged::{LogColumns, LoggedProperty, MAX_STATE_TEXT_OBJECTS};
pub use wire::format_property_value;
use bacnet_encoding::constructed::{ConstructedElement, ElementShape, property_element};
use bacnet_encoding::primitives::{decode_application_value, decode_priority_date_time};
use bacnet_types::enums::{
EngineeringUnits, ErrorClass, ErrorCode, ObjectType, PropertyIdentifier,
};
use bacnet_types::error::{Error, RemoteError};
use bacnet_types::primitives::{Date, PropertyValue, Time};
use units::unit_symbol;
pub type RawRow = (PropertyIdentifier, Result<Vec<u8>, RowError>);
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RowError {
Remote(RemoteError),
NoValue,
Other(String),
}
impl RowError {
pub fn class_code(&self) -> Option<(ErrorClass, ErrorCode)> {
match self {
Self::Remote(RemoteError::Error { class, code }) => Some((*class, *code)),
_ => None,
}
}
}
impl From<Error> for RowError {
fn from(error: Error) -> Self {
match error.remote() {
Some(remote) => Self::Remote(remote),
None => Self::Other(error.to_string()),
}
}
}
impl std::error::Error for RowError {}
impl std::fmt::Display for RowError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Remote(RemoteError::Error { class, code }) => write!(f, "{class}:{code}"),
Self::Remote(remote) => write!(f, "{remote}"),
Self::NoValue => f.write_str("no value"),
Self::Other(text) => f.write_str(text),
}
}
}
const MAX_LIST_SHOWN: usize = 8;
pub fn decode_all(data: &[u8]) -> Option<Vec<PropertyValue>> {
let mut offset = 0;
let mut values = Vec::new();
while offset < data.len() {
let (value, next) = decode_application_value(data, offset).ok()?;
values.push(value);
offset = next;
}
Some(values)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Floats {
Display,
Exact,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StateTexts {
pub active: Option<String>,
pub inactive: Option<String>,
pub states: Vec<String>,
}
impl StateTexts {
pub fn properties(object_type: ObjectType) -> &'static [PropertyIdentifier] {
if is_binary(object_type) {
&[
PropertyIdentifier::ACTIVE_TEXT,
PropertyIdentifier::INACTIVE_TEXT,
]
} else if is_multistate(object_type) {
&[PropertyIdentifier::STATE_TEXT]
} else {
&[]
}
}
pub fn name(object_type: ObjectType, property: PropertyIdentifier, index: Option<u32>) -> bool {
(is_binary(object_type) || is_multistate(object_type))
&& holds_state(property)
&& index != Some(0)
}
pub fn cleaned(&self) -> Self {
Self {
active: self.active.as_deref().map(clean_name),
inactive: self.inactive.as_deref().map(clean_name),
states: self.states.iter().map(|text| clean_name(text)).collect(),
}
}
pub fn from_rows(rows: &[RawRow]) -> Self {
let values = |property| {
rows.iter()
.find(|(p, _)| *p == property)
.and_then(|(_, value)| value.as_deref().ok())
.and_then(decode_all)
.unwrap_or_default()
.into_iter()
.map(|value| match value {
PropertyValue::CharacterString(text) => text,
_ => String::new(),
})
};
let text = |property| values(property).next().filter(|text| !text.is_empty());
Self {
active: text(PropertyIdentifier::ACTIVE_TEXT),
inactive: text(PropertyIdentifier::INACTIVE_TEXT),
states: values(PropertyIdentifier::STATE_TEXT).collect(),
}
}
}
pub(crate) fn is_binary(object_type: ObjectType) -> bool {
[
ObjectType::BINARY_INPUT,
ObjectType::BINARY_OUTPUT,
ObjectType::BINARY_VALUE,
]
.contains(&object_type)
}
pub(crate) fn is_multistate(object_type: ObjectType) -> bool {
[
ObjectType::MULTI_STATE_INPUT,
ObjectType::MULTI_STATE_OUTPUT,
ObjectType::MULTI_STATE_VALUE,
]
.contains(&object_type)
}
pub(crate) fn is_date_time_value(object_type: ObjectType) -> bool {
[
ObjectType::DATETIME_VALUE,
ObjectType::DATETIMEPATTERN_VALUE,
]
.contains(&object_type)
}
pub(crate) fn holds_state(property: PropertyIdentifier) -> bool {
[
PropertyIdentifier::PRESENT_VALUE,
PropertyIdentifier::FEEDBACK_VALUE,
PropertyIdentifier::RELINQUISH_DEFAULT,
PropertyIdentifier::PRIORITY_ARRAY,
PropertyIdentifier::ALARM_VALUE,
PropertyIdentifier::ALARM_VALUES,
PropertyIdentifier::FAULT_VALUES,
]
.contains(&property)
}
#[derive(Debug, Clone, Copy)]
pub struct ValueFormat<'a> {
object_type: ObjectType,
texts: &'a StateTexts,
floats: Floats,
max_shown: usize,
}
impl<'a> ValueFormat<'a> {
pub fn new(object_type: ObjectType, texts: &'a StateTexts, floats: Floats) -> Self {
Self {
object_type,
texts,
floats,
max_shown: MAX_LIST_SHOWN,
}
}
pub fn whole_lists(self) -> Self {
Self {
max_shown: usize::MAX,
..self
}
}
pub fn for_object<'b>(
&self,
object_type: ObjectType,
texts: &'b StateTexts,
) -> ValueFormat<'b> {
ValueFormat {
object_type,
texts,
floats: self.floats,
max_shown: self.max_shown,
}
}
pub fn real(&self, value: f32) -> String {
primitive::text(
PropertyIdentifier::PRESENT_VALUE,
&PropertyValue::Real(value),
self.floats,
)
}
pub fn constructed_items(
&self,
property: PropertyIdentifier,
data: &[u8],
) -> Option<Vec<String>> {
let (element, _) = property_element(self.object_type, property)?;
constructed::elements(self, element, data, 0)
}
pub fn value(&self, property: PropertyIdentifier, data: &[u8]) -> String {
self.value_at(property, None, data)
}
fn element_at(
&self,
property: PropertyIdentifier,
index: Option<u32>,
) -> Option<(ConstructedElement, ElementShape, usize)> {
let (element, shape) = property_element(self.object_type, property)?;
match index {
None => Some((element, shape, 0)),
Some(0) => None,
Some(index) => Some((
element,
ElementShape::Single,
usize::try_from(index - 1).ok()?,
)),
}
}
fn elements_at(
&self,
property: PropertyIdentifier,
index: Option<u32>,
data: &[u8],
) -> Option<(ConstructedElement, ElementShape, Vec<String>)> {
let (element, shape, first) = self.element_at(property, index)?;
let texts = constructed::elements(self, element, data, first)?;
Some((element, shape, texts))
}
pub fn value_at(
&self,
property: PropertyIdentifier,
index: Option<u32>,
data: &[u8],
) -> String {
if let Some((element, shape, texts)) = self.elements_at(property, index, data) {
return match (shape, texts.as_slice()) {
(ElementShape::Single, [one]) => one.clone(),
_ => self.list(texts, constructed::separator(element)),
};
}
let Some(values) = decode_all(data) else {
return primitive::raw(data);
};
match values.as_slice() {
[] => "(empty)".into(),
[one] if index == Some(0) => primitive::text(property, one, self.floats),
[one] => self.element(property, one),
[PropertyValue::Date(date), PropertyValue::Time(time)] => {
primitive::date_time(date, time)
}
[PropertyValue::Date(start), PropertyValue::Date(end)] => {
primitive::date_range(start, end)
}
many => self.list(
many.iter()
.map(|value| self.element(property, value))
.collect(),
", ",
),
}
}
pub fn with_units(
&self,
property: PropertyIdentifier,
data: &[u8],
units: Option<EngineeringUnits>,
) -> String {
let text = self.value(property, data);
let numeric = matches!(
decode_all(data).as_deref(),
Some([PropertyValue::Real(_)
| PropertyValue::Double(_)
| PropertyValue::Unsigned(_)
| PropertyValue::Signed(_)])
);
match units {
Some(units) if numeric && units != EngineeringUnits::NO_UNITS => {
format!("{text} {}", unit_symbol(units))
}
_ => text,
}
}
pub fn plain(&self, property: PropertyIdentifier, data: &[u8]) -> String {
match decode_all(data).as_deref() {
Some([PropertyValue::CharacterString(text)]) => text.clone(),
_ => self.value(property, data),
}
}
pub fn element(&self, property: PropertyIdentifier, value: &PropertyValue) -> String {
if holds_state(property)
&& let Some(state) = self.state(value)
{
return state;
}
if let Some((date, time)) = self.priority_date_time(property, value) {
return primitive::date_time(&date, &time);
}
primitive::text(property, value, self.floats)
}
fn priority_date_time(
&self,
property: PropertyIdentifier,
value: &PropertyValue,
) -> Option<(Date, Time)> {
let PropertyValue::ApplicationData(octets) = value else {
return None;
};
if !is_date_time_value(self.object_type) || property != PropertyIdentifier::PRIORITY_ARRAY {
return None;
}
match decode_priority_date_time(octets, 0) {
Ok((date_time, end)) if end == octets.len() => Some(date_time),
_ => None,
}
}
fn state(&self, value: &PropertyValue) -> Option<String> {
let named = |name: String, text: Option<&String>| match text {
Some(text) if !text.is_empty() => format!("{name} ({text})"),
_ => name,
};
match value {
PropertyValue::Enumerated(raw @ (0 | 1)) if is_binary(self.object_type) => {
Some(if *raw == 1 {
named("active".into(), self.texts.active.as_ref())
} else {
named("inactive".into(), self.texts.inactive.as_ref())
})
}
PropertyValue::Unsigned(state) if is_multistate(self.object_type) => {
let text = usize::try_from(*state)
.ok()
.and_then(|state| state.checked_sub(1))
.and_then(|index| self.texts.states.get(index));
Some(named(state.to_string(), text))
}
_ => None,
}
}
}
impl ValueFormat<'_> {
fn list(&self, texts: Vec<String>, separator: &str) -> String {
let count = texts.len();
let shown: Vec<String> = texts.into_iter().take(self.max_shown).collect();
match count.saturating_sub(self.max_shown) {
0 => format!("[{}]", shown.join(separator)),
more => format!(
"[{}{separator}+{more} more] ({count} items)",
shown.join(separator)
),
}
}
}
pub fn raw_text(data: &[u8]) -> String {
primitive::raw(data)
}
pub fn object_name(rows: &[RawRow]) -> Option<String> {
let (_, value) = rows
.iter()
.find(|(p, _)| *p == PropertyIdentifier::OBJECT_NAME)?;
name_text(value.as_deref().ok()?)
}
pub fn name_text(data: &[u8]) -> Option<String> {
match decode_all(data)?.as_slice() {
[PropertyValue::CharacterString(text)] => Some(clean_name(text)),
_ => None,
}
}
pub fn units(rows: &[RawRow]) -> Option<EngineeringUnits> {
let (_, value) = rows.iter().find(|(p, _)| *p == PropertyIdentifier::UNITS)?;
match decode_all(value.as_deref().ok()?)?.as_slice() {
[PropertyValue::Enumerated(raw)] => Some(EngineeringUnits::from_raw(*raw)),
_ => None,
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod render_tests;