Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions crates/bacnet-server/src/event_enrollment/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::event::EventStateChange;
use bacnet_types::enums::EventType;
use bacnet_types::primitives::ObjectIdentifier;

use super::{
evaluate_event_enrollments_for_delivery, EventEnrollmentDetailedEvaluationReport,
EventEnrollmentEvaluationReport,
};

/// A state transition detected during event enrollment evaluation.
#[derive(Debug, Clone, PartialEq)]
pub struct EventEnrollmentTransition {
/// The EventEnrollment object that detected the transition.
pub enrollment_oid: ObjectIdentifier,
/// The monitored object whose property triggered the transition.
pub monitored_oid: ObjectIdentifier,
/// The detected state change. `from == to` is a genuine same-state
/// transition emitted by applicable event algorithms.
pub change: EventStateChange,
/// The event type that was evaluated.
pub event_type: EventType,
/// Whether `Event_Enable` permits distributing a notification for this
/// transition. The transition is reported and `Event_State` persisted
/// either way; a cleared bit suppresses only the outbound notification.
pub distribute: bool,
}

/// Evaluate all EventEnrollment objects in the database.
///
/// For each active enrollment, reads the monitored property, evaluates the
/// configured algorithm, applies the Time_Delay / Time_Delay_Normal
/// countdown (seconds, converted by the module's delay helper), executes the
/// transition actions for every indicated transition that fires — same-state
/// included — and returns the fired transitions.
///
/// `interval_secs` is the driving task's evaluation period in wall-clock
/// seconds; the lifecycle passes its (clamped to >= 1)
/// `event_enrollment_interval_secs`. The conversion is never-fire-early, and
/// the pending countdown retains no residual seconds: in-memory state plus
/// builder-config interval means no mid-run rescale exists.
pub fn evaluate_event_enrollments(
db: &mut ObjectDatabase,
interval_secs: u64,
) -> Vec<EventEnrollmentTransition> {
evaluate_event_enrollments_report(db, interval_secs).transitions
}

/// Evaluate all EventEnrollment objects and expose legacy commit diagnostics.
///
/// This preserves the original report shape. Reliability results and typed
/// observation diagnostics are available from
/// [`evaluate_event_enrollments_detailed_report`].
pub fn evaluate_event_enrollments_report(
db: &mut ObjectDatabase,
interval_secs: u64,
) -> EventEnrollmentEvaluationReport {
evaluate_event_enrollments_detailed_report(db, interval_secs).into_legacy()
}

/// Evaluate all EventEnrollment objects and expose every detailed result.
///
/// Unlike [`evaluate_event_enrollments_report`], this additive API includes
/// committed Reliability results plus typed Reliability and observation
/// diagnostics. Only results whose complete object-owned commit succeeds
/// appear in `transitions` or `reliability_results`.
pub fn evaluate_event_enrollments_detailed_report(
db: &mut ObjectDatabase,
interval_secs: u64,
) -> EventEnrollmentDetailedEvaluationReport {
evaluate_event_enrollments_for_delivery(db, interval_secs).report
}
67 changes: 57 additions & 10 deletions crates/bacnet-server/src/event_enrollment/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use bacnet_types::enums::{EventState, EventType, PropertyIdentifier, Reliability
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};

use super::EventEnrollmentTransition;
use crate::server::event_notification_payload::{
project_event_enrollment_payload, CapturedReferencedValue, CommittedNotificationPayload,
EventEnrollmentProjectionSnapshot,
};
use crate::server::event_timestamp::{
confirm_event_timestamp, stage_event_timestamp, SampledEventClock,
};
Expand Down Expand Up @@ -192,6 +196,8 @@ pub(crate) struct CommittedEventEnrollmentDelivery {
pub(crate) result: CommittedEventEnrollmentResult,
pub(crate) ack_required: bool,
pub(crate) recipient_clock: SampledEventClock,
pub(crate) event_type: EventType,
pub(crate) event_values: CommittedNotificationPayload,
}

/// Public report plus the commit-order stream consumed only by the server.
Expand Down Expand Up @@ -305,6 +311,7 @@ pub(super) struct FiredTransition {
pub(super) to: EventState,
pub(super) distribute: bool,
pub(super) ack_required: bool,
pub(super) projection: EventEnrollmentProjectionSnapshot,
}

