bacnet-server 0.15.0

BACnet server: APDU dispatch and service handlers
Documentation
//! GetEnrollmentSummary service handler.

use super::super::*;
use bacnet_objects::event::{EnrollmentSummaryCapability, EventTransition};
use bacnet_objects::notification_class::resolve_enrollment_summary_class_internal;
use bacnet_objects::traits::BACnetObject;
use bacnet_services::enrollment_summary::{
    EnrollmentSummaryEntry, GetEnrollmentSummaryAck, GetEnrollmentSummaryRequest,
};
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::enums::{AcknowledgmentFilter, EnrollmentSummaryEventStateFilter};

/// Handle the deprecated GetEnrollmentSummary interoperability service.
///
/// Candidates opt in through object-owned event capability. Every mandatory
/// projected value is then read and validated strictly; a malformed candidate
/// fails DEVICE / OPERATIONAL_PROBLEM rather than fabricating a summary.
pub fn handle_get_enrollment_summary(
    db: &ObjectDatabase,
    service_data: &[u8],
    buf: &mut BytesMut,
) -> Result<(), Error> {
    let request =
        GetEnrollmentSummaryRequest::decode(service_data).map_err(Error::into_request_reject)?;
    let mut entries = Vec::new();
    visit_entries::<Error>(db, &request, |entry| {
        entries.push(entry);
        Ok(())
    })?;
    GetEnrollmentSummaryAck { entries }.encode(buf);
    Ok(())
}

#[derive(Debug)]
pub(crate) enum EnrollmentSummaryFailure {
    Work,
    Bytes,
    Service(Error),
}

impl From<Error> for EnrollmentSummaryFailure {
    fn from(error: Error) -> Self {
        Self::Service(error)
    }
}

/// Complete logical service ACK, transactional under the caller's database view.
pub(crate) fn handle_get_enrollment_summary_budgeted(
    db: &ObjectDatabase,
    service_data: &[u8],
    buf: &mut BytesMut,
    budget: crate::server::GetEnrollmentSummaryBudget,
) -> Result<(), EnrollmentSummaryFailure> {
    // Preserve decoder errors even when the database cannot be admitted.
    let request =
        GetEnrollmentSummaryRequest::decode(service_data).map_err(Error::into_request_reject)?;
    if db.len() > budget.max_objects {
        return Err(EnrollmentSummaryFailure::Work);
    }
    let mut scratch = BytesMut::new();
    let mut entry_buf = BytesMut::new();
    visit_entries::<EnrollmentSummaryFailure>(db, &request, |entry| {
        entry_buf.clear();
        // The ACK is a bare sequence: concatenated singleton encodings retain
        // the complete-service wire shape without retaining all entries.
        GetEnrollmentSummaryAck {
            entries: vec![entry],
        }
        .encode(&mut entry_buf);
        if entry_buf.len() > budget.max_service_ack_bytes - scratch.len() {
            return Err(EnrollmentSummaryFailure::Bytes);
        }
        scratch.extend_from_slice(&entry_buf);
        Ok(())
    })?;
    buf.extend_from_slice(&scratch);
    Ok(())
}

fn visit_entries<E: From<Error>>(
    db: &ObjectDatabase,
    request: &GetEnrollmentSummaryRequest,
    mut emit: impl FnMut(EnrollmentSummaryEntry) -> Result<(), E>,
) -> Result<(), E> {
    for (_oid, object) in db.iter_objects() {
        let Some(capability) = object.enrollment_summary_capability_internal() else {
            continue;
        };
        let object_identifier = object.object_identifier();
        let advertised = object.property_list();

        if advertised.contains(&PropertyIdentifier::EVENT_DETECTION_ENABLE) {
            match read_required(
                object,
                object_identifier,
                PropertyIdentifier::EVENT_DETECTION_ENABLE,
            )? {
                PropertyValue::Boolean(false) => continue,
                PropertyValue::Boolean(true) => {}
                _ => {
                    return Err(operational_problem(
                        object_identifier,
                        "Event_Detection_Enable is not Boolean",
                    )
                    .into());
                }
            }
        }

        let event_state = read_event_state(object, object_identifier)?;
        let acknowledged_transitions = read_acknowledged_transitions(object, object_identifier)?;
        let transition = latest_transition(
            capability,
            event_state,
            acknowledged_transitions,
            object_identifier,
        )?;
        let notification_class = read_notification_class(object, object_identifier)?;
        let class_projection = resolve_enrollment_summary_class_internal(
            db,
            notification_class,
            transition,
            request
                .enrollment_filter
                .as_ref()
                .map(|filter| (&filter.recipient, filter.process_identifier)),
        )
        .map_err(|error| {
            operational_problem(
                object_identifier,
                format!("Notification Class {notification_class} projection failed: {error}"),
            )
        })?;

        if !acknowledgment_matches(request.acknowledgment_filter, acknowledged_transitions)
            || !class_projection.enrollment_member
            || !event_state_matches(request.event_state_filter, event_state)
            || request
                .event_type_filter
                .is_some_and(|filter| filter != capability.event_type)
            || request.priority_filter.is_some_and(|filter| {
                class_projection.priority < filter.min_priority
                    || class_projection.priority > filter.max_priority
            })
            || request
                .notification_class_filter
                .is_some_and(|filter| filter != notification_class)
        {
            continue;
        }

        emit(EnrollmentSummaryEntry {
            object_identifier,
            event_type: capability.event_type,
            event_state,
            priority: class_projection.priority,
            notification_class: Some(notification_class),
        })?;
    }

    Ok(())
}

