bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use bacnet_objects::access_control::{AccessDoorObject, AccessZoneObject};
use bacnet_objects::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
use bacnet_objects::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::event_enrollment::{AlertEnrollmentObject, EventEnrollmentObject};
use bacnet_objects::multistate::{
    MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject,
};
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::{
    EngineeringUnits, ErrorClass, ErrorCode, EventType, ObjectType, PropertyIdentifier,
};
use bacnet_types::error::{Error, RemoteError};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};

use super::{PicsConfig, PicsGenerator};
use crate::server::ServerConfig;

#[test]
fn acked_transitions_network_policy_is_uniform_on_all_supported_types() {
    let alert_source = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
    let cases: Vec<(&str, ObjectType, Box<dyn BACnetObject>)> = vec![
        (
            "Analog Input",
            ObjectType::ANALOG_INPUT,
            Box::new(AnalogInputObject::new(1, "AI-1", EngineeringUnits::NO_UNITS).unwrap()),
        ),
        (
            "Analog Output",
            ObjectType::ANALOG_OUTPUT,
            Box::new(AnalogOutputObject::new(1, "AO-1", EngineeringUnits::NO_UNITS).unwrap()),
        ),
        (
            "Analog Value",
            ObjectType::ANALOG_VALUE,
            Box::new(AnalogValueObject::new(1, "AV-1", EngineeringUnits::NO_UNITS).unwrap()),
        ),
        (
            "Binary Input",
            ObjectType::BINARY_INPUT,
            Box::new(BinaryInputObject::new(1, "BI-1").unwrap()),
        ),
        (
            "Binary Output",
            ObjectType::BINARY_OUTPUT,
            Box::new(BinaryOutputObject::new(1, "BO-1").unwrap()),
        ),
        (
            "Binary Value",
            ObjectType::BINARY_VALUE,
            Box::new(BinaryValueObject::new(1, "BV-1").unwrap()),
        ),
        (
            "Multi-state Input",
            ObjectType::MULTI_STATE_INPUT,
            Box::new(MultiStateInputObject::new(1, "MSI-1", 2).unwrap()),
        ),
        (
            "Multi-state Output",
            ObjectType::MULTI_STATE_OUTPUT,
            Box::new(MultiStateOutputObject::new(1, "MSO-1", 2).unwrap()),
        ),
        (
            "Multi-state Value",
            ObjectType::MULTI_STATE_VALUE,
            Box::new(MultiStateValueObject::new(1, "MSV-1", 2).unwrap()),
        ),
        (
            "Access Zone",
            ObjectType::ACCESS_ZONE,
            Box::new(AccessZoneObject::new(1, "ZONE-1").unwrap()),
        ),
        (
            "Access Door",
            ObjectType::ACCESS_DOOR,
            Box::new(AccessDoorObject::new(1, "DOOR-1").unwrap()),
        ),
        (
            "Event Enrollment",
            ObjectType::EVENT_ENROLLMENT,
            Box::new(
                EventEnrollmentObject::new(1, "EE-1", EventType::CHANGE_OF_BITSTRING).unwrap(),
            ),
        ),
        (
            "Alert Enrollment",
            ObjectType::ALERT_ENROLLMENT,
            Box::new(AlertEnrollmentObject::new(1, "AE-1", alert_source).unwrap()),
        ),
    ];

    let mut database = ObjectDatabase::new();
    let mut object_types = Vec::with_capacity(cases.len());
    for (label, object_type, mut object) in cases {
        let property_list = object
            .read_property(PropertyIdentifier::PROPERTY_LIST, None)
            .unwrap_or_else(|error| panic!("{label}: Property_List read failed: {error:?}"));
        assert!(
            matches!(
                property_list,
                PropertyValue::List(ref properties)
                    if properties.contains(&PropertyValue::Enumerated(
                        PropertyIdentifier::ACKED_TRANSITIONS.to_raw()
                    ))
            ),
            "{label}: Property_List must expose Acked_Transitions"
        );

        let before = object
            .read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
            .unwrap_or_else(|error| panic!("{label}: Acked_Transitions read failed: {error:?}"));
        assert!(
            matches!(
                &before,
                PropertyValue::BitString { unused_bits: 5, data } if data.len() == 1
            ),
            "{label}: Acked_Transitions must be a three-bit BitString, got {before:?}"
        );
        assert!(
            !object.is_writable_property(PropertyIdentifier::ACKED_TRANSITIONS),
            "{label}: Acked_Transitions must not be advertised writable"
        );

        let error = object
            .write_property(
                PropertyIdentifier::ACKED_TRANSITIONS,
                None,
                PropertyValue::BitString {
                    unused_bits: 5,
                    data: vec![0],
                },
                None,
            )
            .expect_err("a network Acked_Transitions write must be denied");
        assert!(
            matches!(
                error,
                Error::Remote(RemoteError::Error { class, code })
                    if class == ErrorClass::PROPERTY
                        && code == ErrorCode::WRITE_ACCESS_DENIED
            ),
            "{label}: expected PROPERTY / WRITE_ACCESS_DENIED, got {error:?}"
        );
        assert_eq!(
            object
                .read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
                .unwrap(),
            before,
            "{label}: denied write must not change Acked_Transitions"
        );

        object_types.push((label, object_type));
        database.add(object).unwrap();
    }

    let pics =
        PicsGenerator::new(&database, &ServerConfig::default(), &PicsConfig::default()).generate();
    for (label, object_type) in object_types {
        let property =
            pics.supported_object_types
                .iter()
                .find(|support| support.object_type == object_type)
                .and_then(|support| {
                    support.supported_properties.iter().find(|property| {
                        property.property_id == PropertyIdentifier::ACKED_TRANSITIONS
                    })
                })
                .unwrap_or_else(|| panic!("{label}: PICS must include Acked_Transitions"));
        assert!(property.access.readable, "{label}: PICS must mark readable");
        assert!(
            !property.access.writable,
            "{label}: PICS must not mark writable"
        );
    }
}