bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use bacnet_types::error::RemoteError;
use std::borrow::Cow;

use bacnet_services::alarm_summary::GetAlarmSummaryAck;
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::enums::NotifyType;

use super::*;

#[path = "alarm_summary_budget.rs"]
mod budget;

struct AlarmSummaryFixture {
    oid: ObjectIdentifier,
    name: String,
    advertised: Vec<PropertyIdentifier>,
    values: Vec<(PropertyIdentifier, PropertyValue)>,
}

impl AlarmSummaryFixture {
    fn alarm(instance: u32) -> Self {
        Self {
            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance).unwrap(),
            name: format!("ALARM-SUMMARY-{instance}"),
            advertised: vec![
                PropertyIdentifier::EVENT_STATE,
                PropertyIdentifier::NOTIFY_TYPE,
                PropertyIdentifier::ACKED_TRANSITIONS,
            ],
            values: vec![
                (
                    PropertyIdentifier::EVENT_STATE,
                    PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw()),
                ),
                (
                    PropertyIdentifier::NOTIFY_TYPE,
                    PropertyValue::Enumerated(NotifyType::ALARM.to_raw()),
                ),
                (
                    PropertyIdentifier::ACKED_TRANSITIONS,
                    transition_bits(EventTransitionBits::all()),
                ),
            ],
        }
    }

    fn advertise(&mut self, property: PropertyIdentifier) {
        if !self.advertised.contains(&property) {
            self.advertised.push(property);
        }
    }

    fn set(&mut self, property: PropertyIdentifier, value: PropertyValue) {
        self.values.retain(|(candidate, _)| *candidate != property);
        self.values.push((property, value));
    }

    fn remove(&mut self, property: PropertyIdentifier) {
        self.values.retain(|(candidate, _)| *candidate != property);
    }
}

impl BACnetObject for AlarmSummaryFixture {
    fn object_identifier(&self) -> ObjectIdentifier {
        self.oid
    }

    fn object_name(&self) -> &str {
        &self.name
    }

    fn read_property(
        &self,
        property: PropertyIdentifier,
        _array_index: Option<u32>,
    ) -> Result<PropertyValue, Error> {
        self.values
            .iter()
            .find(|(candidate, _)| *candidate == property)
            .map(|(_, value)| value.clone())
            .ok_or(Error::protocol(
                ErrorClass::PROPERTY,
                ErrorCode::UNKNOWN_PROPERTY,
            ))
    }

    fn write_property(
        &mut self,
        _property: PropertyIdentifier,
        _array_index: Option<u32>,
        _value: PropertyValue,
        _priority: Option<u8>,
    ) -> Result<(), Error> {
        Err(Error::protocol(
            ErrorClass::PROPERTY,
            ErrorCode::WRITE_ACCESS_DENIED,
        ))
    }

    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
        Cow::Owned(self.advertised.clone())
    }
}

fn transition_bits(bits: EventTransitionBits) -> PropertyValue {
    PropertyValue::BitString {
        unused_bits: 5,
        data: vec![bits.to_bacnet()],
    }
}

fn response(db: &ObjectDatabase) -> Result<GetAlarmSummaryAck, Error> {
    let mut encoded = BytesMut::new();
    let legacy = handle_get_alarm_summary(db, &mut encoded);
    let mut bounded = BytesMut::new();
    let result = handle_get_alarm_summary_budgeted(
        db,
        &mut bounded,
        crate::server::GetAlarmSummaryBudget::default(),
    );
    match (&legacy, &result) {
        (Ok(()), Ok(())) => assert_eq!(encoded, bounded),
        (Err(expected), Err(AlarmSummaryFailure::Service(actual))) => {
            assert_eq!(expected.to_string(), actual.to_string())
        }
        other => panic!("bounded/legacy projection drift: {other:?}"),
    }
    legacy?;
    GetAlarmSummaryAck::decode(&encoded)
}

fn assert_operational_problem(error: Error) {
    assert!(matches!(
        error,
        Error::Remote(RemoteError::Error { class, code })
            if class == ErrorClass::DEVICE
                && code == ErrorCode::OPERATIONAL_PROBLEM
    ));
}

fn add(db: &mut ObjectDatabase, object: AlarmSummaryFixture) {
    db.add(Box::new(object)).unwrap();
}

#[test]
fn selects_only_active_alarm_notify_type_and_preserves_output_fields() {
    let mut db = ObjectDatabase::new();
    let mut selected = AlarmSummaryFixture::alarm(1);
    selected.set(
        PropertyIdentifier::EVENT_STATE,
        PropertyValue::Enumerated(EventState::FAULT.to_raw()),
    );
    selected.set(
        PropertyIdentifier::ACKED_TRANSITIONS,
        transition_bits(EventTransitionBits::TO_FAULT),
    );
    add(&mut db, selected);

    let mut informational = AlarmSummaryFixture::alarm(2);
    informational.set(
        PropertyIdentifier::NOTIFY_TYPE,
        PropertyValue::Enumerated(NotifyType::EVENT.to_raw()),
    );
    add(&mut db, informational);

    let mut normal_alarm = AlarmSummaryFixture::alarm(3);
    normal_alarm.set(
        PropertyIdentifier::EVENT_STATE,
        PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
    );
    add(&mut db, normal_alarm);

    let ack = response(&db).unwrap();
    assert_eq!(ack.entries.len(), 1);
    assert_eq!(ack.entries[0].object_identifier.instance_number(), 1);
    assert_eq!(ack.entries[0].alarm_state, EventState::FAULT);
    assert_eq!(
        ack.entries[0].acknowledged_transitions,
        EventTransitionBits::TO_FAULT
    );
}

