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};
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)
}
}
pub(crate) fn handle_get_enrollment_summary_budgeted(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: crate::server::GetEnrollmentSummaryBudget,
) -> Result<(), EnrollmentSummaryFailure> {
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();
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)
}