bacnet-server 0.15.0

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

use bacnet_objects::event::{EnrollmentSummaryCapability, EventTransition};
use bacnet_objects::notification_class::NotificationClass;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::enrollment_summary::{GetEnrollmentSummaryAck, GetEnrollmentSummaryRequest};
use bacnet_types::bitstring::{DaysOfWeek, EventTransitionBits};
use bacnet_types::constructed::{BACnetDestination, BACnetRecipient};
use bacnet_types::enums::{AcknowledgmentFilter, EventType};
use bacnet_types::primitives::Time;

use super::*;

pub(super) struct SummaryFixture {
    oid: ObjectIdentifier,
    name: String,
    advertised: Vec<PropertyIdentifier>,
    values: Vec<(PropertyIdentifier, PropertyValue)>,
    capability: Option<EnrollmentSummaryCapability>,
}

impl SummaryFixture {
    pub(super) fn candidate(
        instance: u32,
        event_type: EventType,
        event_state: EventState,
        acknowledged_transitions: EventTransitionBits,
        notification_class: u32,
        last_transition: Option<EventTransition>,
    ) -> Self {
        Self {
            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance).unwrap(),
            name: format!("SUMMARY-{instance}"),
            advertised: vec![
                PropertyIdentifier::EVENT_STATE,
                PropertyIdentifier::ACKED_TRANSITIONS,
                PropertyIdentifier::NOTIFICATION_CLASS,
            ],
            values: vec![
                (
                    PropertyIdentifier::EVENT_STATE,
                    PropertyValue::Enumerated(event_state.to_raw()),
                ),
                (
                    PropertyIdentifier::ACKED_TRANSITIONS,
                    transition_bits(acknowledged_transitions),
                ),
                (
                    PropertyIdentifier::NOTIFICATION_CLASS,
                    PropertyValue::Unsigned(notification_class as u64),
                ),
            ],
            capability: Some(EnrollmentSummaryCapability {
                event_type,
                last_transition,
            }),
        }
    }

    pub(super) fn notification_class(
        instance: u32,
        class_value: Option<PropertyValue>,
        priority: Option<PropertyValue>,
        recipient_list: Option<PropertyValue>,
    ) -> Self {
        let mut values = Vec::new();
        for (property, value) in [
            (PropertyIdentifier::NOTIFICATION_CLASS, class_value),
            (PropertyIdentifier::PRIORITY, priority),
            (PropertyIdentifier::RECIPIENT_LIST, recipient_list),
        ] {
            if let Some(value) = value {
                values.push((property, value));
            }
        }
        Self {
            oid: ObjectIdentifier::new(ObjectType::NOTIFICATION_CLASS, instance).unwrap(),
            name: format!("SUMMARY-NC-{instance}"),
            advertised: Vec::new(),
            values,
            capability: None,
        }
    }

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

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

    pub(super) fn without_capability(mut self) -> Self {
        self.capability = None;
        self
    }
}

impl BACnetObject for SummaryFixture {
    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 enrollment_summary_capability_internal(&self) -> Option<EnrollmentSummaryCapability> {
        self.capability
    }
}

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

pub(super) fn class(
    instance: u32,
    intrinsic_notification_class: u32,
    priority: [u8; 3],
    recipient_list: Vec<BACnetDestination>,
) -> NotificationClass {
    let mut class = NotificationClass::new(instance, format!("NC-{instance}")).unwrap();
    class.notification_class = intrinsic_notification_class;
    class.priority = priority;
    for destination in recipient_list {
        class.add_destination(destination).unwrap();
    }
    class
}

pub(super) fn destination(
    recipient: BACnetRecipient,
    process_identifier: u32,
) -> BACnetDestination {
    BACnetDestination {
        valid_days: DaysOfWeek::empty(),
        from_time: Time {
            hour: 23,
            minute: 0,
            second: 0,
            hundredths: 0,
        },
        to_time: Time {
            hour: 1,
            minute: 0,
            second: 0,
            hundredths: 0,
        },
        recipient,
        process_identifier,
        issue_confirmed_notifications: false,
        transitions: EventTransitionBits::empty(),
    }
}

pub(super) fn request() -> GetEnrollmentSummaryRequest {
    GetEnrollmentSummaryRequest {
        acknowledgment_filter: AcknowledgmentFilter::ALL,
        enrollment_filter: None,
        event_state_filter: None,
        event_type_filter: None,
        priority_filter: None,
        notification_class_filter: None,
    }
}

pub(super) fn response(
    db: &ObjectDatabase,
    request: &GetEnrollmentSummaryRequest,
) -> Result<GetEnrollmentSummaryAck, Error> {
    let mut encoded_request = BytesMut::new();
    request.encode(&mut encoded_request);
    let mut encoded_ack = BytesMut::new();
    let legacy = handle_get_enrollment_summary(db, &encoded_request, &mut encoded_ack);
    let mut bounded_ack = BytesMut::new();
    let bounded = handle_get_enrollment_summary_budgeted(
        db,
        &encoded_request,
        &mut bounded_ack,
        crate::server::GetEnrollmentSummaryBudget::default(),
    );
    match (&legacy, bounded) {
        (Ok(()), Ok(())) => assert_eq!(bounded_ack, encoded_ack),
        (Err(expected), Err(EnrollmentSummaryFailure::Service(actual))) => {
            assert_eq!(format!("{actual:?}"), format!("{expected:?}"))
        }
        (expected, actual) => panic!("legacy/budget parity: {expected:?} vs {actual:?}"),
    }
    legacy?;
    GetEnrollmentSummaryAck::decode(&encoded_ack)
}

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