bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use super::*;
use bacnet_objects::event::EventStateChange;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::{EventState, EventType};
use bacnet_types::primitives::BACnetTimeStamp;

struct UnsupportedFaultReindication {
    oid: ObjectIdentifier,
}

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

    fn object_name(&self) -> &str {
        "unsupported-fault"
    }

    fn read_property(
        &self,
        property: PropertyIdentifier,
        _array_index: Option<u32>,
    ) -> Result<PropertyValue, Error> {
        if property == PropertyIdentifier::NOTIFICATION_CLASS {
            Ok(PropertyValue::Unsigned(0))
        } else {
            Err(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) -> std::borrow::Cow<'static, [PropertyIdentifier]> {
        std::borrow::Cow::Borrowed(&[PropertyIdentifier::NOTIFICATION_CLASS])
    }

    fn evaluate_intrinsic_reporting(&mut self) -> Option<bacnet_objects::event::TransitionOutcome> {
        Some(bacnet_objects::event::TransitionOutcome {
            change: EventStateChange {
                from: EventState::FAULT,
                to: EventState::FAULT,
            },
            event_type: EventType::CHANGE_OF_RELIABILITY,
            distribute: true,
        })
    }
}

#[tokio::test]
async fn no_recipient_transition_commits_locally_without_sending() {
    let db = db_with_high_limit_transition(0x80);
    {
        let mut guard = db.write().await;
        guard
            .add(Box::new(
                bacnet_objects::notification_class::NotificationClass::new(0, "NC-0").unwrap(),
            ))
            .unwrap();
    }

    let sent = broadcasts_from_per_write_path(&db, DccState::Enable).await;
    assert!(sent.is_empty());
    let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
    assert_eq!(
        db.read()
            .await
            .get(&oid)
            .unwrap()
            .read_property(PropertyIdentifier::EVENT_STATE, None)
            .unwrap(),
        PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw())
    );
}

#[test]
fn unsupported_fault_reindication_is_retryable_without_sequence_consumption() {
    let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
    let mut db = ObjectDatabase::new();
    db.add(Box::new(UnsupportedFaultReindication { oid }))
        .unwrap();

    let proposal = db
        .get_mut(&oid)
        .unwrap()
        .evaluate_intrinsic_reporting()
        .unwrap();
    assert!(BACnetServer::<TestTransport>::commit_intrinsic_transition(
        &mut db,
        &oid,
        proposal.clone(),
    )
    .is_none());
    assert_eq!(db.reserve_event_sequence_number().number(), 0);
    assert_eq!(
        db.get_mut(&oid).unwrap().evaluate_intrinsic_reporting(),
        Some(proposal)
    );
}

#[tokio::test]
async fn dcc_suppressed_intrinsic_transition_commits_locally_without_sending() {
    let db = db_with_high_limit_transition(0x80);
    let sent = broadcasts_from_per_write_path(&db, DccState::DisableInitiation).await;

    assert!(sent.is_empty(), "DCC must suppress external distribution");
    let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
    assert_eq!(
        db.read()
            .await
            .get(&oid)
            .unwrap()
            .read_property(PropertyIdentifier::EVENT_STATE, None)
            .unwrap(),
        PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw()),
        "DCC must not suppress the local transition commit"
    );
}

#[tokio::test]
async fn clockless_intrinsic_commit_stores_and_sends_one_reserved_sequence() {
    let db = db_with_high_limit_transition(0x80);
    {
        let mut guard = db.write().await;
        guard.set_clock_reader(None);
        let mut notification_class = notification_class_0_broadcasting();
        notification_class.ack_required = EventTransitionBits::TO_OFFNORMAL;
        guard.add(Box::new(notification_class)).unwrap();
    }

    let sent = broadcasts_from_per_write_path(&db, DccState::Enable).await;
    let notification = decode_broadcast_notification(&sent);
    assert_eq!(
        notification.timestamp,
        BACnetTimeStamp::SequenceNumber(0),
        "the outbound request must reuse the timestamp committed under the lock"
    );

    let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
    let mut encoded = BytesMut::new();
    bacnet_encoding::primitives::encode_timestamp_choice(
        &mut encoded,
        &BACnetTimeStamp::SequenceNumber(0),
    )
    .unwrap();
    let mut guard = db.write().await;
    let object = guard.get(&oid).unwrap();
    assert_eq!(
        object
            .read_property(PropertyIdentifier::EVENT_TIME_STAMPS, Some(1))
            .unwrap(),
        PropertyValue::ApplicationData(encoded.to_vec())
    );
    assert_eq!(
        object
            .read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
            .unwrap(),
        PropertyValue::BitString {
            unused_bits: 5,
            data: vec![0x60],
        },
        "Ack_Required clears only TO_OFFNORMAL"
    );
    assert_eq!(guard.reserve_event_sequence_number().number(), 1);

    let mut replacement = notification_class_0_broadcasting();
    replacement.ack_required = EventTransitionBits::empty();
    guard.add(Box::new(replacement)).unwrap();
    assert_eq!(
        guard
            .get(&oid)
            .unwrap()
            .read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
            .unwrap(),
        PropertyValue::BitString {
            unused_bits: 5,
            data: vec![0x60],
        },
        "later Notification Class edits must not rewrite committed acknowledgment state"
    );
}

#[tokio::test]
async fn committed_ack_required_snapshot_survives_notification_class_replacement() {
    let db = db_with_high_limit_transition(0x80);
    let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
    let committed = {
        let mut guard = db.write().await;
        let mut notification_class = notification_class_0_broadcasting();
        notification_class.ack_required = EventTransitionBits::TO_OFFNORMAL;
        guard.add(Box::new(notification_class)).unwrap();
        let outcome = guard
            .get_mut(&oid)
            .unwrap()
            .evaluate_intrinsic_reporting()
            .unwrap();
        BACnetServer::<TestTransport>::commit_intrinsic_transition(&mut guard, &oid, outcome)
            .unwrap()
    };

    {
        let mut guard = db.write().await;
        let mut replacement = notification_class_0_broadcasting();
        replacement.ack_required = EventTransitionBits::empty();
        guard.add(Box::new(replacement)).unwrap();
        assert_eq!(
            guard
                .get(&oid)
                .unwrap()
                .read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
                .unwrap(),
            PropertyValue::BitString {
                unused_bits: 5,
                data: vec![0x60],
            },
            "the committed TO_OFFNORMAL acknowledgment bit stays cleared"
        );
    }

    let (transport, sent) = recording_transport();
    let network = Arc::new(NetworkLayer::new(transport));
    BACnetServer::<TestTransport>::build_and_send_event_notification_with_bindings(
        &crate::server::event_delivery::EventDelivery {
            db: &db,
            network: &network,
            comm_state: &Arc::new(CommState::default()),
            learned_routers: &Arc::new(Mutex::new(LearnedRouterCache::new())),
            notification_transactions: &NotificationTransactions::new(),
            device_bindings: &Arc::new(RwLock::new(
                crate::server::device_bindings::DeviceBindingTable::new(),
            )),
            suppressions: &Default::default(),
            retry_timeout_ms: 1000,
            local_apdu_capacity: 1476,
        },
        &oid,
        committed,
    )
    .await;

    assert!(
        decode_broadcast_notification(&sent.npdus()).ack_required,
        "wire Ack_Required must use the commit-time snapshot"
    );
}

#[path = "event_notifications_custom_tests.rs"]
mod custom;