use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType};
use bacnet_types::error::Error;
use bacnet_types::primitives::ObjectIdentifier;
use crate::clock::{ClockFrame, ClockReader};
use crate::event_enrollment::EventEnrollmentMonitoredSource;
use crate::traits::{BACnetObject, MonotonicClock};
pub struct ObjectDatabase {
objects: HashMap<ObjectIdentifier, Box<dyn BACnetObject>>,
clock: Option<Arc<dyn ClockReader>>,
monotonic_clock: Option<Arc<MonotonicClock>>,
event_sequence_number: u16,
name_index: HashMap<String, ObjectIdentifier>,
type_index: HashMap<ObjectType, Vec<ObjectIdentifier>>,
invalid_enrollment_eval_state: HashSet<ObjectIdentifier>,
enrollment_eval_sources: HashMap<ObjectIdentifier, EventEnrollmentMonitoredSource>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EventSequenceReservation {
number: u16,
}
impl EventSequenceReservation {
#[doc(hidden)]
pub fn number(self) -> u16 {
self.number
}
}
impl Default for ObjectDatabase {
fn default() -> Self {
Self::new()
}
}
impl ObjectDatabase {
pub fn new() -> Self {
Self {
objects: HashMap::new(),
clock: None,
monotonic_clock: None,
event_sequence_number: 0,
name_index: HashMap::new(),
type_index: HashMap::new(),
invalid_enrollment_eval_state: HashSet::new(),
enrollment_eval_sources: HashMap::new(),
}
}
pub fn add(&mut self, mut object: Box<dyn BACnetObject>) -> Result<(), Error> {
object.bind_clock_internal(self.clock.clone());
object.bind_monotonic_clock_internal(self.monotonic_clock.clone());
let oid = object.object_identifier();
let name = object.object_name().to_string();
if let Some(&existing_oid) = self.name_index.get(&name) {
if existing_oid != oid {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::DUPLICATE_NAME.to_raw() as u32,
});
}
}
if let Some(old) = self.objects.get(&oid) {
let old_name = old.object_name().to_string();
self.name_index.remove(&old_name);
self.invalidate_enrollments_monitoring(&oid);
}
self.name_index.insert(name, oid);
let is_new = !self.objects.contains_key(&oid);
self.enrollment_eval_sources.remove(&oid);
self.objects.insert(oid, object);
if is_new {
self.type_index
.entry(oid.object_type())
.or_default()
.push(oid);
}
Ok(())
}
pub fn find_by_name(&self, name: &str) -> Option<&dyn BACnetObject> {
let oid = self.name_index.get(name)?;
self.objects.get(oid).map(|o| o.as_ref())
}
pub fn check_name_available(
&self,
oid: &ObjectIdentifier,
new_name: &str,
) -> Result<(), Error> {
if let Some(&owner) = self.name_index.get(new_name) {
if owner != *oid {
return Err(Error::Protocol {
class: ErrorClass::OBJECT.to_raw() as u32,
code: ErrorCode::DUPLICATE_NAME.to_raw() as u32,
});
}
}
Ok(())
}
pub fn update_name_index(&mut self, oid: &ObjectIdentifier) {
if let Some(obj) = self.objects.get(oid) {
self.name_index.retain(|_, v| v != oid);
self.name_index.insert(obj.object_name().to_string(), *oid);
}
}
pub fn get(&self, oid: &ObjectIdentifier) -> Option<&dyn BACnetObject> {
self.objects.get(oid).map(|o| o.as_ref())
}
pub fn get_mut(&mut self, oid: &ObjectIdentifier) -> Option<&mut Box<dyn BACnetObject>> {
self.objects.get_mut(oid)
}
pub fn enrollment_eval_state_invalidated(&self, oid: &ObjectIdentifier) -> bool {
self.invalid_enrollment_eval_state.contains(oid)
}
pub fn set_enrollment_eval_state_invalidated(
&mut self,
oid: ObjectIdentifier,
invalidated: bool,
) {
if invalidated {
self.invalid_enrollment_eval_state.insert(oid);
} else {
self.invalid_enrollment_eval_state.remove(&oid);
}
}
pub fn enrollment_eval_source(
&self,
oid: &ObjectIdentifier,
) -> Option<EventEnrollmentMonitoredSource> {
self.enrollment_eval_sources.get(oid).copied()
}
pub fn set_enrollment_eval_source(
&mut self,
oid: ObjectIdentifier,
source: Option<EventEnrollmentMonitoredSource>,
) {
if let Some(source) = source {
self.enrollment_eval_sources.insert(oid, source);
} else {
self.enrollment_eval_sources.remove(&oid);
}
}
fn invalidate_enrollments_monitoring(&mut self, monitored_oid: &ObjectIdentifier) {
let affected = self
.objects
.iter()
.filter_map(|(oid, object)| {
let source = object
.enrollment_eval_source_internal()
.flatten()
.or_else(|| self.enrollment_eval_sources.get(oid).copied());
source
.is_some_and(|source| source.0 == *monitored_oid)
.then_some(*oid)
})
.collect::<Vec<_>>();
self.invalid_enrollment_eval_state.extend(affected);
}
pub fn remove(&mut self, oid: &ObjectIdentifier) -> Option<Box<dyn BACnetObject>> {
if self.objects.contains_key(oid) {
self.invalidate_enrollments_monitoring(oid);
}
if let Some(mut obj) = self.objects.remove(oid) {
self.enrollment_eval_sources.remove(oid);
if obj.enrollment_eval_state_internal().is_some() {
if obj
.set_enrollment_eval_state_internal(Default::default())
.is_err()
{
self.invalid_enrollment_eval_state.insert(*oid);
} else {
self.invalid_enrollment_eval_state.remove(oid);
}
let _ = obj.set_enrollment_eval_source_internal(None);
} else {
self.invalid_enrollment_eval_state.remove(oid);
}
self.name_index.remove(obj.object_name());
if let Some(type_set) = self.type_index.get_mut(&oid.object_type()) {
type_set.retain(|o| o != oid);
}
Some(obj)
} else {
None
}
}
pub fn list_objects(&self) -> Vec<ObjectIdentifier> {
self.objects.keys().copied().collect()
}
pub fn find_by_type(&self, object_type: ObjectType) -> Vec<ObjectIdentifier> {
self.type_index
.get(&object_type)
.cloned()
.unwrap_or_default()
}
pub fn iter_objects(&self) -> impl Iterator<Item = (ObjectIdentifier, &dyn BACnetObject)> {
self.objects.iter().map(|(&oid, obj)| (oid, obj.as_ref()))
}
pub fn set_clock_reader(&mut self, clock: Option<Arc<dyn ClockReader>>) {
self.clock = clock;
for object in self.objects.values_mut() {
object.bind_clock_internal(self.clock.clone());
}
}
#[doc(hidden)]
pub fn set_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
self.monotonic_clock = clock;
for object in self.objects.values_mut() {
object.bind_monotonic_clock_internal(self.monotonic_clock.clone());
}
}
pub fn clock_frame(&self) -> Option<ClockFrame> {
self.clock.as_ref()?.read_clock()
}
#[doc(hidden)]
pub fn next_event_sequence_number(&mut self) -> u16 {
let reservation = self.reserve_event_sequence_number();
let number = reservation.number();
let confirmed = self.confirm_event_sequence_number(reservation);
debug_assert!(
confirmed,
"a reservation cannot become stale without mutation"
);
number
}
#[doc(hidden)]
pub fn reserve_event_sequence_number(&self) -> EventSequenceReservation {
EventSequenceReservation {
number: self.event_sequence_number,
}
}
#[doc(hidden)]
pub fn confirm_event_sequence_number(&mut self, reservation: EventSequenceReservation) -> bool {
if reservation.number != self.event_sequence_number {
return false;
}
self.event_sequence_number = self.event_sequence_number.wrapping_add(1);
true
}
pub fn for_each_object_mut<F>(&mut self, mut f: F)
where
F: FnMut(ObjectIdentifier, &mut dyn BACnetObject),
{
for (&oid, obj) in self.objects.iter_mut() {
f(oid, obj.as_mut());
}
}
pub fn len(&self) -> usize {
self.objects.len()
}
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::analog::{AnalogInputObject, AnalogOutputObject, AnalogValueObject};
use crate::binary::{BinaryInputObject, BinaryOutputObject, BinaryValueObject};
use crate::event::{EventStateChange, EventTransition, EventTransitionCommit};
use crate::multistate::{MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject};
use bacnet_types::enums::{ErrorClass, ErrorCode, EventState, ObjectType, PropertyIdentifier};
use bacnet_types::error::Error;
use bacnet_types::primitives::{BACnetTimeStamp, PropertyValue};
#[test]
fn all_nine_builtin_intrinsic_families_implement_atomic_commit() {
let mut objects: Vec<Box<dyn BACnetObject>> = vec![
Box::new(AnalogInputObject::new(1, "AI", 0).unwrap()),
Box::new(AnalogOutputObject::new(1, "AO", 0).unwrap()),
Box::new(AnalogValueObject::new(1, "AV", 0).unwrap()),
Box::new(BinaryInputObject::new(1, "BI").unwrap()),
Box::new(BinaryOutputObject::new(1, "BO").unwrap()),
Box::new(BinaryValueObject::new(1, "BV").unwrap()),
Box::new(MultiStateInputObject::new(1, "MSI", 3).unwrap()),
Box::new(MultiStateOutputObject::new(1, "MSO", 3).unwrap()),
Box::new(MultiStateValueObject::new(1, "MSV", 3).unwrap()),
];
for object in &mut objects {
assert!(
object.intrinsic_reporting_requires_atomic_commit(),
"{} must opt into the atomic server path",
object.object_name()
);
object
.commit_event_transition_internal(EventTransitionCommit {
change: EventStateChange {
from: EventState::NORMAL,
to: EventState::OFFNORMAL,
},
coordinate: EventTransition::ToOffnormal,
ack_required: true,
timestamp: BACnetTimeStamp::SequenceNumber(73),
message_text: None,
})
.unwrap_or_else(|error| {
panic!(
"{} must implement the shared commit hook: {error:?}",
object.object_name()
)
});
assert_eq!(
object
.read_property(PropertyIdentifier::EVENT_STATE, None)
.unwrap(),
PropertyValue::Enumerated(EventState::OFFNORMAL.to_raw()),
"{} Event_State",
object.object_name()
);
assert_eq!(
object
.read_property(PropertyIdentifier::ACKED_TRANSITIONS, None)
.unwrap(),
PropertyValue::BitString {
unused_bits: 5,
data: vec![0x60],
},
"{} Acked_Transitions",
object.object_name()
);
}
}
#[test]
fn event_sequence_wraps_and_is_database_local() {
let mut first = ObjectDatabase::new();
let mut second = ObjectDatabase::new();
assert_eq!(first.next_event_sequence_number(), 0);
assert_eq!(first.next_event_sequence_number(), 1);
assert_eq!(second.next_event_sequence_number(), 0);
first.event_sequence_number = u16::MAX;
assert_eq!(first.next_event_sequence_number(), u16::MAX);
assert_eq!(first.next_event_sequence_number(), 0);
assert_eq!(second.next_event_sequence_number(), 1);
}
#[test]
fn event_sequence_reservation_is_non_consuming_until_confirmed() {
let mut db = ObjectDatabase::new();
let reservation = db.reserve_event_sequence_number();
assert_eq!(reservation.number(), 0);
assert_eq!(db.reserve_event_sequence_number().number(), 0);
assert!(db.confirm_event_sequence_number(reservation));
assert_eq!(db.reserve_event_sequence_number().number(), 1);
}
struct TestObject {
oid: ObjectIdentifier,
name: String,
}
impl BACnetObject for TestObject {
fn object_identifier(&self) -> ObjectIdentifier {
self.oid
}
fn object_name(&self) -> &str {
&self.name
}
fn read_property(
&self,
property: PropertyIdentifier,
_array_index: Option<u32>,
) -> Result<PropertyValue, Error> {
if property == PropertyIdentifier::OBJECT_NAME {
Ok(PropertyValue::CharacterString(self.name.clone()))
} else {
Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
})
}
}
fn write_property(
&mut self,
_property: PropertyIdentifier,
_array_index: Option<u32>,
_value: PropertyValue,
_priority: Option<u8>,
) -> Result<(), Error> {
Err(Error::Protocol {
class: ErrorClass::PROPERTY.to_raw() as u32,
code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
})
}
fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
Cow::Borrowed(&[PropertyIdentifier::OBJECT_NAME])
}
}
fn make_test_object(instance: u32) -> Box<dyn BACnetObject> {
Box::new(TestObject {
oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance).unwrap(),
name: format!("AI-{instance}"),
})
}
fn make_test_object_typed(
object_type: ObjectType,
instance: u32,
name: &str,
) -> Box<dyn BACnetObject> {
Box::new(TestObject {
oid: ObjectIdentifier::new(object_type, instance).unwrap(),
name: name.to_string(),
})
}
#[test]
fn add_and_get() {
let mut db = ObjectDatabase::new();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
db.add(make_test_object(1)).unwrap();
assert_eq!(db.len(), 1);
let obj = db.get(&oid).unwrap();
assert_eq!(obj.object_name(), "AI-1");
}
#[test]
fn get_nonexistent_returns_none() {
let db = ObjectDatabase::new();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
assert!(db.get(&oid).is_none());
}
#[test]
fn read_property_via_database() {
let mut db = ObjectDatabase::new();
db.add(make_test_object(1)).unwrap();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
let obj = db.get(&oid).unwrap();
let val = obj
.read_property(PropertyIdentifier::OBJECT_NAME, None)
.unwrap();
assert_eq!(val, PropertyValue::CharacterString("AI-1".into()));
}
#[test]
fn remove_object() {
let mut db = ObjectDatabase::new();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
db.add(make_test_object(1)).unwrap();
assert_eq!(db.len(), 1);
let removed = db.remove(&oid);
assert!(removed.is_some());
assert_eq!(db.len(), 0);
}
#[test]
fn list_objects() {
let mut db = ObjectDatabase::new();
db.add(make_test_object(1)).unwrap();
db.add(make_test_object(2)).unwrap();
let oids = db.list_objects();
assert_eq!(oids.len(), 2);
}
#[test]
fn find_by_type_returns_matching_objects() {
let mut db = ObjectDatabase::new();
db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "AI-1"))
.unwrap();
db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 2, "AI-2"))
.unwrap();
db.add(make_test_object_typed(ObjectType::BINARY_INPUT, 1, "BI-1"))
.unwrap();
db.add(make_test_object_typed(ObjectType::ANALOG_OUTPUT, 1, "AO-1"))
.unwrap();
let ai_oids = db.find_by_type(ObjectType::ANALOG_INPUT);
assert_eq!(ai_oids.len(), 2);
for oid in &ai_oids {
assert_eq!(oid.object_type(), ObjectType::ANALOG_INPUT);
}
let bi_oids = db.find_by_type(ObjectType::BINARY_INPUT);
assert_eq!(bi_oids.len(), 1);
assert_eq!(bi_oids[0].object_type(), ObjectType::BINARY_INPUT);
assert_eq!(bi_oids[0].instance_number(), 1);
let ao_oids = db.find_by_type(ObjectType::ANALOG_OUTPUT);
assert_eq!(ao_oids.len(), 1);
}
#[test]
fn find_by_type_returns_empty_for_no_matches() {
let mut db = ObjectDatabase::new();
db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "AI-1"))
.unwrap();
let results = db.find_by_type(ObjectType::BINARY_VALUE);
assert!(results.is_empty());
}
#[test]
fn find_by_type_on_empty_database() {
let db = ObjectDatabase::new();
let results = db.find_by_type(ObjectType::ANALOG_INPUT);
assert!(results.is_empty());
}
#[test]
fn iter_objects_yields_all_entries() {
let mut db = ObjectDatabase::new();
db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "AI-1"))
.unwrap();
db.add(make_test_object_typed(ObjectType::BINARY_INPUT, 1, "BI-1"))
.unwrap();
let items: Vec<_> = db.iter_objects().collect();
assert_eq!(items.len(), 2);
for (oid, obj) in &items {
assert_eq!(oid.object_type(), obj.object_identifier().object_type());
assert!(!obj.object_name().is_empty());
}
}
#[test]
fn iter_objects_on_empty_database() {
let db = ObjectDatabase::new();
assert_eq!(db.iter_objects().count(), 0);
}
#[test]
fn duplicate_name_rejected() {
let mut db = ObjectDatabase::new();
db.add(make_test_object_typed(
ObjectType::ANALOG_INPUT,
1,
"Sensor",
))
.unwrap();
let result = db.add(make_test_object_typed(
ObjectType::ANALOG_INPUT,
2,
"Sensor",
));
assert!(result.is_err());
assert_eq!(db.len(), 1); }
#[test]
fn replace_same_oid_allowed() {
let mut db = ObjectDatabase::new();
db.add(make_test_object_typed(
ObjectType::ANALOG_INPUT,
1,
"Sensor",
))
.unwrap();
db.add(make_test_object_typed(
ObjectType::ANALOG_INPUT,
1,
"Sensor-v2",
))
.unwrap();
assert_eq!(db.len(), 1);
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
assert_eq!(db.get(&oid).unwrap().object_name(), "Sensor-v2");
}
#[test]
fn find_by_name_works() {
let mut db = ObjectDatabase::new();
db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "Temp"))
.unwrap();
db.add(make_test_object_typed(ObjectType::BINARY_INPUT, 1, "Alarm"))
.unwrap();
let obj = db.find_by_name("Temp").unwrap();
assert_eq!(obj.object_identifier().instance_number(), 1);
assert_eq!(
obj.object_identifier().object_type(),
ObjectType::ANALOG_INPUT
);
assert!(db.find_by_name("NonExistent").is_none());
}
#[test]
fn remove_frees_name() {
let mut db = ObjectDatabase::new();
let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
db.add(make_test_object_typed(
ObjectType::ANALOG_INPUT,
1,
"Sensor",
))
.unwrap();
db.remove(&oid);
db.add(make_test_object_typed(
ObjectType::ANALOG_INPUT,
2,
"Sensor",
))
.unwrap();
assert_eq!(db.len(), 1);
}
}