bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
use bacnet_objects::clock::ClockFrame;
use bacnet_objects::database::{EventSequenceReservation, ObjectDatabase};
use bacnet_types::primitives::BACnetTimeStamp;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SampledEventClock {
    Valid(ClockFrame),
    Unavailable,
    Invalid,
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EventTimestampSample {
    pub(crate) timestamp: BACnetTimeStamp,
    pub(crate) clock: SampledEventClock,
}

/// A timestamp selected without consuming the clockless sequence source.
pub(crate) struct StagedEventTimestamp {
    pub(crate) sample: EventTimestampSample,
    sequence: Option<EventSequenceReservation>,
}

/// Select a timestamp while leaving a clockless sequence retryable.
pub(crate) fn stage_event_timestamp(db: &ObjectDatabase) -> StagedEventTimestamp {
    match db.clock_frame() {
        Some(frame) if frame.is_valid_actual_datetime() => StagedEventTimestamp {
            sample: EventTimestampSample {
                timestamp: BACnetTimeStamp::DateTime {
                    date: frame.local_date,
                    time: frame.local_time,
                },
                clock: SampledEventClock::Valid(frame),
            },
            sequence: None,
        },
        frame => {
            let reservation = db.reserve_event_sequence_number();
            StagedEventTimestamp {
                sample: EventTimestampSample {
                    timestamp: BACnetTimeStamp::SequenceNumber(reservation.number()),
                    clock: if frame.is_some() {
                        SampledEventClock::Invalid
                    } else {
                        SampledEventClock::Unavailable
                    },
                },
                sequence: Some(reservation),
            }
        }
    }
}

/// Confirm staged sequence consumption after the transition commit succeeds.
pub(crate) fn confirm_event_timestamp(
    db: &mut ObjectDatabase,
    staged: StagedEventTimestamp,
) -> EventTimestampSample {
    if let Some(reservation) = staged.sequence {
        assert!(
            db.confirm_event_sequence_number(reservation),
            "event sequence reservation changed while one database guard was held"
        );
    }
    staged.sample
}

/// Sample the Device clock once and select one valid EventNotification
/// timestamp. A missing or malformed wall clock consumes the database-local
/// sequence source instead of inventing a DateTime.
pub(super) fn sample_event_timestamp(db: &mut ObjectDatabase) -> EventTimestampSample {
    let staged = stage_event_timestamp(db);
    confirm_event_timestamp(db, staged)
}

#[cfg(test)]
mod tests {
    use bacnet_encoding::constructed::decode_event_notification;
    use std::sync::Arc;

    use bacnet_encoding::apdu::{Apdu, decode_apdu};
    use bacnet_encoding::npdu::decode_npdu;
    use bacnet_network::layer::NetworkLayer;
    use bacnet_objects::analog::AnalogInputObject;
    use bacnet_objects::clock::ClockReader;
    use bacnet_objects::device::{DeviceConfig, DeviceObject};
    use bacnet_objects::event::EventStateChange;
    use bacnet_objects::notification_class::NotificationClass;
    use bacnet_services::alarm_event::EventNotificationRequest;
    use bacnet_types::enums::{EventState, EventType, ObjectType, UnconfirmedServiceChoice};
    use bacnet_types::primitives::{Date, Time};
    use bytes::Bytes;
    use tokio::sync::{Mutex, RwLock};

    use super::super::event_notifications_tests::{
        local_broadcast_destination, recording_transport,
    };
    use super::super::{BACnetServer, LearnedRouterCache, NotificationTransactions};
    use super::*;
    use crate::server::test_transport::TestTransport;
    use bacnet_types::enums::EngineeringUnits;

    struct FixedClock(ClockFrame);

    impl ClockReader for FixedClock {
        fn read_clock(&self) -> Option<ClockFrame> {
            Some(self.0)
        }
    }

    fn fixed_frame() -> ClockFrame {
        ClockFrame {
            local_date: Date {
                year: 124,
                month: 2,
                day: 29,
                day_of_week: 4,
            },
            local_time: Time {
                hour: 12,
                minute: 34,
                second: 56,
                hundredths: 78,
            },
            utc_offset: 300,
            daylight_savings_status: true,
        }
    }

    #[test]
    fn valid_clock_frame_selects_exact_local_datetime() {
        let frame = fixed_frame();
        let mut db = ObjectDatabase::new();
        db.set_clock_reader(Some(Arc::new(FixedClock(frame))));

        assert_eq!(
            sample_event_timestamp(&mut db),
            EventTimestampSample {
                timestamp: BACnetTimeStamp::DateTime {
                    date: frame.local_date,
                    time: frame.local_time,
                },
                clock: SampledEventClock::Valid(frame),
            }
        );
    }

    #[test]
    fn clockless_source_increments_independently_per_database() {
        let mut first = ObjectDatabase::new();
        let mut second = ObjectDatabase::new();

        assert_eq!(
            sample_event_timestamp(&mut first).timestamp,
            BACnetTimeStamp::SequenceNumber(0)
        );
        assert_eq!(
            sample_event_timestamp(&mut first).timestamp,
            BACnetTimeStamp::SequenceNumber(1)
        );
        assert_eq!(
            sample_event_timestamp(&mut second).timestamp,
            BACnetTimeStamp::SequenceNumber(0)
        );
    }

    #[test]
    fn clockless_staging_does_not_consume_until_confirmed() {
        let mut db = ObjectDatabase::new();

        let staged = stage_event_timestamp(&db);
        assert_eq!(staged.sample.timestamp, BACnetTimeStamp::SequenceNumber(0));
        assert_eq!(db.reserve_event_sequence_number().number(), 0);

        let sample = confirm_event_timestamp(&mut db, staged);
        assert_eq!(sample.timestamp, BACnetTimeStamp::SequenceNumber(0));
        assert_eq!(db.reserve_event_sequence_number().number(), 1);
    }

    #[test]
    fn malformed_clock_frame_falls_back_to_sequence_and_is_flagged() {
        let mut frame = fixed_frame();
        frame.local_time.hour = Time::UNSPECIFIED;
        let mut db = ObjectDatabase::new();
        db.set_clock_reader(Some(Arc::new(FixedClock(frame))));

        assert_eq!(
            sample_event_timestamp(&mut db),
            EventTimestampSample {
                timestamp: BACnetTimeStamp::SequenceNumber(0),
                clock: SampledEventClock::Invalid,
            }
        );
    }

    fn outbound_database(frame: Option<ClockFrame>, recipients: usize) -> ObjectDatabase {
        let mut db = ObjectDatabase::new();
        if let Some(frame) = frame {
            db.set_clock_reader(Some(Arc::new(FixedClock(frame))));
        }
        db.add(Box::new(
            DeviceObject::new(DeviceConfig {
                identity: bacnet_objects::DeviceIdentity::default()
                    .with_name("Timestamp-Policy-Device"),
                ..DeviceConfig::default()
            })
            .unwrap(),
        ))
        .unwrap();
        db.add(Box::new(
            AnalogInputObject::new(1, "AI-1", EngineeringUnits::SQUARE_METERS).unwrap(),
        ))
        .unwrap();
        let mut notification_class = NotificationClass::new(0, "NC-0").unwrap();
        for _ in 0..recipients {
            notification_class
                .add_destination(local_broadcast_destination())
                .unwrap();
        }
        db.add(Box::new(notification_class)).unwrap();
        db
    }

    fn decode_notification(frame: &Bytes) -> EventNotificationRequest {
        let npdu = decode_npdu(frame.clone()).expect("decode NPDU");
        let Apdu::UnconfirmedRequest(request) = decode_apdu(npdu.payload).expect("decode APDU")
        else {
            panic!("expected unconfirmed EventNotification");
        };
        assert_eq!(
            request.service_choice,
            UnconfirmedServiceChoice::UNCONFIRMED_EVENT_NOTIFICATION
        );
        decode_event_notification(&request.service_request).expect("decode EventNotification")
    }

    async fn send_event(
        db: &Arc<RwLock<ObjectDatabase>>,
        network: &Arc<NetworkLayer<TestTransport>>,
    ) {
        BACnetServer::<TestTransport>::build_and_send_event_notification_with_bindings(
            &crate::server::event_delivery::EventDelivery {
                db,
                network,
                comm_state: &Arc::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,
            },
            &bacnet_types::primitives::ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
            (
                EventStateChange {
                    from: EventState::NORMAL,
                    to: EventState::HIGH_LIMIT,
                },
                EventType::OUT_OF_RANGE,
            ),
        )
        .await;
    }

    #[tokio::test]
    async fn outbound_event_uses_exact_sampled_device_datetime() {
        let frame = fixed_frame();
        let (transport, sent) = recording_transport();
        let network = Arc::new(NetworkLayer::new(transport));
        let db = Arc::new(RwLock::new(outbound_database(Some(frame), 1)));

        send_event(&db, &network).await;

        let sent = sent.npdus();
        assert_eq!(sent.len(), 1);
        assert_eq!(
            decode_notification(&sent[0]).timestamp,
            BACnetTimeStamp::DateTime {
                date: frame.local_date,
                time: frame.local_time,
            }
        );
    }

    #[tokio::test]
    async fn outbound_event_with_invalid_clock_uses_sequence_without_suppressing_delivery() {
        let mut frame = fixed_frame();
        frame.local_time.hour = Time::UNSPECIFIED;
        let (transport, sent) = recording_transport();
        let network = Arc::new(NetworkLayer::new(transport));
        let db = Arc::new(RwLock::new(outbound_database(Some(frame), 1)));

        send_event(&db, &network).await;

        let sent = sent.npdus();
        assert_eq!(sent.len(), 1);
        assert_eq!(
            decode_notification(&sent[0]).timestamp,
            BACnetTimeStamp::SequenceNumber(0)
        );
    }

    #[tokio::test]
    async fn clockless_event_sequence_is_shared_by_all_recipients_and_increments_per_event() {
        let (transport, sent) = recording_transport();
        let network = Arc::new(NetworkLayer::new(transport));
        let db = Arc::new(RwLock::new(outbound_database(None, 2)));

        send_event(&db, &network).await;
        {
            let first = sent.npdus();
            assert_eq!(first.len(), 2);
            assert!(first.iter().all(|frame| {
                decode_notification(frame).timestamp == BACnetTimeStamp::SequenceNumber(0)
            }));
        }

        sent.clear();
        send_event(&db, &network).await;
        let second = sent.npdus();
        assert_eq!(second.len(), 2);
        assert!(second.iter().all(|frame| {
            decode_notification(frame).timestamp == BACnetTimeStamp::SequenceNumber(1)
        }));
    }
}