pub(super) struct ReliabilityUpdate {
Expand All @@ -316,6 +323,7 @@ pub(super) struct ReliabilityUpdate {
pub(super) distribute: bool,
pub(super) ack_required: bool,
pub(super) cause: EventEnrollmentReliabilityCause,
pub(super) referenced_value: CapturedReferencedValue,
}

impl EnrollmentUpdate {
Expand Down Expand Up @@ -586,11 +594,32 @@ pub(super) fn apply_updates_for_delivery(
cause: reliability.cause,
};
report.reliability_results.push(result.clone());
deliveries.push(CommittedEventEnrollmentDelivery {
result: CommittedEventEnrollmentResult::Reliability(result),
ack_required: reliability.ack_required,
recipient_clock,
});
let event_type = EventType::CHANGE_OF_RELIABILITY;
if let Some(event_values) = project_event_enrollment_payload(
db,
oid,
reliability.monitored_oid,
result
.state_change
.as_ref()
.expect("committed Reliability delivery has a state change"),
event_type,
None,
Some(&reliability.referenced_value),
) {
deliveries.push(CommittedEventEnrollmentDelivery {
result: CommittedEventEnrollmentResult::Reliability(result),
ack_required: reliability.ack_required,
recipient_clock,
event_type,
event_values,
});
} else {
tracing::debug!(
enrollment = %oid,
"Committed Event Enrollment reliability payload rejected; suppressing distribution"
);
}
continue;
}

Expand Down Expand Up @@ -660,11 +689,29 @@ pub(super) fn apply_updates_for_delivery(
distribute: fired.distribute,
};
report.transitions.push(result.clone());
deliveries.push(CommittedEventEnrollmentDelivery {
result: CommittedEventEnrollmentResult::Normal(result),
ack_required: fired.ack_required,
recipient_clock,
});
if let Some(event_values) = project_event_enrollment_payload(
db,
oid,
Some(fired.monitored_oid),
&result.change,
event_type,
Some(&fired.projection),
None,
) {
deliveries.push(CommittedEventEnrollmentDelivery {
result: CommittedEventEnrollmentResult::Normal(result),
ack_required: fired.ack_required,
recipient_clock,
event_type,
event_values,
});
} else {
tracing::debug!(
enrollment = %oid,
?event_type,
"Committed Event Enrollment payload rejected; suppressing distribution"
);
}
}

EventEnrollmentEvaluationBatch { report, deliveries }
Expand Down
97 changes: 31 additions & 66 deletions crates/bacnet-server/src/event_enrollment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
//! notification sender without repeating these transition actions.

mod algorithms;
mod api;
mod commit;
mod fault;
mod reference;
Expand All @@ -51,6 +52,10 @@ pub use algorithms::{
encode_change_of_bitstring_params, encode_change_of_state_params,
encode_change_of_value_params, encode_floating_limit_params, encode_out_of_range_params,
};
pub use api::{
evaluate_event_enrollments, evaluate_event_enrollments_detailed_report,
evaluate_event_enrollments_report, EventEnrollmentTransition,
};
pub use commit::{
EventEnrollmentDetailedEvaluationDiagnostic, EventEnrollmentDetailedEvaluationOutcome,
EventEnrollmentDetailedEvaluationReport, EventEnrollmentDetailedEvaluationStage,
Expand Down Expand Up @@ -88,80 +93,20 @@ enum LocalConfigurationReadError {
Unavailable,
}

use crate::server::event_notification_payload::{
capture_status_flags, CapturedReferencedValue, EventEnrollmentProjectionSnapshot,
};
use bacnet_objects::database::ObjectDatabase;
use bacnet_objects::event::{EventStateChange, EventTransition};
#[cfg(test)]
use bacnet_objects::event::EventStateChange;
use bacnet_objects::event::EventTransition;
use bacnet_objects::event_enrollment::{EventEnrollmentEvalState, EventEnrollmentPending};
#[cfg(test)]
use bacnet_objects::traits::BACnetObject;
use bacnet_types::constructed::BACnetEventParameter;
use bacnet_types::enums::{EventState, EventType, ObjectType, PropertyIdentifier, Reliability};
use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};

/// A state transition detected during event enrollment evaluation.
#[derive(Debug, Clone, PartialEq)]
pub struct EventEnrollmentTransition {
/// The EventEnrollment object that detected the transition.
pub enrollment_oid: ObjectIdentifier,
/// The monitored object whose property triggered the transition.
pub monitored_oid: ObjectIdentifier,
/// The detected state change. `from == to` is a genuine same-state
/// transition (Clause 13.2.2.1.4), emitted by CHANGE_OF_VALUE (Figure
/// 13-10's NORMAL→NORMAL) and CHANGE_OF_STATE condition (c).
pub change: EventStateChange,
/// The event type that was evaluated.
pub event_type: EventType,
/// Whether `Event_Enable` permits distributing a notification for this
/// transition. The transition is reported and `Event_State` persisted
/// either way; a cleared bit suppresses only the outbound notification
/// (ASHRAE 135-2020 Clause 12.12).
pub distribute: bool,
}

