bacnet-server 0.15.0

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

use bacnet_encoding::primitives::encode_timestamp_choice;
use bacnet_objects::notification_class::NotificationClass;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::alarm_event::GetEventInformationRequest;
use bacnet_types::enums::EventState;
use bacnet_types::primitives::{BACnetTimeStamp, Date, Time};

use super::*;

const INVOKE_ID: u8 = 77;

struct LargeSummaryObject {
    oid: ObjectIdentifier,
    timestamps: Vec<PropertyValue>,
}

impl LargeSummaryObject {
    fn new() -> Self {
        let timestamps = (1..=3)
            .map(|hour| {
                let timestamp = BACnetTimeStamp::DateTime {
                    date: Date {
                        year: 126,
                        month: 9,
                        day: hour,
                        day_of_week: hour,
                    },
                    time: Time {
                        hour,
                        minute: 2,
                        second: 3,
                        hundredths: 4,
                    },
                };
                let mut encoded = BytesMut::new();
                encode_timestamp_choice(&mut encoded, &timestamp).unwrap();
                PropertyValue::ApplicationData(encoded.to_vec())
            })
            .collect();
        Self {
            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
            timestamps,
        }
    }
}

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

    fn object_name(&self) -> &str {
        "LARGE-SUMMARY"
    }

    fn read_property(
        &self,
        property: PropertyIdentifier,
        _array_index: Option<u32>,
    ) -> Result<PropertyValue, Error> {
        match property {
            p if p == PropertyIdentifier::EVENT_STATE => {
                Ok(PropertyValue::Enumerated(EventState::HIGH_LIMIT.to_raw()))
            }
            p if p == PropertyIdentifier::ACKED_TRANSITIONS
                || p == PropertyIdentifier::EVENT_ENABLE =>
            {
                Ok(PropertyValue::BitString {
                    unused_bits: 5,
                    data: vec![0xe0],
                })
            }
            p if p == PropertyIdentifier::EVENT_TIME_STAMPS => {
                Ok(PropertyValue::List(self.timestamps.clone()))
            }
            p if p == PropertyIdentifier::NOTIFY_TYPE => Ok(PropertyValue::Enumerated(0)),
            p if p == PropertyIdentifier::NOTIFICATION_CLASS => Ok(PropertyValue::Unsigned(42)),
            _ => 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) -> Cow<'static, [PropertyIdentifier]> {
        Cow::Borrowed(&[
            PropertyIdentifier::EVENT_STATE,
            PropertyIdentifier::ACKED_TRANSITIONS,
            PropertyIdentifier::EVENT_TIME_STAMPS,
            PropertyIdentifier::NOTIFY_TYPE,
            PropertyIdentifier::EVENT_ENABLE,
            PropertyIdentifier::NOTIFICATION_CLASS,
        ])
    }
}

fn database() -> ObjectDatabase {
    let mut db = ObjectDatabase::new();
    db.add(Box::new(LargeSummaryObject::new())).unwrap();
    let mut class = NotificationClass::new(99, "NC-99").unwrap();
    class.notification_class = 42;
    class.priority = [1, 100, 255];
    db.add(Box::new(class)).unwrap();
    db
}

fn service_request() -> Bytes {
    let mut encoded = BytesMut::new();
    GetEventInformationRequest {
        last_received_object_identifier: None,
    }
    .encode(&mut encoded);
    encoded.freeze()
}

fn full_service_ack() -> Bytes {
    let mut encoded = BytesMut::new();
    handlers::handle_get_event_information(&database(), &service_request(), &mut encoded).unwrap();
    encoded.freeze()
}

fn unsegmented_apdu_len(service_ack: Bytes) -> usize {
    let mut encoded = BytesMut::new();
    encode_apdu(
        &mut encoded,
        &Apdu::ComplexAck(ComplexAck {
            segmented: false,
            more_follows: false,
            invoke_id: INVOKE_ID,
            sequence_number: None,
            proposed_window_size: None,
            service_choice: ConfirmedServiceChoice::GET_EVENT_INFORMATION,
            service_ack,
        }),
    )
    .unwrap();
    encoded.len()
}

async fn dispatch(
    segmentation: Segmentation,
    client_accepts_segmented: bool,
    client_max_apdu: u16,
    local_max_apdu: u32,
) -> (
    SendLog,
    Arc<segmented_send::SegmentedSendRegistry>,
    MacAddr,
    Arc<crate::server::request_tasks::RequestTasks>,
) {
    dispatch_with_budget(
        segmentation,
        client_accepts_segmented,
        client_max_apdu,
        local_max_apdu,
        GetEventInformationBudget::default(),
    )
    .await
}