#[test]
fn detection_false_is_excluded_absence_is_eligible_and_missing_signature_is_skipped() {
    let mut db = ObjectDatabase::new();
    let mut disabled = AlarmSummaryFixture::alarm(1);
    disabled.advertise(PropertyIdentifier::EVENT_DETECTION_ENABLE);
    disabled.set(
        PropertyIdentifier::EVENT_DETECTION_ENABLE,
        PropertyValue::Boolean(false),
    );
    disabled.set(
        PropertyIdentifier::EVENT_STATE,
        PropertyValue::Boolean(false),
    );
    disabled.set(
        PropertyIdentifier::NOTIFY_TYPE,
        PropertyValue::Boolean(false),
    );
    disabled.set(
        PropertyIdentifier::ACKED_TRANSITIONS,
        PropertyValue::Boolean(false),
    );
    add(&mut db, disabled);

    add(&mut db, AlarmSummaryFixture::alarm(2));

    let mut not_event_initiating = AlarmSummaryFixture::alarm(3);
    not_event_initiating
        .advertised
        .retain(|property| *property != PropertyIdentifier::NOTIFY_TYPE);
    not_event_initiating.values.clear();
    add(&mut db, not_event_initiating);

    let ack = response(&db).unwrap();
    assert_eq!(ack.entries.len(), 1);
    assert_eq!(ack.entries[0].object_identifier.instance_number(), 2);
}

#[test]
fn excluded_state_and_notify_types_short_circuit_malformed_later_fields() {
    let mut db = ObjectDatabase::new();
    let mut normal = AlarmSummaryFixture::alarm(1);
    normal.set(
        PropertyIdentifier::EVENT_STATE,
        PropertyValue::Enumerated(EventState::NORMAL.to_raw()),
    );
    normal.set(
        PropertyIdentifier::NOTIFY_TYPE,
        PropertyValue::Boolean(false),
    );
    normal.set(
        PropertyIdentifier::ACKED_TRANSITIONS,
        PropertyValue::Boolean(false),
    );
    add(&mut db, normal);

    for (instance, notify_type) in [
        (2, NotifyType::EVENT.to_raw()),
        (3, NotifyType::ACK_NOTIFICATION.to_raw()),
        (4, 99),
    ] {
        let mut non_alarm = AlarmSummaryFixture::alarm(instance);
        non_alarm.set(
            PropertyIdentifier::NOTIFY_TYPE,
            PropertyValue::Enumerated(notify_type),
        );
        non_alarm.set(
            PropertyIdentifier::ACKED_TRANSITIONS,
            PropertyValue::Boolean(false),
        );
        add(&mut db, non_alarm);
    }

    let mut encoded = BytesMut::new();
    handle_get_alarm_summary(&db, &mut encoded).unwrap();
    assert!(encoded.is_empty());
    assert!(
        GetAlarmSummaryAck::decode(&encoded)
            .unwrap()
            .entries
            .is_empty()
    );
}

#[test]
fn unreadable_and_wrongly_typed_advertised_fields_are_operational_problems() {
    for (property, value) in [
        (
            PropertyIdentifier::EVENT_DETECTION_ENABLE,
            Some(PropertyValue::Unsigned(1)),
        ),
        (PropertyIdentifier::EVENT_DETECTION_ENABLE, None),
        (
            PropertyIdentifier::EVENT_STATE,
            Some(PropertyValue::Boolean(true)),
        ),
        (PropertyIdentifier::EVENT_STATE, None),
        (
            PropertyIdentifier::NOTIFY_TYPE,
            Some(PropertyValue::Boolean(true)),
        ),
        (PropertyIdentifier::NOTIFY_TYPE, None),
        (
            PropertyIdentifier::ACKED_TRANSITIONS,
            Some(PropertyValue::Boolean(true)),
        ),
        (PropertyIdentifier::ACKED_TRANSITIONS, None),
    ] {
        let mut object = AlarmSummaryFixture::alarm(1);
        if property == PropertyIdentifier::EVENT_DETECTION_ENABLE {
            object.advertise(property);
        }
        object.remove(property);
        if let Some(value) = value {
            object.set(property, value);
        }
        let mut db = ObjectDatabase::new();
        add(&mut db, object);
        assert_operational_problem(response(&db).unwrap_err());
    }
}

#[test]
fn acknowledged_transitions_requires_one_canonical_three_bit_octet() {
    for malformed in [
        PropertyValue::BitString {
            unused_bits: 4,
            data: vec![0b1110_0000],
        },
        PropertyValue::BitString {
            unused_bits: 5,
            data: vec![0b1110_0000, 0],
        },
        PropertyValue::BitString {
            unused_bits: 5,
            data: vec![0b1110_0001],
        },
    ] {
        let mut object = AlarmSummaryFixture::alarm(1);
        object.set(PropertyIdentifier::ACKED_TRANSITIONS, malformed);
        let mut db = ObjectDatabase::new();
        add(&mut db, object);
        assert_operational_problem(response(&db).unwrap_err());
    }
}