/// Evaluate all EventEnrollment objects in the database.
///
/// For each active enrollment, reads the monitored property, evaluates the
/// configured algorithm, applies the Time_Delay / Time_Delay_Normal
/// countdown (seconds, converted with [`passes_for_delay`]), executes the
/// Clause 13.2.2.1.4 transition actions for every indicated transition that
/// fires — same-state included — and returns the fired transitions.
///
/// `interval_secs` is the driving task's evaluation period in wall-clock
/// seconds; the lifecycle passes its (clamped to >= 1)
/// `event_enrollment_interval_secs`. The conversion is never-fire-early, and
/// the pending countdown retains no residual seconds: in-memory state plus
/// builder-config interval means no mid-run rescale exists.
pub fn evaluate_event_enrollments(
db: &mut ObjectDatabase,
interval_secs: u64,
) -> Vec<EventEnrollmentTransition> {
evaluate_event_enrollments_report(db, interval_secs).transitions
}

/// Evaluate all EventEnrollment objects and expose legacy commit diagnostics.
///
/// This preserves the original report shape. Reliability results and typed
/// observation diagnostics are available from
/// [`evaluate_event_enrollments_detailed_report`].
pub fn evaluate_event_enrollments_report(
db: &mut ObjectDatabase,
interval_secs: u64,
) -> EventEnrollmentEvaluationReport {
evaluate_event_enrollments_detailed_report(db, interval_secs).into_legacy()
}

/// Evaluate all EventEnrollment objects and expose every detailed result.
///
/// Unlike [`evaluate_event_enrollments_report`], this additive API includes
/// committed Reliability results plus typed Reliability and observation
/// diagnostics. Only results whose complete object-owned commit succeeds
/// appear in `transitions` or `reliability_results`.
pub fn evaluate_event_enrollments_detailed_report(
db: &mut ObjectDatabase,
interval_secs: u64,
) -> EventEnrollmentDetailedEvaluationReport {
evaluate_event_enrollments_for_delivery(db, interval_secs).report
}

/// Evaluate and retain the private commit-order stream for server delivery.
pub(crate) fn evaluate_event_enrollments_for_delivery(
db: &mut ObjectDatabase,
Expand Down Expand Up @@ -289,6 +234,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::Configuration,
CapturedReferencedValue::Unavailable,
);
continue;
}
Expand Down Expand Up @@ -358,6 +304,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::Configuration,
CapturedReferencedValue::Unavailable,
);
continue;
}
Expand All @@ -383,6 +330,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::Configuration,
CapturedReferencedValue::NotEvaluated,
);
continue;
}
Expand Down Expand Up @@ -412,6 +360,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::MonitoredObject,
CapturedReferencedValue::NotEvaluated,
);
continue;
}
Expand All @@ -437,6 +386,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::Configuration,
CapturedReferencedValue::Unavailable,
);
continue;
}
Expand Down Expand Up @@ -468,6 +418,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::Configuration,
CapturedReferencedValue::from_evaluated(monitored_value.as_ref()),
);
continue;
}
Expand Down Expand Up @@ -495,6 +446,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::FaultAlgorithm,
CapturedReferencedValue::from_evaluated(monitored_value.as_ref()),
);
continue;
}
Expand Down Expand Up @@ -531,6 +483,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
cause,
CapturedReferencedValue::from_evaluated(monitored_value.as_ref()),
);
continue;
}
Expand All @@ -554,6 +507,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
current_state,
event_enable,
EventEnrollmentReliabilityCause::Configuration,
CapturedReferencedValue::Unavailable,
);
continue;
}
Expand Down Expand Up @@ -611,6 +565,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(

let event_type = EventType::from_raw(event_type_raw);

let mut projection_setpoint = None;
let (time_delay, arm) = match &params {
BACnetEventParameter::OutOfRange {
high_limit,
Expand Down Expand Up @@ -675,6 +630,7 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
continue;
}
};
projection_setpoint = Some(setpoint);
(
*time_delay,
eval_floating_limit_struct(
Expand Down Expand Up @@ -877,6 +833,15 @@ pub(crate) fn evaluate_event_enrollments_for_delivery(
to: fired.target,
distribute,
ack_required,
projection: EventEnrollmentProjectionSnapshot::new(
monitored.object_identifier,
monitored.property_identifier,
monitored.array_index,
monitored_value,
params,
capture_status_flags(monitored_obj),
projection_setpoint,
),
});
}

Expand Down
Loading
Loading