bacnet-server 0.11.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_types::enums::{ErrorClass, ErrorCode};
use std::borrow::Cow;

struct BuiltInProjectionObject {
    oid: ObjectIdentifier,
    present_value: PropertyValue,
    feedback_value: Option<PropertyValue>,
    reliability: PropertyValue,
    status_flags: PropertyValue,
}

impl BuiltInProjectionObject {
    fn new(
        instance: u32,
        object_type: ObjectType,
        present_value: PropertyValue,
        feedback_value: Option<PropertyValue>,
    ) -> Self {
        Self {
            oid: ObjectIdentifier::new(object_type, instance).unwrap(),
            present_value,
            feedback_value,
            reliability: PropertyValue::Enumerated(2),
            status_flags: PropertyValue::BitString {
                unused_bits: 4,
                data: vec![0xc0],
            },
        }
    }
}

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

    fn object_name(&self) -> &str {
        "projection-source"
    }

    fn read_property(
        &self,
        property: PropertyIdentifier,
        _array_index: Option<u32>,
    ) -> Result<PropertyValue, bacnet_types::error::Error> {
        match property {
            p if p == PropertyIdentifier::PRESENT_VALUE => Ok(self.present_value.clone()),
            p if p == PropertyIdentifier::FEEDBACK_VALUE => {
                self.feedback_value
                    .clone()
                    .ok_or(bacnet_types::error::Error::Protocol {
                        class: ErrorClass::PROPERTY.to_raw() as u32,
                        code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
                    })
            }
            p if p == PropertyIdentifier::RELIABILITY => Ok(self.reliability.clone()),
            p if p == PropertyIdentifier::STATUS_FLAGS => Ok(self.status_flags.clone()),
            p if p == PropertyIdentifier::HIGH_LIMIT => Ok(PropertyValue::Real(80.0)),
            p if p == PropertyIdentifier::LOW_LIMIT => Ok(PropertyValue::Real(20.0)),
            p if p == PropertyIdentifier::DEADBAND => Ok(PropertyValue::Real(2.0)),
            _ => Err(bacnet_types::error::Error::Protocol {
                class: ErrorClass::PROPERTY.to_raw() as u32,
                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
            }),
        }
    }

    fn write_property(
        &mut self,
        _property: PropertyIdentifier,
        _array_index: Option<u32>,
        _value: PropertyValue,
        _priority: Option<u8>,
    ) -> Result<(), bacnet_types::error::Error> {
        Err(bacnet_types::error::Error::Protocol {
            class: ErrorClass::PROPERTY.to_raw() as u32,
            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
        })
    }

    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
        Cow::Borrowed(&[])
    }
}

fn normal_payload(object: &BuiltInProjectionObject) -> NotificationParameters {
    let (event_type, to) = match object.oid.object_type() {
        ObjectType::ANALOG_INPUT | ObjectType::ANALOG_OUTPUT | ObjectType::ANALOG_VALUE => {
            (EventType::OUT_OF_RANGE, EventState::HIGH_LIMIT)
        }
        ObjectType::BINARY_INPUT
        | ObjectType::BINARY_VALUE
        | ObjectType::MULTI_STATE_INPUT
        | ObjectType::MULTI_STATE_VALUE => (EventType::CHANGE_OF_STATE, EventState::OFFNORMAL),
        ObjectType::BINARY_OUTPUT | ObjectType::MULTI_STATE_OUTPUT => {
            (EventType::COMMAND_FAILURE, EventState::OFFNORMAL)
        }
        other => panic!("unexpected built-in type {other:?}"),
    };
    project_intrinsic_payload(
        object,
        &EventStateChange {
            from: EventState::NORMAL,
            to,
        },
        event_type,
    )
    .unwrap()
    .0
}

fn all_nine_sources() -> Vec<(BuiltInProjectionObject, NotificationParameters)> {
    vec![
        (
            BuiltInProjectionObject::new(
                1,
                ObjectType::ANALOG_INPUT,
                PropertyValue::Real(85.0),
                None,
            ),
            NotificationParameters::OutOfRange {
                exceeding_value: 85.0,
                status_flags: 0b1100,
                deadband: 2.0,
                exceeded_limit: 80.0,
            },
        ),
        (
            BuiltInProjectionObject::new(
                2,
                ObjectType::ANALOG_OUTPUT,
                PropertyValue::Real(85.0),
                None,
            ),
            NotificationParameters::OutOfRange {
                exceeding_value: 85.0,
                status_flags: 0b1100,
                deadband: 2.0,
                exceeded_limit: 80.0,
            },
        ),
        (
            BuiltInProjectionObject::new(
                3,
                ObjectType::ANALOG_VALUE,
                PropertyValue::Real(85.0),
                None,
            ),
            NotificationParameters::OutOfRange {
                exceeding_value: 85.0,
                status_flags: 0b1100,
                deadband: 2.0,
                exceeded_limit: 80.0,
            },
        ),
        (
            BuiltInProjectionObject::new(
                4,
                ObjectType::BINARY_INPUT,
                PropertyValue::Enumerated(1),
                None,
            ),
            NotificationParameters::ChangeOfState {
                new_state: BACnetPropertyStates::BinaryValue(1),
                status_flags: 0b1100,
            },
        ),
        (
            BuiltInProjectionObject::new(
                5,
                ObjectType::BINARY_VALUE,
                PropertyValue::Enumerated(1),
                None,
            ),
            NotificationParameters::ChangeOfState {
                new_state: BACnetPropertyStates::BinaryValue(1),
                status_flags: 0b1100,
            },
        ),
        (
            BuiltInProjectionObject::new(
                6,
                ObjectType::MULTI_STATE_INPUT,
                PropertyValue::Unsigned(3),
                None,
            ),
            NotificationParameters::ChangeOfState {
                new_state: BACnetPropertyStates::UnsignedValue(3),
                status_flags: 0b1100,
            },
        ),
        (
            BuiltInProjectionObject::new(
                7,
                ObjectType::MULTI_STATE_VALUE,
                PropertyValue::Unsigned(3),
                None,
            ),
            NotificationParameters::ChangeOfState {
                new_state: BACnetPropertyStates::UnsignedValue(3),
                status_flags: 0b1100,
            },
        ),
        (
            BuiltInProjectionObject::new(
                8,
                ObjectType::BINARY_OUTPUT,
                PropertyValue::Enumerated(1),
                Some(PropertyValue::Enumerated(0)),
            ),
            NotificationParameters::CommandFailure {
                command_value: vec![0x91, 0x01],
                status_flags: 0b1100,
                feedback_value: vec![0x91, 0x00],
            },
        ),
        (
            BuiltInProjectionObject::new(
                9,
                ObjectType::MULTI_STATE_OUTPUT,
                PropertyValue::Unsigned(3),
                Some(PropertyValue::Unsigned(2)),
            ),
            NotificationParameters::CommandFailure {
                command_value: vec![0x21, 0x03],
                status_flags: 0b1100,
                feedback_value: vec![0x21, 0x02],
            },
        ),
    ]
}

