mod constructed;
mod primitive;
mod units;
use bacnet_encoding::constructed::{property_element, ConstructedElement, ElementShape};
use bacnet_encoding::primitives::decode_application_value;
use bacnet_types::enums::{EngineeringUnits, ObjectType, PropertyIdentifier};
use bacnet_types::primitives::PropertyValue;
use units::unit_symbol;
pub(crate) type RawRow = (PropertyIdentifier, Result<Vec<u8>, String>);
const MAX_LIST_SHOWN: usize = 8;
pub(crate) 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(crate) enum Floats {
Display,
Exact,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct StateTexts {
pub(crate) active: Option<String>,
pub(crate) inactive: Option<String>,
pub(crate) states: Vec<String>,
}
impl StateTexts {
pub(crate) 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(crate) 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()
.filter_map(|value| match value {
PropertyValue::CharacterString(text) => Some(text),
_ => None,
})
};
Self {
active: values(PropertyIdentifier::ACTIVE_TEXT).next(),
inactive: values(PropertyIdentifier::INACTIVE_TEXT).next(),
states: values(PropertyIdentifier::STATE_TEXT).collect(),
}
}
}
fn is_binary(object_type: ObjectType) -> bool {
[
ObjectType::BINARY_INPUT,
ObjectType::BINARY_OUTPUT,
ObjectType::BINARY_VALUE,
]
.contains(&object_type)
}
fn is_multistate(object_type: ObjectType) -> bool {
[
ObjectType::MULTI_STATE_INPUT,
ObjectType::MULTI_STATE_OUTPUT,
ObjectType::MULTI_STATE_VALUE,
]
.contains(&object_type)
}
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(crate) struct ValueFormat<'a> {
object_type: ObjectType,
texts: &'a StateTexts,
floats: Floats,
}
impl<'a> ValueFormat<'a> {
pub(crate) fn new(object_type: ObjectType, texts: &'a StateTexts, floats: Floats) -> Self {
Self {
object_type,
texts,
floats,
}
}
pub(crate) fn value(&self, property: PropertyIdentifier, data: &[u8]) -> String {
if let Some((element, shape)) = property_element(self.object_type, property) {
if let Some(texts) = constructed::elements(self, element, data) {
let separator = if element == ConstructedElement::DailySchedule {
"; "
} else {
", "
};
return match (shape, texts.as_slice()) {
(ElementShape::Single, [one]) => one.clone(),
_ => list(texts, separator),
};
}
}
let Some(values) = decode_all(data) else {
return primitive::raw(data);
};
match values.as_slice() {
[] => "(empty)".into(),
[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 => list(
many.iter()
.map(|value| self.element(property, value))
.collect(),
", ",
),
}
}
pub(crate) 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(crate) 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(crate) fn element(&self, property: PropertyIdentifier, value: &PropertyValue) -> String {
if holds_state(property) {
if let Some(state) = self.state(value) {
return state;
}
}
primitive::text(property, value, self.floats)
}
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,
}
}
}
fn list(texts: Vec<String>, separator: &str) -> String {
let count = texts.len();
let shown: Vec<String> = texts.into_iter().take(MAX_LIST_SHOWN).collect();
match count.saturating_sub(MAX_LIST_SHOWN) {
0 => format!("[{}]", shown.join(separator)),
more => format!(
"[{}{separator}+{more} more] ({count} items)",
shown.join(separator)
),
}
}
pub(crate) const MAX_NAME_CHARS: usize = 128;
pub(crate) fn object_name(rows: &[RawRow]) -> Option<String> {
let (_, value) = rows
.iter()
.find(|(p, _)| *p == PropertyIdentifier::OBJECT_NAME)?;
name_text(value.as_deref().ok()?)
}
pub(crate) fn name_text(data: &[u8]) -> Option<String> {
match decode_all(data)?.as_slice() {
[PropertyValue::CharacterString(text)] => Some(clean_name(text)),
_ => None,
}
}
pub(crate) fn clean_name(text: &str) -> String {
text.chars()
.take(MAX_NAME_CHARS)
.map(|ch| if ch.is_control() { ' ' } else { ch })
.collect()
}
pub(crate) 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;