use bacnet_types::enums::{resolve_bits, ObjectType, PropertyIdentifier, ResolvedBits};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
use crate::tui::format::{decode_all, object_name, units, Floats, RawRow, StateTexts, ValueFormat};
use crate::tui::point::{
DeviceCard, Health, ObjectView, PriorityView, PropertyRow, ReadVia, PRIORITIES,
};
fn single(rows: &[RawRow], property: PropertyIdentifier) -> Option<PropertyValue> {
let (_, value) = rows.iter().find(|(p, _)| *p == property)?;
let mut values = decode_all(value.as_ref().ok()?)?;
(values.len() == 1).then(|| values.remove(0))
}
fn supported(data: &[u8], property: PropertyIdentifier) -> Option<String> {
let value = decode_all(data)?.into_iter().next()?;
let count = match resolve_bits(property, &value)? {
ResolvedBits::ServicesSupported(bits) => bits.iter().count(),
ResolvedBits::ObjectTypesSupported(bits) => bits.iter().count(),
_ => return None,
};
Some(format!("{count} supported"))
}
pub(crate) fn card(rows: &[RawRow], via: ReadVia) -> DeviceCard {
const LABELS: [(PropertyIdentifier, &str); 13] = [
(PropertyIdentifier::OBJECT_NAME, "Name"),
(PropertyIdentifier::VENDOR_NAME, "Vendor"),
(PropertyIdentifier::VENDOR_IDENTIFIER, "Vendor ID"),
(PropertyIdentifier::MODEL_NAME, "Model"),
(PropertyIdentifier::FIRMWARE_REVISION, "Firmware"),
(PropertyIdentifier::APPLICATION_SOFTWARE_VERSION, "Software"),
(PropertyIdentifier::PROTOCOL_VERSION, "Protocol version"),
(PropertyIdentifier::PROTOCOL_REVISION, "Protocol revision"),
(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED, "Services"),
(
PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED,
"Object types",
),
(PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED, "Max APDU"),
(PropertyIdentifier::SEGMENTATION_SUPPORTED, "Segmentation"),
(PropertyIdentifier::DATABASE_REVISION, "Database revision"),
];
let texts = StateTexts::default();
let format = ValueFormat::new(ObjectType::DEVICE, &texts, Floats::Display);
let name = object_name(rows);
let rows = LABELS
.iter()
.map(|&(property, label)| {
let text = match rows.iter().find(|(p, _)| *p == property) {
Some((_, Ok(data))) => {
supported(data, property).unwrap_or_else(|| format.plain(property, data))
}
Some((_, Err(error))) => format!("({error})"),
None => "-".into(),
};
(label, text)
})
.collect();
DeviceCard {
rows,
via: Some(via),
name,
}
}
fn health(rows: &[RawRow]) -> Health {
if let Some(PropertyValue::BitString { data, .. }) =
single(rows, PropertyIdentifier::STATUS_FLAGS)
{
let flags = bacnet_types::bitstring::status_flags_from_bacnet(&data);
return Health {
alarm: flags.contains(StatusFlags::IN_ALARM),
fault: flags.contains(StatusFlags::FAULT),
overridden: flags.contains(StatusFlags::OVERRIDDEN),
out_of_service: flags.contains(StatusFlags::OUT_OF_SERVICE),
};
}
let nonzero =
|property| matches!(single(rows, property), Some(PropertyValue::Enumerated(n)) if n != 0);
Health {
alarm: nonzero(PropertyIdentifier::EVENT_STATE),
fault: nonzero(PropertyIdentifier::RELIABILITY),
overridden: false,
out_of_service: matches!(
single(rows, PropertyIdentifier::OUT_OF_SERVICE),
Some(PropertyValue::Boolean(true))
),
}
}
fn priority(rows: &[RawRow], format: &ValueFormat<'_>) -> Option<PriorityView> {
let (_, data) = rows
.iter()
.find(|(p, _)| *p == PropertyIdentifier::PRIORITY_ARRAY)?;
let values = decode_all(data.as_ref().ok()?)?;
if values.len() != PRIORITIES {
return None;
}
let mut view = PriorityView::default();
for (slot, value) in view.slots.iter_mut().zip(&values) {
if *value != PropertyValue::Null {
*slot = Some(format.element(PropertyIdentifier::PRIORITY_ARRAY, value));
}
}
let data = |property| {
rows.iter()
.find(|(p, _)| *p == property)
.and_then(|(_, value)| value.as_deref().ok())
};
view.relinquish_default = data(PropertyIdentifier::RELINQUISH_DEFAULT)
.map(|data| format.value(PropertyIdentifier::RELINQUISH_DEFAULT, data));
view.present_value = data(PropertyIdentifier::PRESENT_VALUE)
.map(|data| format.with_units(PropertyIdentifier::PRESENT_VALUE, data, units(rows)));
Some(view)
}
pub(crate) fn object_view(object: ObjectIdentifier, rows: &[RawRow], via: ReadVia) -> ObjectView {
let texts = StateTexts::from_rows(rows);
let format = ValueFormat::new(object.object_type(), &texts, Floats::Display);
let units = units(rows);
let priority = priority(rows, &format);
let properties = rows
.iter()
.map(|(property, value)| PropertyRow {
property: *property,
value: match value {
Ok(_) if *property == PropertyIdentifier::PRIORITY_ARRAY && priority.is_some() => {
Ok(match priority.as_ref().and_then(PriorityView::winner) {
Some(slot) => format!("priority {slot} in control"),
None => "all relinquished".into(),
})
}
Ok(data) if *property == PropertyIdentifier::PRESENT_VALUE => {
Ok(format.with_units(*property, data, units))
}
Ok(data) => Ok(format.value(*property, data)),
Err(error) => Err(error.clone()),
},
})
.collect();
ObjectView {
object,
properties,
health: health(rows),
priority,
via,
name: object_name(rows),
}
}
#[cfg(test)]
mod tests {
use bacnet_encoding::primitives::encode_property_value;
use bytes::BytesMut;
use super::*;
fn encoded(values: &[PropertyValue]) -> Vec<u8> {
let mut buf = BytesMut::new();
for value in values {
encode_property_value(&mut buf, value).unwrap();
}
buf.to_vec()
}
#[test]
fn health_comes_from_status_flags_or_the_properties_behind_them() {
let flags = PropertyValue::BitString {
unused_bits: 4,
data: vec![0b0101_0000],
};
let rows = vec![(PropertyIdentifier::STATUS_FLAGS, Ok(encoded(&[flags])))];
let flagged = health(&rows);
assert!(flagged.fault && flagged.out_of_service && !flagged.alarm && !flagged.overridden);
let rows = vec![
(
PropertyIdentifier::EVENT_STATE,
Ok(encoded(&[PropertyValue::Enumerated(2)])),
),
(
PropertyIdentifier::OUT_OF_SERVICE,
Ok(encoded(&[PropertyValue::Boolean(true)])),
),
];
assert_eq!(health(&rows).badges(), ["[ALM]", "[OOS]"]);
}
#[test]
fn a_priority_array_needs_sixteen_values() {
let mut slots = vec![PropertyValue::Null; 16];
slots[9] = PropertyValue::Real(55.0);
let rows = vec![
(PropertyIdentifier::PRIORITY_ARRAY, Ok(encoded(&slots))),
(
PropertyIdentifier::RELINQUISH_DEFAULT,
Ok(encoded(&[PropertyValue::Real(0.0)])),
),
];
let texts = StateTexts::default();
let format = ValueFormat::new(ObjectType::ANALOG_OUTPUT, &texts, Floats::Display);
let view = priority(&rows, &format).unwrap();
assert_eq!(view.winner(), Some(10));
assert_eq!(view.slots[9].as_deref(), Some("55"));
assert_eq!(view.relinquish_default.as_deref(), Some("0"));
let short = vec![(PropertyIdentifier::PRIORITY_ARRAY, Ok(encoded(&slots[..4])))];
assert!(priority(&short, &format).is_none());
}
}
#[cfg(test)]
mod card_tests {
use bacnet_encoding::primitives::encode_property_value;
use bytes::BytesMut;
use super::*;
fn encoded(value: &PropertyValue) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_property_value(&mut buf, value).unwrap();
buf.to_vec()
}
#[test]
fn the_card_shows_strings_without_quotes() {
let rows = vec![
(
PropertyIdentifier::OBJECT_NAME,
Ok(encoded(&PropertyValue::CharacterString("AHU-1".into()))),
),
(
PropertyIdentifier::SEGMENTATION_SUPPORTED,
Ok(encoded(&PropertyValue::Enumerated(3))),
),
];
let card = card(&rows, ReadVia::Rpm);
assert!(card.rows.contains(&("Name", "AHU-1".into())), "{card:?}");
assert!(
card.rows.contains(&("Segmentation", "none".into())),
"{card:?}"
);
}
#[test]
fn a_binary_priority_array_names_its_states_with_the_object_texts() {
let mut slots = vec![PropertyValue::Null; 16];
slots[7] = PropertyValue::Enumerated(1);
let text = |s: &str| Ok(encoded(&PropertyValue::CharacterString(s.into())));
let rows = vec![
(
PropertyIdentifier::PRESENT_VALUE,
Ok(encoded(&PropertyValue::Enumerated(1))),
),
(
PropertyIdentifier::PRIORITY_ARRAY,
Ok({
let mut buf = BytesMut::new();
for slot in &slots {
encode_property_value(&mut buf, slot).unwrap();
}
buf.to_vec()
}),
),
(
PropertyIdentifier::RELINQUISH_DEFAULT,
Ok(encoded(&PropertyValue::Enumerated(0))),
),
(PropertyIdentifier::ACTIVE_TEXT, text("On")),
(PropertyIdentifier::INACTIVE_TEXT, text("Off")),
];
let object = ObjectIdentifier::new(ObjectType::BINARY_OUTPUT, 1).unwrap();
let view = object_view(object, &rows, ReadVia::Rpm);
let priority = view.priority.as_ref().unwrap();
assert_eq!(priority.slots[7].as_deref(), Some("active (On)"));
assert_eq!(
priority.relinquish_default.as_deref(),
Some("inactive (Off)")
);
assert_eq!(
view.value(PropertyIdentifier::PRESENT_VALUE),
Some("active (On)")
);
}
}