#[test]
fn all_nine_builtin_normal_families_project_exact_typed_values() {
    for (source, expected) in all_nine_sources() {
        assert_eq!(normal_payload(&source), expected, "source {}", source.oid);
    }
}

#[test]
fn builtin_fault_projection_is_tag_19_with_explicit_property_order() {
    for (source, _) in all_nine_sources() {
        let payload = project_intrinsic_payload(
            &source,
            &EventStateChange {
                from: EventState::NORMAL,
                to: EventState::FAULT,
            },
            EventType::CHANGE_OF_RELIABILITY,
        )
        .unwrap()
        .0;
        let NotificationParameters::ChangeOfReliability {
            reliability,
            status_flags,
            property_values,
        } = payload
        else {
            panic!("{} did not project CHANGE_OF_RELIABILITY", source.oid);
        };
        assert_eq!(reliability, 2);
        assert_eq!(status_flags, 0b1100);

        let mut decoded = Vec::new();
        let mut offset = 0;
        while offset < property_values.len() {
            let (entry, next) = BACnetPropertyValue::decode(&property_values, offset).unwrap();
            assert!(next > offset);
            decoded.push(entry);
            offset = next;
        }
        let expected_properties = if matches!(
            source.oid.object_type(),
            ObjectType::BINARY_OUTPUT | ObjectType::MULTI_STATE_OUTPUT
        ) {
            vec![
                PropertyIdentifier::PRESENT_VALUE,
                PropertyIdentifier::FEEDBACK_VALUE,
            ]
        } else {
            vec![PropertyIdentifier::PRESENT_VALUE]
        };
        assert_eq!(
            decoded
                .iter()
                .map(|entry| entry.property_identifier)
                .collect::<Vec<_>>(),
            expected_properties,
            "{} property order",
            source.oid
        );
        assert_eq!(
            decoded[0].value,
            encode_abstract_value(&source.present_value).unwrap()
        );
        if let Some(feedback) = &source.feedback_value {
            assert_eq!(decoded[1].value, encode_abstract_value(feedback).unwrap());
        }
    }
}

#[test]
fn fault_recovery_requires_effective_reliability_type_and_none_is_not_projected() {
    let source =
        BuiltInProjectionObject::new(1, ObjectType::ANALOG_INPUT, PropertyValue::Real(50.0), None);
    let recovery = EventStateChange {
        from: EventState::FAULT,
        to: EventState::NORMAL,
    };
    assert!(
        project_intrinsic_payload(&source, &recovery, EventType::CHANGE_OF_RELIABILITY).is_some()
    );
    assert!(project_intrinsic_payload(&source, &recovery, EventType::OUT_OF_RANGE).is_none());
    assert!(project_intrinsic_payload(
        &source,
        &EventStateChange {
            from: EventState::NORMAL,
            to: EventState::HIGH_LIMIT,
        },
        EventType::NONE,
    )
    .is_none());
}

#[test]
fn malformed_required_builtin_data_fails_closed() {
    let mut source =
        BuiltInProjectionObject::new(1, ObjectType::ANALOG_INPUT, PropertyValue::Real(85.0), None);
    source.status_flags = PropertyValue::Unsigned(0);
    assert!(project_intrinsic_payload(
        &source,
        &EventStateChange {
            from: EventState::NORMAL,
            to: EventState::HIGH_LIMIT,
        },
        EventType::OUT_OF_RANGE,
    )
    .is_none());
}

#[test]
fn limit_selection_covers_entries_crossings_and_normal_recovery() {
    let low = 20.0;
    let high = 80.0;
    for (from, to, expected) in [
        (EventState::NORMAL, EventState::LOW_LIMIT, low),
        (EventState::HIGH_LIMIT, EventState::LOW_LIMIT, low),
        (EventState::LOW_LIMIT, EventState::NORMAL, low),
        (EventState::NORMAL, EventState::HIGH_LIMIT, high),
        (EventState::LOW_LIMIT, EventState::HIGH_LIMIT, high),
        (EventState::HIGH_LIMIT, EventState::NORMAL, high),
    ] {
        assert_eq!(
            selected_limit(&EventStateChange { from, to }, low, high),
            Some(expected)
        );
    }
}