bacnet-services 0.15.0

BACnet service request/response encode/decode per ASHRAE 135-2020
Documentation
use super::*;
use bacnet_encoding::constructed::{
    encode_audit_log_record, encode_event_log_record, encode_log_multiple_record, encode_log_record,
};
use bacnet_types::bitstring::LogStatus;
use bacnet_types::constructed::{BACnetAuditLogDatum, EventLogDatum, LogData, LogDatum, LogValue};
use bacnet_types::primitives::{Date, ObjectIdentifier, StatusFlags, Time};
use bytes::BytesMut;

const DATE: Date = Date {
    year: 126,
    month: 10,
    day: 5,
    day_of_week: 1,
};

fn time(second: u8) -> Time {
    Time {
        hour: 9,
        minute: 30,
        second,
        hundredths: 0,
    }
}

fn trend(value: f32) -> BACnetLogRecord {
    BACnetLogRecord {
        date: DATE,
        time: time(value as u8),
        log_datum: LogDatum::RealValue(value),
        status_flags: Some(StatusFlags::IN_ALARM),
    }
}

fn ack(object_type: ObjectType, item_count: u32, item_data: Vec<u8>) -> ReadRangeAck {
    ReadRangeAck {
        object_identifier: ObjectIdentifier::new(object_type, 1).unwrap(),
        property_identifier: PropertyIdentifier::LOG_BUFFER,
        property_array_index: None,
        result_flags: (true, true, false),
        item_count,
        item_data,
        first_sequence_number: None,
    }
}

fn trend_data(records: &[BACnetLogRecord]) -> Vec<u8> {
    let mut buf = BytesMut::new();
    for record in records {
        encode_log_record(record, &mut buf).unwrap();
    }
    buf.to_vec()
}

#[test]
fn trend_log_records_decode_every_record_in_order() {
    let records = vec![trend(1.0), trend(2.0), trend(3.0)];
    let ack = ack(ObjectType::TREND_LOG, 3, trend_data(&records));
    assert_eq!(ack.trend_log_records().unwrap(), records);
    assert_eq!(
        ack.log_records().unwrap().unwrap(),
        LogRecords::TrendLog(records)
    );
}

#[test]
fn empty_item_data_is_no_records() {
    let ack = ack(ObjectType::TREND_LOG, 0, Vec::new());
    assert!(ack.trend_log_records().unwrap().is_empty());
    assert!(ack.log_records().unwrap().unwrap().is_empty());
}

#[test]
fn a_record_that_fails_names_its_index_and_offset_and_keeps_the_ones_before() {
    let records = vec![trend(1.0), trend(2.0)];
    let mut data = trend_data(&records);
    let second = trend_data(&records[..1]).len();
    // Cut the second record's closing tag off: it no longer decodes.
    data.truncate(data.len() - 1);
    let ack = ack(ObjectType::TREND_LOG, 2, data);
    let error = ack.trend_log_records().unwrap_err();
    assert_eq!(error.index, 1);
    assert_eq!(error.offset, second);
    assert_eq!(error.decoded, records[..1]);
    match Error::from(error) {
        Error::Decoding(refused) => {
            assert_eq!(refused.offset, second);
            assert!(
                refused.message().to_string().starts_with("log record[1]: "),
                "{refused}"
            );
        }
        other => panic!("expected a decoding error, got {other:?}"),
    }
}

#[test]
fn item_count_must_match_the_records_in_the_data() {
    let records = vec![trend(1.0), trend(2.0)];
    let data = trend_data(&records);
    let first_len = trend_data(&records[..1]).len();

    let fewer = ack(ObjectType::TREND_LOG, 3, data.clone())
        .trend_log_records()
        .unwrap_err();
    assert_eq!((fewer.index, fewer.offset), (2, data.len()));
    assert_eq!(fewer.decoded, records);
    assert_eq!(
        fewer.error.decode_fault().map(|(_, kind)| kind),
        Some(bacnet_types::error::DecodingKind::Missing)
    );

    let more = ack(ObjectType::TREND_LOG, 1, data)
        .trend_log_records()
        .unwrap_err();
    assert_eq!((more.index, more.offset), (1, first_len));
    assert_eq!(more.decoded, records[..1]);
    assert_eq!(
        more.error.decode_fault().map(|(_, kind)| kind),
        Some(bacnet_types::error::DecodingKind::Trailing)
    );
}