async fn dispatch_with_budget(
    segmentation: Segmentation,
    client_accepts_segmented: bool,
    client_max_apdu: u16,
    local_max_apdu: u32,
    budget: GetEventInformationBudget,
) -> (
    SendLog,
    Arc<segmented_send::SegmentedSendRegistry>,
    MacAddr,
    Arc<crate::server::request_tasks::RequestTasks>,
) {
    let (network, sent) = recording_network();
    let db = Arc::new(RwLock::new(database()));
    let seg_ack_senders = Arc::new(segmented_send::SegmentedSendRegistry::default());
    let confirmed_request_tracker = Arc::new(ConfirmedRequestTracker::default());
    let config = ServerConfig {
        device: bacnet_objects::DeviceIdentity::default()
            .with_max_apdu(u16::try_from(local_max_apdu).expect("Device APDU fits u16"))
            .expect("valid Device APDU")
            .with_segmentation(segmentation),
        get_event_information_budget: budget,
        ..ServerConfig::default()
    };
    let source_mac = test_mac(41);
    let request_tasks = Arc::new(crate::server::request_tasks::RequestTasks::default());

    BACnetServer::<TestTransport>::handle_confirmed_request(
        &RequestServices {
            db: Arc::clone(&db),
            seg_ack_senders: Arc::clone(&seg_ack_senders),
            ..RequestServices::for_test(Arc::clone(&network), config.clone())
        },
        &confirmed_request_tracker,
        &request_tasks.spawner(),
        source_mac.as_slice(),
        None,
        ConfirmedRequestPdu {
            segmented: false,
            more_follows: false,
            segmented_response_accepted: client_accepts_segmented,
            max_segments: None,
            max_apdu_length: client_max_apdu,
            invoke_id: INVOKE_ID,
            sequence_number: None,
            proposed_window_size: None,
            service_choice: ConfirmedServiceChoice::GET_EVENT_INFORMATION,
            service_request: service_request(),
        },
        None,
    )
    .await;

    (sent, seg_ack_senders, source_mac, request_tasks)
}

#[tokio::test]
async fn first_summary_over_unsegmented_budget_uses_buffer_overflow_abort() {
    assert!(unsegmented_apdu_len(full_service_ack()) > 50);
    for (client_max_apdu, local_max_apdu) in [(50, 1476), (480, 50)] {
        let (sent, _, _, _owner) =
            dispatch(Segmentation::NONE, false, client_max_apdu, local_max_apdu).await;
        wait_for_sent_len(&sent, 1).await;
        assert_eq!(sent_count(&sent), 1);
        assert_eq!(abort_reason(&sent, 0), AbortReason::BUFFER_OVERFLOW);
    }
}

#[tokio::test]
async fn get_event_information_configured_abort_independent_of_segmentation() {
    for segmentation in [Segmentation::NONE, Segmentation::BOTH] {
        for (budget, expected) in [
            (
                GetEventInformationBudget {
                    max_objects: 1,
                    ..Default::default()
                },
                AbortReason::OUT_OF_RESOURCES,
            ),
            (
                GetEventInformationBudget {
                    max_service_ack_bytes: 4,
                    ..Default::default()
                },
                AbortReason::BUFFER_OVERFLOW,
            ),
        ] {
            let (sent, _, _, owner) =
                dispatch_with_budget(segmentation, true, 50, 1476, budget).await;
            wait_for_sent_len(&sent, 1).await;
            assert_eq!(abort_reason(&sent, 0), expected);
            owner.close();
            while owner.join_next().await.is_some() {}
        }
    }
}

#[tokio::test]
async fn segmentation_capable_dispatch_retains_the_complete_service_ack() {
    let expected = full_service_ack();
    assert!(unsegmented_apdu_len(expected.clone()) > 50);
    let (sent, seg_ack_senders, source_mac, owner) = tokio::time::timeout(
        Duration::from_secs(1),
        dispatch(Segmentation::BOTH, true, 50, 1476),
    )
    .await
    .expect("parent must return without awaiting segmented response ACKs");
    let key = segmented_transaction_key(
        &source_mac,
        None,
        INVOKE_ID,
        bacnet_transport::port::TransportProvenance::unverified(),
    );
    let mut reconstructed = BytesMut::new();
    let mut index = 0usize;

    loop {
        wait_for_sent_len(&sent, index + 1).await;
        let Apdu::ComplexAck(ack) = decoded_sent_apdu(&sent, index) else {
            panic!("expected segmented ComplexAck");
        };
        assert!(ack.segmented);
        assert_eq!(
            ack.service_choice,
            ConfirmedServiceChoice::GET_EVENT_INFORMATION
        );
        assert_eq!(ack.sequence_number, Some(index as u8));
        reconstructed.extend_from_slice(&ack.service_ack);
        if !ack.more_follows {
            break;
        }
        send_segment_ack(
            &seg_ack_senders,
            &key,
            segment_ack(INVOKE_ID, false, index as u8),
        )
        .await;
        index += 1;
    }

    assert_eq!(reconstructed.freeze(), expected);
    owner.close();
    while owner.join_next().await.is_some() {}
}