fn latest_transition(
    capability: EnrollmentSummaryCapability,
    event_state: EventState,
    acknowledged_transitions: EventTransitionBits,
    object_identifier: ObjectIdentifier,
) -> Result<EventTransition, Error> {
    if let Some(transition) = capability.last_transition {
        return Ok(transition);
    }
    if event_state == EventState::NORMAL && acknowledged_transitions.is_all() {
        return Ok(EventTransition::ToNormal);
    }
    Err(operational_problem(
        object_identifier,
        "no committed transition and initial event invariants are inconsistent",
    ))
}

fn acknowledgment_matches(
    filter: AcknowledgmentFilter,
    acknowledged_transitions: EventTransitionBits,
) -> bool {
    match filter {
        AcknowledgmentFilter::ALL => true,
        AcknowledgmentFilter::ACKED => acknowledged_transitions.is_all(),
        AcknowledgmentFilter::NOT_ACKED => !acknowledged_transitions.is_all(),
        _ => unreachable!("request decoder rejects undefined acknowledgment filters"),
    }
}

fn event_state_matches(
    filter: Option<EnrollmentSummaryEventStateFilter>,
    event_state: EventState,
) -> bool {
    match filter {
        None => true,
        Some(filter) if filter == EnrollmentSummaryEventStateFilter::ALL => true,
        Some(filter) if filter == EnrollmentSummaryEventStateFilter::ACTIVE => {
            event_state != EventState::NORMAL
        }
        Some(filter) if filter == EnrollmentSummaryEventStateFilter::OFFNORMAL => {
            event_state == EventState::OFFNORMAL
        }
        Some(filter) if filter == EnrollmentSummaryEventStateFilter::FAULT => {
            event_state == EventState::FAULT
        }
        Some(filter) if filter == EnrollmentSummaryEventStateFilter::NORMAL => {
            event_state == EventState::NORMAL
        }
        Some(_) => false,
    }
}

fn read_required(
    object: &dyn BACnetObject,
    object_identifier: ObjectIdentifier,
    property: PropertyIdentifier,
) -> Result<PropertyValue, Error> {
    object.read_property(property, None).map_err(|error| {
        operational_problem(
            object_identifier,
            format!("required property {property:?} is unreadable: {error}"),
        )
    })
}

fn read_event_state(
    object: &dyn BACnetObject,
    object_identifier: ObjectIdentifier,
) -> Result<EventState, Error> {
    match read_required(object, object_identifier, PropertyIdentifier::EVENT_STATE)? {
        PropertyValue::Enumerated(value) => Ok(EventState::from_raw(value)),
        _ => Err(operational_problem(
            object_identifier,
            "Event_State is not Enumerated",
        )),
    }
}

fn read_acknowledged_transitions(
    object: &dyn BACnetObject,
    object_identifier: ObjectIdentifier,
) -> Result<EventTransitionBits, Error> {
    match read_required(
        object,
        object_identifier,
        PropertyIdentifier::ACKED_TRANSITIONS,
    )? {
        PropertyValue::BitString { unused_bits, data }
            if unused_bits == 5 && data.len() == 1 && data[0] & 0x1f == 0 =>
        {
            Ok(EventTransitionBits::from_bacnet(&data))
        }
        _ => Err(operational_problem(
            object_identifier,
            "Acked_Transitions is not a canonical three-bit BitString",
        )),
    }
}

fn read_notification_class(
    object: &dyn BACnetObject,
    object_identifier: ObjectIdentifier,
) -> Result<u32, Error> {
    match read_required(
        object,
        object_identifier,
        PropertyIdentifier::NOTIFICATION_CLASS,
    )? {
        PropertyValue::Unsigned(value) => u32::try_from(value).map_err(|_| {
            operational_problem(object_identifier, "Notification_Class exceeds Unsigned32")
        }),
        _ => Err(operational_problem(
            object_identifier,
            "Notification_Class is not Unsigned32",
        )),
    }
}

fn operational_problem(
    object_identifier: ObjectIdentifier,
    detail: impl std::fmt::Display,
) -> Error {
    tracing::warn!(?object_identifier, reason = %detail, "GetEnrollmentSummary projection failed");
    Error::protocol(ErrorClass::DEVICE, ErrorCode::OPERATIONAL_PROBLEM)
}