#[test]
fn a_huge_item_count_does_not_reserve_memory_up_front() {
    let error = ack(ObjectType::TREND_LOG, u32::MAX, Vec::new())
        .trend_log_records()
        .unwrap_err();
    assert_eq!((error.index, error.offset), (0, 0));
}

#[test]
fn event_trend_multiple_and_audit_records_decode_by_object_type() {
    let event = BACnetEventLogRecord {
        date: DATE,
        time: time(1),
        log_datum: EventLogDatum::LogStatus(LogStatus::BUFFER_PURGED),
    };
    let mut buf = BytesMut::new();
    encode_event_log_record(&event, &mut buf).unwrap();
    let event_ack = ack(ObjectType::EVENT_LOG, 1, buf.to_vec());
    assert_eq!(event_ack.event_log_records().unwrap(), vec![event.clone()]);
    assert_eq!(
        event_ack.log_records().unwrap().unwrap(),
        LogRecords::EventLog(vec![event])
    );

    let multiple = BACnetLogMultipleRecord {
        date: DATE,
        time: time(2),
        log_data: LogData::Values(vec![LogValue::RealValue(1.5), LogValue::NullValue]),
    };
    let mut buf = BytesMut::new();
    encode_log_multiple_record(&multiple, &mut buf).unwrap();
    let multiple_ack = ack(ObjectType::TREND_LOG_MULTIPLE, 1, buf.to_vec());
    assert_eq!(
        multiple_ack.trend_log_multiple_records().unwrap(),
        vec![multiple.clone()]
    );
    assert_eq!(
        multiple_ack.log_records().unwrap().unwrap(),
        LogRecords::TrendLogMultiple(vec![multiple])
    );

    let audit = BACnetAuditLogRecord {
        timestamp: (DATE, time(3)),
        datum: BACnetAuditLogDatum::TimeChange(-2.0),
    };
    let mut buf = BytesMut::new();
    encode_audit_log_record(&audit, &mut buf).unwrap();
    encode_audit_log_record(&audit, &mut buf).unwrap();
    let audit_ack = ack(ObjectType::AUDIT_LOG, 2, buf.to_vec());
    assert_eq!(
        audit_ack.audit_log_records().unwrap(),
        vec![audit.clone(), audit.clone()]
    );
    assert_eq!(
        audit_ack.log_records().unwrap().unwrap(),
        LogRecords::AuditLog(vec![audit.clone(), audit])
    );
}

#[test]
fn log_records_is_none_off_a_log_buffer() {
    let data = trend_data(&[trend(1.0)]);
    let mut other_property = ack(ObjectType::TREND_LOG, 1, data.clone());
    other_property.property_identifier = PropertyIdentifier::EVENT_TIME_STAMPS;
    assert!(other_property.log_records().is_none());
    assert!(ack(ObjectType::ANALOG_INPUT, 1, data)
        .log_records()
        .is_none());
}

#[test]
fn a_failure_in_dynamic_decoding_keeps_the_decoded_records_typed() {
    let records = vec![trend(1.0)];
    let mut data = trend_data(&records);
    data.push(0xFF);
    let error = ack(ObjectType::TREND_LOG, 2, data)
        .log_records()
        .unwrap()
        .unwrap_err();
    assert_eq!(error.decoded, LogRecords::TrendLog(records));
    assert_eq!(error.index, 1);
}

#[test]
fn empty_for_names_each_log_kind() {
    assert_eq!(
        LogRecords::empty_for(ObjectType::AUDIT_LOG),
        Some(LogRecords::AuditLog(Vec::new()))
    );
    assert_eq!(LogRecords::empty_for(ObjectType::DEVICE), None);
}