use bacnet_client::client::BACnetClient;
use bacnet_encoding::constructed::{
decode_audit_log_record_at, decode_event_log_record, decode_log_multiple_record,
decode_log_record,
};
use bacnet_transport::port::TransportPort;
use bacnet_types::bitstring::LogStatus;
use bacnet_types::constructed::{
BACnetAuditLogDatum, BACnetAuditNotification, BACnetRecipient, EventLogDatum,
EventNotificationRequest, LogData, LogDatum,
};
use bacnet_types::enums::{ErrorClass, ErrorCode, NotifyType, ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{Date, ObjectIdentifier, PropertyValue, StatusFlags, Time};
use serde::Serialize;
use super::read::{decode_and_format, format_application_values, hex};
use crate::output::{self, OutputFormat};
pub async fn read_range_cmd<T: TransportPort + 'static>(
client: &BACnetClient<T>,
mac: &[u8],
object_type: ObjectType,
instance: u32,
property: PropertyIdentifier,
index: Option<u32>,
format: OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let oid = ObjectIdentifier::new(object_type, instance)?;
let ack = client.read_range(mac, oid, property, index, None).await?;
let heading = Heading {
object: format!("{object_type}:{instance}"),
property: format!("{property}"),
item_count: ack.item_count,
};
let decoder = if property == PropertyIdentifier::LOG_BUFFER {
record_decoder(object_type)
} else {
None
};
match decoder {
Some(decode) => print_records(&heading, &decode_records(decode, &ack.item_data), format),
None => print_items(&heading, &ack.item_data, format),
}
Ok(())
}
struct Heading {
object: String,
property: String,
item_count: u32,
}
#[derive(Debug, PartialEq, Serialize)]
struct LogRecordRow {
timestamp: String,
datum: String,
#[serde(skip_serializing_if = "Option::is_none")]
status_flags: Option<String>,
}
#[derive(Debug, Default)]
struct LogRecords {
rows: Vec<LogRecordRow>,
undecoded: Option<String>,
}
type RecordDecoder = fn(&[u8], usize) -> Result<(LogRecordRow, usize), Error>;
fn record_decoder(object_type: ObjectType) -> Option<RecordDecoder> {
match object_type {
ObjectType::TREND_LOG => Some(trend_log_row),
ObjectType::EVENT_LOG => Some(event_log_row),
ObjectType::TREND_LOG_MULTIPLE => Some(trend_log_multiple_row),
ObjectType::AUDIT_LOG => Some(audit_log_row),
_ => None,
}
}
fn decode_records(decode: RecordDecoder, data: &[u8]) -> LogRecords {
let mut records = LogRecords::default();
let mut offset = 0;
while offset < data.len() {
match decode(data, offset) {
Ok((row, next)) if next > offset => {
records.rows.push(row);
offset = next;
}
_ => {
records.undecoded = Some(hex(&data[offset..]));
break;
}
}
}
records
}
fn trend_log_row(data: &[u8], offset: usize) -> Result<(LogRecordRow, usize), Error> {
let (record, next) = decode_log_record(data, offset)?;
let row = LogRecordRow {
timestamp: timestamp(record.date, record.time),
datum: log_datum(&record.log_datum),
status_flags: record.status_flags.map(status_flags),
};
Ok((row, next))
}
fn event_log_row(data: &[u8], offset: usize) -> Result<(LogRecordRow, usize), Error> {
let (record, next) = decode_event_log_record(data, offset)?;
let datum = match &record.log_datum {
EventLogDatum::LogStatus(status) => log_status(*status),
EventLogDatum::Notification(notification) => event_notification(notification),
EventLogDatum::TimeChange(seconds) => time_change(*seconds),
};
let row = LogRecordRow {
timestamp: timestamp(record.date, record.time),
datum,
status_flags: None,
};
Ok((row, next))
}
fn trend_log_multiple_row(data: &[u8], offset: usize) -> Result<(LogRecordRow, usize), Error> {
let (record, next) = decode_log_multiple_record(data, offset)?;
let datum = match &record.log_data {
LogData::LogStatus(status) => log_status(*status),
LogData::Values(values) => {
let values: Vec<String> = values
.iter()
.map(|value| log_datum(&LogDatum::from(value.clone())))
.collect();
format!("[{}]", values.join(", "))
}
LogData::TimeChange(seconds) => time_change(*seconds),
};
let row = LogRecordRow {
timestamp: timestamp(record.date, record.time),
datum,
status_flags: None,
};
Ok((row, next))
}
fn audit_log_row(data: &[u8], offset: usize) -> Result<(LogRecordRow, usize), Error> {
let (record, next) = decode_audit_log_record_at(data, offset)?;
let datum = match &record.datum {
BACnetAuditLogDatum::LogStatus(status) => log_status(*status),
BACnetAuditLogDatum::AuditNotification(notification) => audit_notification(notification),
BACnetAuditLogDatum::TimeChange(seconds) => time_change(*seconds),
};
let (date, time) = record.timestamp;
let row = LogRecordRow {
timestamp: timestamp(date, time),
datum,
status_flags: None,
};
Ok((row, next))
}
fn timestamp(date: Date, time: Time) -> String {
format!(
"{} {}",
output::format_property_value(&PropertyValue::Date(date)),
output::format_property_value(&PropertyValue::Time(time))
)
}
fn status_flags(flags: StatusFlags) -> String {
if flags.is_empty() {
String::new()
} else {
flags.to_string()
}
}
fn log_status(status: LogStatus) -> String {
format!("log-status {status}")
}
fn time_change(seconds: f32) -> String {
format!("time-change {seconds} s")
}
fn object(oid: ObjectIdentifier) -> String {
format!("{}:{}", oid.object_type(), oid.instance_number())
}
fn log_datum(datum: &LogDatum) -> String {
match datum {
LogDatum::LogStatus(status) => log_status(*status),
LogDatum::BooleanValue(value) => value.to_string(),
LogDatum::RealValue(value) => value.to_string(),
LogDatum::EnumValue(value) => format!("enumerated({value})"),
LogDatum::UnsignedValue(value) => value.to_string(),
LogDatum::SignedValue(value) => value.to_string(),
LogDatum::BitstringValue { unused_bits, data } => {
output::format_property_value(&PropertyValue::BitString {
unused_bits: *unused_bits,
data: data.clone(),
})
}
LogDatum::NullValue => "null".to_string(),
LogDatum::Failure {
error_class,
error_code,
} => format!("failure {}", raw_error(*error_class, *error_code)),
LogDatum::TimeChange(seconds) => time_change(*seconds),
LogDatum::AnyValue(bytes) => decode_and_format(bytes),
}
}
fn raw_error(class: u32, code: u32) -> String {
let class = u16::try_from(class).map_or(class.to_string(), |raw| {
ErrorClass::from_raw(raw).to_string()
});
let code =
u16::try_from(code).map_or(code.to_string(), |raw| ErrorCode::from_raw(raw).to_string());
format!("{class}/{code}")
}
fn event_notification(notification: &EventNotificationRequest) -> String {
let transition = if notification.notify_type == NotifyType::ACK_NOTIFICATION {
notification.to_state.to_string()
} else {
format!("{} -> {}", notification.from_state, notification.to_state)
};
let mut text = format!(
"{} {} {} {transition}",
notification.notify_type,
notification.event_type,
object(notification.event_object_identifier)
);
if let Some(message) = ¬ification.message_text {
text.push_str(&format!(" \"{message}\""));
}
text
}
fn audit_notification(notification: &BACnetAuditNotification) -> String {
let mut text = format!(
"{} {}",
notification.operation,
recipient(¬ification.target_device)
);
if let Some(target) = notification.target_object {
text.push_str(&format!(" {}", object(target)));
}
if let Some(property) = ¬ification.target_property {
text.push_str(&format!(" {}", property.property_identifier));
if let Some(index) = property.property_array_index {
text.push_str(&format!("[{index}]"));
}
}
text.push_str(&format!(" by {}", recipient(¬ification.source_device)));
if let Some((class, code)) = notification.result {
text.push_str(&format!(" failed {class}/{code}"));
}
text
}
fn recipient(recipient: &BACnetRecipient) -> String {
match recipient {
BACnetRecipient::Device(oid) => object(*oid),
BACnetRecipient::Address(address) => format!(
"network {} mac [{}]",
address.network_number,
hex(&address.mac_address)
),
}
}
fn print_records(heading: &Heading, records: &LogRecords, format: OutputFormat) {
match format {
OutputFormat::Table => {
println!(
"ReadRange {} {} count={}",
heading.object, heading.property, heading.item_count
);
let flags = records.rows.iter().any(|row| row.status_flags.is_some());
let mut table = comfy_table::Table::new();
let mut header = vec!["#", "Timestamp", "Datum"];
if flags {
header.push("Status flags");
}
table.set_header(header);
for (number, row) in (1..).zip(&records.rows) {
let mut cells = vec![number.to_string(), row.timestamp.clone(), row.datum.clone()];
if flags {
cells.push(row.status_flags.clone().unwrap_or_default());
}
table.add_row(cells);
}
println!("{table}");
if let Some(hex) = &records.undecoded {
println!(" [raw] {hex}");
}
}
OutputFormat::Json => {
let mut json = serde_json::json!({
"object": heading.object,
"property": heading.property,
"item_count": heading.item_count,
"records": records.rows,
});
if let Some(hex) = &records.undecoded {
json["undecoded"] = serde_json::Value::from(hex.as_str());
}
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
}
}
}
fn print_items(heading: &Heading, data: &[u8], format: OutputFormat) {
let (mut items, undecoded) = format_application_values(data);
match format {
OutputFormat::Table => {
println!(
"ReadRange {} {} count={}",
heading.object, heading.property, heading.item_count
);
for (number, item) in (1..).zip(&items) {
println!(" [{number}] {item}");
}
if let Some(hex) = undecoded {
println!(" [raw] {hex}");
}
}
OutputFormat::Json => {
if let Some(hex) = undecoded {
items.push(format!("[raw: {hex}]"));
}
let json = serde_json::json!({
"object": heading.object,
"property": heading.property,
"item_count": heading.item_count,
"items": items,
});
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
}
}
}
#[cfg(test)]
#[path = "read_range_tests.rs"]
mod tests;