use super::super::*;
use bacnet_encoding::primitives::decode_timestamp_choice;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::bitstring::EventTransitionBits;
use bacnet_types::enums::NotifyType;
#[path = "event_information_page.rs"]
mod page;
pub(crate) use page::{EventInformationFailure, handle_get_event_information_configured};
const EVENT_SUMMARY_SIGNATURE: [PropertyIdentifier; 6] = [
PropertyIdentifier::EVENT_STATE,
PropertyIdentifier::ACKED_TRANSITIONS,
PropertyIdentifier::EVENT_TIME_STAMPS,
PropertyIdentifier::NOTIFY_TYPE,
PropertyIdentifier::EVENT_ENABLE,
PropertyIdentifier::NOTIFICATION_CLASS,
];
enum EventSummaryProjectionResult {
NotEventInitiating,
Excluded,
Projected(EventSummary),
}
fn project_event_summary(
object: &dyn BACnetObject,
db: &ObjectDatabase,
) -> Result<EventSummaryProjectionResult, Error> {
let advertised = object.property_list();
if !EVENT_SUMMARY_SIGNATURE
.iter()
.all(|property| advertised.contains(property))
{
return Ok(EventSummaryProjectionResult::NotEventInitiating);
}
let object_identifier = object.object_identifier();
if advertised.contains(&PropertyIdentifier::EVENT_DETECTION_ENABLE) {
match read_required(
object,
object_identifier,
PropertyIdentifier::EVENT_DETECTION_ENABLE,
)? {
PropertyValue::Boolean(false) => return Ok(EventSummaryProjectionResult::Excluded),
PropertyValue::Boolean(true) => {}
_ => {
return Err(operational_problem(
object_identifier,
"Event_Detection_Enable is not Boolean",
));
}
}
}
let event_state = EventState::from_raw(read_enumerated(
object,
object_identifier,
PropertyIdentifier::EVENT_STATE,
)?);
let acknowledged_transitions = read_transition_bits(
object,
object_identifier,
PropertyIdentifier::ACKED_TRANSITIONS,
)?;
let event_timestamps = read_event_timestamps(object, object_identifier)?;
let notify_type = NotifyType::from_raw(read_enumerated(
object,
object_identifier,
PropertyIdentifier::NOTIFY_TYPE,
)?);
let event_enable =
read_transition_bits(object, object_identifier, PropertyIdentifier::EVENT_ENABLE)?;
let notification_class = read_unsigned_u32(
object,
object_identifier,
PropertyIdentifier::NOTIFICATION_CLASS,
)?;
let event_priorities =
read_notification_class_priorities(db, object_identifier, notification_class)?;
Ok(EventSummaryProjectionResult::Projected(EventSummary {
object_identifier,
event_state,
acknowledged_transitions,
event_timestamps,
notify_type,
event_enable,
event_priorities,
}))
}
fn is_active_event_state(summary: &EventSummary) -> bool {
summary.event_state != EventState::NORMAL
|| summary.acknowledged_transitions != EventTransitionBits::all()
}
pub fn handle_get_event_information(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
handle_get_event_information_with_budget(db, service_data, buf, None)
}
pub(crate) fn handle_get_event_information_with_budget(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
max_service_ack_bytes: Option<usize>,
) -> Result<(), Error> {
let request =
GetEventInformationRequest::decode(service_data).map_err(Error::into_request_reject)?;
let cursor = request
.last_received_object_identifier
.map(|identifier| identifier.encode());
let mut object_identifiers = db.list_objects();
object_identifiers.sort_unstable_by_key(ObjectIdentifier::encode);
let mut summaries = Vec::new();
for oid in object_identifiers {
if cursor.is_some_and(|cursor| oid.encode() <= cursor) {
continue;
}
let Some(object) = db.get(&oid) else {
continue;
};
let summary = match project_event_summary(object, db)? {
EventSummaryProjectionResult::NotEventInitiating
| EventSummaryProjectionResult::Excluded => continue,
EventSummaryProjectionResult::Projected(summary) => summary,
};
if is_active_event_state(&summary) {
summaries.push(summary);
}
}
let ack = paginate_ack(summaries, max_service_ack_bytes)?;
let mut encoded = BytesMut::new();
ack.encode(&mut encoded)?;
buf.extend_from_slice(&encoded);
Ok(())
}
fn paginate_ack(
summaries: Vec<EventSummary>,
max_service_ack_bytes: Option<usize>,
) -> Result<GetEventInformationAck, Error> {
let Some(max_service_ack_bytes) = max_service_ack_bytes else {
return Ok(GetEventInformationAck {
list_of_event_summaries: summaries,
more_events: false,
});
};
let full = GetEventInformationAck {
list_of_event_summaries: summaries.clone(),
more_events: false,
};
if summaries.is_empty() || encoded_ack_len(&full)? <= max_service_ack_bytes {
return Ok(full);
}
let mut low = 0usize;
let mut high = summaries.len() - 1;
while low < high {
let count = low + (high - low).div_ceil(2);
let candidate = GetEventInformationAck {
list_of_event_summaries: summaries[..count].to_vec(),
more_events: true,
};
if encoded_ack_len(&candidate)? <= max_service_ack_bytes {
low = count;
} else {
high = count - 1;
}
}
let count = low.max(1);
Ok(GetEventInformationAck {
list_of_event_summaries: summaries[..count].to_vec(),
more_events: count < summaries.len(),
})
}
fn encoded_ack_len(ack: &GetEventInformationAck) -> Result<usize, Error> {
let mut encoded = BytesMut::new();
ack.encode(&mut encoded)?;
Ok(encoded.len())
}
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_enumerated(
object: &dyn BACnetObject,
object_identifier: ObjectIdentifier,
property: PropertyIdentifier,
) -> Result<u32, Error> {
match read_required(object, object_identifier, property)? {
PropertyValue::Enumerated(value) => Ok(value),
_ => Err(operational_problem(
object_identifier,
format!("required property {property:?} is not Enumerated"),
)),
}
}
fn read_unsigned_u32(
object: &dyn BACnetObject,
object_identifier: ObjectIdentifier,
property: PropertyIdentifier,
) -> Result<u32, Error> {
match read_required(object, object_identifier, property)? {
PropertyValue::Unsigned(value) => u32::try_from(value).map_err(|_| {
operational_problem(
object_identifier,
format!("required property {property:?} exceeds Unsigned32"),
)
}),
_ => Err(operational_problem(
object_identifier,
format!("required property {property:?} is not Unsigned"),
)),
}
}
fn read_transition_bits(
object: &dyn BACnetObject,
object_identifier: ObjectIdentifier,
property: PropertyIdentifier,
) -> Result<EventTransitionBits, Error> {
match read_required(object, object_identifier, property)? {
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,
format!("required property {property:?} is not a canonical three-bit BitString"),
)),
}
}
fn read_event_timestamps(
object: &dyn BACnetObject,
object_identifier: ObjectIdentifier,
) -> Result<[BACnetTimeStamp; 3], Error> {
let PropertyValue::List(items) = read_required(
object,
object_identifier,
PropertyIdentifier::EVENT_TIME_STAMPS,
)?
else {
return Err(operational_problem(
object_identifier,
"Event_Time_Stamps is not a three-item List",
));
};
let [offnormal, fault, normal] = items.as_slice() else {
return Err(operational_problem(
object_identifier,
"Event_Time_Stamps does not contain exactly three items",
));
};
Ok([
decode_event_timestamp(offnormal).ok_or_else(|| {
operational_problem(object_identifier, "invalid TO_OFFNORMAL event timestamp")
})?,
decode_event_timestamp(fault).ok_or_else(|| {
operational_problem(object_identifier, "invalid TO_FAULT event timestamp")
})?,
decode_event_timestamp(normal).ok_or_else(|| {
operational_problem(object_identifier, "invalid TO_NORMAL event timestamp")
})?,
])
}
fn read_notification_class_priorities(
db: &ObjectDatabase,
event_object_identifier: ObjectIdentifier,
notification_class: u32,
) -> Result<[u32; 3], Error> {
let mut matching = None;
for class_oid in db.find_by_type(ObjectType::NOTIFICATION_CLASS) {
let Some(class_object) = db.get(&class_oid) else {
continue;
};
let Some(class_number) = read_notification_class_number(class_object) else {
continue;
};
if class_number == notification_class && matching.replace(class_object).is_some() {
return Err(operational_problem(
event_object_identifier,
format!("multiple Notification Class objects match {notification_class}"),
));
}
}
let class_object = matching.ok_or_else(|| {
operational_problem(
event_object_identifier,
format!("no Notification Class object matches {notification_class}"),
)
})?;
let PropertyValue::List(priorities) = read_required(
class_object,
event_object_identifier,
PropertyIdentifier::PRIORITY,
)?
else {
return Err(operational_problem(
event_object_identifier,
"Notification Class Priority is not a three-item List",
));
};
let [offnormal, fault, normal] = priorities.as_slice() else {
return Err(operational_problem(
event_object_identifier,
"Notification Class Priority does not contain exactly three items",
));
};
Ok([
read_priority_coordinate(event_object_identifier, offnormal, "TO_OFFNORMAL")?,
read_priority_coordinate(event_object_identifier, fault, "TO_FAULT")?,
read_priority_coordinate(event_object_identifier, normal, "TO_NORMAL")?,
])
}
fn read_notification_class_number(object: &dyn BACnetObject) -> Option<u32> {
match object
.read_property(PropertyIdentifier::NOTIFICATION_CLASS, None)
.ok()?
{
PropertyValue::Unsigned(value) => u32::try_from(value).ok(),
_ => None,
}
}
fn read_priority_coordinate(
object_identifier: ObjectIdentifier,
value: &PropertyValue,
coordinate: &str,
) -> Result<u32, Error> {
match value {
PropertyValue::Unsigned(priority) if *priority <= 255 => Ok(*priority as u32),
PropertyValue::Unsigned(_) => Err(operational_problem(
object_identifier,
format!("Notification Class {coordinate} Priority exceeds 255"),
)),
_ => Err(operational_problem(
object_identifier,
format!("Notification Class {coordinate} Priority is not Unsigned"),
)),
}
}
fn operational_problem(
object_identifier: ObjectIdentifier,
detail: impl std::fmt::Display,
) -> Error {
tracing::warn!(?object_identifier, reason = %detail, "GetEventInformation projection failed");
Error::protocol(ErrorClass::DEVICE, ErrorCode::OPERATIONAL_PROBLEM)
}
fn decode_event_timestamp(value: &PropertyValue) -> Option<BACnetTimeStamp> {
match value {
PropertyValue::ApplicationData(encoded) => {
let (timestamp, consumed) = decode_timestamp_choice(encoded, 0).ok()?;
(consumed == encoded.len()).then_some(timestamp)
}
PropertyValue::Unsigned(sequence_number) => u16::try_from(*sequence_number)
.ok()
.map(BACnetTimeStamp::SequenceNumber),
_ => None,
}
}