Skip to main content

bacnet_objects/
database.rs

1//! ObjectDatabase — stores and retrieves BACnet objects by identifier.
2
3use crate::collections::{HashMap, HashSet};
4use crate::prelude::*;
5use alloc::sync::Arc;
6
7use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::ObjectIdentifier;
10
11use crate::clock::{ClockFrame, ClockReader};
12use crate::event_enrollment::EventEnrollmentMonitoredSource;
13use crate::traits::{BACnetObject, DeadlineWaker, MonotonicClock};
14
15mod averaging_sampling;
16mod event_algorithm_inhibit;
17mod event_log;
18mod input_references;
19mod local_device;
20mod membership;
21mod network_port;
22mod trend_poll;
23pub use local_device::LocalDevice;
24#[doc(hidden)]
25pub use membership::{MembershipWaker, MembershipWork};
26use trend_poll::TrendPollSchedule;
27
28/// A collection of BACnet objects, keyed by ObjectIdentifier.
29///
30/// Enforces Object_Name uniqueness within a device.
31/// Maintains secondary indexes for O(1) name lookup and O(1) type lookup.
32pub struct ObjectDatabase {
33    audit_owner: Option<alloc::sync::Weak<AuditOwnership>>,
34    network_port: Option<network_port::NetworkPortRegistration>,
35    objects: HashMap<ObjectIdentifier, Box<dyn BACnetObject>>,
36    trend_poll: TrendPollSchedule,
37    /// Shared Device clock reader. `None` is an explicit clockless database.
38    clock: Option<Arc<dyn ClockReader>>,
39    monotonic_clock: Option<Arc<MonotonicClock>>,
40    /// Bound into every object with the monotonic clock (#1384).
41    deadline_waker: Option<Arc<DeadlineWaker>>,
42    /// Device-local EventNotification ordering source for clockless operation.
43    event_sequence: Arc<EventSequence>,
44    /// Reverse index: object name → ObjectIdentifier for uniqueness enforcement.
45    name_index: HashMap<String, ObjectIdentifier>,
46    /// Type index: object type → set of ObjectIdentifiers for fast enumeration.
47    type_index: HashMap<ObjectType, Vec<ObjectIdentifier>>,
48    /// Event Enrollment objects whose private evaluator state must be reset
49    /// before it can be used again.
50    invalid_enrollment_eval_state: HashSet<ObjectIdentifier>,
51    /// Source ownership for custom Event Enrollment objects that implement
52    /// evaluation state but not the optional object-owned source channel.
53    enrollment_eval_sources: HashMap<ObjectIdentifier, EventEnrollmentMonitoredSource>,
54    /// What adding and removing objects left for the server (#1341, #1440).
55    membership: membership::MembershipQueue,
56}
57
58/// A non-consuming reservation of the database-local event sequence source.
59#[doc(hidden)]
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct EventSequenceReservation {
62    number: u16,
63}
64
65impl EventSequenceReservation {
66    /// The sequence number selected by this reservation.
67    #[doc(hidden)]
68    pub fn number(self) -> u16 {
69        self.number
70    }
71}
72
73mod audit_ownership;
74mod event_sequence;
75pub use audit_ownership::AuditOwnership;
76pub use event_sequence::EventSequence;
77
78impl Default for ObjectDatabase {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl ObjectDatabase {
85    /// Create an empty database.
86    pub fn new() -> Self {
87        Self {
88            objects: HashMap::new(),
89            trend_poll: TrendPollSchedule::default(),
90            clock: None,
91            monotonic_clock: None,
92            deadline_waker: None,
93            event_sequence: Arc::default(),
94            audit_owner: None,
95            network_port: None,
96            name_index: HashMap::new(),
97            type_index: HashMap::new(),
98            invalid_enrollment_eval_state: HashSet::new(),
99            enrollment_eval_sources: HashMap::new(),
100            membership: membership::MembershipQueue::default(),
101        }
102    }
103
104    /// Add an object to the database.
105    ///
106    /// Returns PROPERTY / DUPLICATE_NAME if another object already has the
107    /// same `object_name()`, as
108    /// [`check_name_available`](Self::check_name_available) does.
109    /// Replacing an object with the same OID is allowed unless an installed
110    /// Audit runtime protects its membership. Protection is checked before any binding.
111    ///
112    /// Once the object is in, every Pulse Converter whose Input_Reference
113    /// names it, or the object itself when it is one, has the reference
114    /// judged again (`ObjectDatabase::check_input_reference`); one whose
115    /// Reliability changes is queued for the server's COV fanout. A Schedule
116    /// holding a refused reference to it is queued for the server to retry
117    /// that reference (#1440).
118    pub fn add(&mut self, mut object: Box<dyn BACnetObject>) -> Result<(), Error> {
119        self.check_network_port_membership(&object.object_identifier())?;
120        self.check_audit_membership(&object.object_identifier(), true)?;
121        object.bind_clock_internal(self.clock.clone());
122        object.bind_monotonic_clock_internal(self.monotonic_clock.clone());
123        object.bind_deadline_waker_internal(self.deadline_waker.clone());
124        let oid = object.object_identifier();
125        let name = object.object_name().to_string();
126
127        // Check for name collision with a *different* object
128        self.check_name_available(&oid, &name)?;
129
130        // If replacing an existing object, remove its old name from the index
131        // and invalidate state owned by enrollments that monitor it.
132        if let Some(old) = self.objects.get(&oid) {
133            if let Some(reporter) = old.audit_reporter_internal() {
134                reporter.status_internal().configuration_changed();
135            }
136            let old_name = old.object_name().to_string();
137            self.name_index.remove(&old_name);
138            self.invalidate_enrollments_monitoring(&oid);
139        }
140
141        self.name_index.insert(name, oid);
142        let is_new = !self.objects.contains_key(&oid);
143        self.enrollment_eval_sources.remove(&oid);
144        self.trend_poll.retire(&oid);
145        self.objects.insert(oid, object);
146        self.audit_membership_changed(oid, true);
147        if is_new {
148            self.type_index
149                .entry(oid.object_type())
150                .or_default()
151                .push(oid);
152        }
153        self.membership_changed(oid, true);
154        Ok(())
155    }
156
157    /// Look up an object by its name. O(1) via the name index.
158    pub fn find_by_name(&self, name: &str) -> Option<&dyn BACnetObject> {
159        let oid = self.name_index.get(name)?;
160        self.objects.get(oid).map(|o| o.as_ref())
161    }
162
163    /// Check whether `new_name` is available for object `oid`.
164    ///
165    /// Returns `Ok(())` if the name is unused or already belongs to `oid`,
166    /// and PROPERTY / DUPLICATE_NAME if another object owns it. Clause 18.3
167    /// lists that code under the PROPERTY class, and the WriteProperty and
168    /// WritePropertyMultiple error tables (15.9.1.3.1, 15.10.1.3.1) give the
169    /// same pair; CreateObject's table (15.3.1.3.1) has no row for a name in
170    /// use, so its initial values answer as WriteProperty does.
171    pub fn check_name_available(
172        &self,
173        oid: &ObjectIdentifier,
174        new_name: &str,
175    ) -> Result<(), Error> {
176        if let Some(&owner) = self.name_index.get(new_name)
177            && owner != *oid
178        {
179            return Err(Error::protocol(
180                ErrorClass::PROPERTY,
181                ErrorCode::DUPLICATE_NAME,
182            ));
183        }
184        Ok(())
185    }
186
187    /// Update the name index after a successful Object_Name write.
188    ///
189    /// Call this after `write_property(OBJECT_NAME, …)` succeeds.
190    pub fn update_name_index(&mut self, oid: &ObjectIdentifier) {
191        if let Some(obj) = self.objects.get(oid) {
192            // Remove any old name mapping for this OID
193            self.name_index.retain(|_, v| v != oid);
194            // Insert the current name
195            self.name_index.insert(obj.object_name().to_string(), *oid);
196        }
197    }
198
199    /// Get a shared reference to an object by identifier.
200    pub fn get(&self, oid: &ObjectIdentifier) -> Option<&dyn BACnetObject> {
201        self.objects.get(oid).map(|o| o.as_ref())
202    }
203
204    /// Get a mutable reference to an object by identifier.
205    pub fn get_mut(&mut self, oid: &ObjectIdentifier) -> Option<&mut (dyn BACnetObject + '_)> {
206        if let Some(object) = self.objects.get_mut(oid) {
207            Some(object.as_mut())
208        } else {
209            None
210        }
211    }
212
213    /// Install an identity-preserving adapter through scoped structural access.
214    ///
215    /// The callback must preserve the object's identifier, name and type. It is
216    /// responsible for allocating/validating before changing the slot. This hook
217    /// neither rebinds clocks nor updates indexes, and does not provide rollback.
218    /// Poll ownership is retired before invocation, even if the callback returns
219    /// an error or unwinds after modifying the slot. Slot borrows cannot escape.
220    /// An installed Audit runtime can reject protected members before the callback.
221    ///
222    /// ```compile_fail
223    /// use bacnet_objects::{database::ObjectDatabase, traits::BACnetObject};
224    /// use bacnet_types::primitives::ObjectIdentifier;
225    /// fn escape<'a>(db: &'a mut ObjectDatabase, oid: &ObjectIdentifier)
226    ///     -> Result<Option<&'a mut Box<dyn BACnetObject>>, bacnet_types::error::Error> {
227    ///     db.with_object_adapter(oid, |slot| slot)
228    /// }
229    /// ```
230    pub fn with_object_adapter<R, F>(
231        &mut self,
232        oid: &ObjectIdentifier,
233        adapt: F,
234    ) -> Result<Option<R>, Error>
235    where
236        F: for<'slot> FnOnce(&'slot mut Box<dyn BACnetObject>) -> R,
237    {
238        self.check_network_port_membership(oid)?;
239        self.check_audit_membership(oid, false)?;
240        self.trend_poll.retire(oid);
241        Ok(self.objects.get_mut(oid).map(adapt))
242    }
243
244    /// Whether an Event Enrollment object's private evaluator state requires a
245    /// successful reset before reuse.
246    pub fn enrollment_eval_state_invalidated(&self, oid: &ObjectIdentifier) -> bool {
247        self.invalid_enrollment_eval_state.contains(oid)
248    }
249
250    /// Mark or clear the reset requirement for Event Enrollment private state.
251    pub fn set_enrollment_eval_state_invalidated(
252        &mut self,
253        oid: ObjectIdentifier,
254        invalidated: bool,
255    ) {
256        if invalidated {
257            self.invalid_enrollment_eval_state.insert(oid);
258        } else {
259            self.invalid_enrollment_eval_state.remove(&oid);
260        }
261    }
262
263    /// Return database-owned monitored-source state for a custom Event
264    /// Enrollment object.
265    pub fn enrollment_eval_source(
266        &self,
267        oid: &ObjectIdentifier,
268    ) -> Option<EventEnrollmentMonitoredSource> {
269        self.enrollment_eval_sources.get(oid).copied()
270    }
271
272    /// Store database-owned monitored-source state for a custom Event
273    /// Enrollment object.
274    pub fn set_enrollment_eval_source(
275        &mut self,
276        oid: ObjectIdentifier,
277        source: Option<EventEnrollmentMonitoredSource>,
278    ) {
279        if let Some(source) = source {
280            self.enrollment_eval_sources.insert(oid, source);
281        } else {
282            self.enrollment_eval_sources.remove(&oid);
283        }
284    }
285
286    fn invalidate_enrollments_monitoring(&mut self, monitored_oid: &ObjectIdentifier) {
287        let affected = self
288            .objects
289            .iter()
290            .filter_map(|(oid, object)| {
291                let source = object
292                    .enrollment_eval_source_internal()
293                    .flatten()
294                    .or_else(|| self.enrollment_eval_sources.get(oid).copied());
295                source
296                    .is_some_and(|source| source.0 == *monitored_oid)
297                    .then_some(*oid)
298            })
299            .collect::<Vec<_>>();
300        self.invalid_enrollment_eval_state.extend(affected);
301    }
302
303    /// Remove an object by identifier. Installed Audit membership protection
304    /// returns an error before removing the object or changing indexes.
305    /// Pulse Converters whose Input_Reference names a removed object have
306    /// the reference judged again, as [`add`](Self::add) does.
307    pub fn remove(
308        &mut self,
309        oid: &ObjectIdentifier,
310    ) -> Result<Option<Box<dyn BACnetObject>>, Error> {
311        self.check_network_port_membership(oid)?;
312        self.check_audit_membership(oid, false)?;
313        self.trend_poll.retire(oid);
314        if self.objects.contains_key(oid) {
315            self.invalidate_enrollments_monitoring(oid);
316        }
317        match self.objects.remove(oid) {
318            Some(mut obj) => {
319                if let Some(reporter) = obj.audit_reporter_internal() {
320                    reporter.status_internal().configuration_changed();
321                }
322                self.enrollment_eval_sources.remove(oid);
323                if obj.enrollment_eval_state_internal().is_some() {
324                    if obj
325                        .set_enrollment_eval_state_internal(Default::default())
326                        .is_err()
327                    {
328                        self.invalid_enrollment_eval_state.insert(*oid);
329                    } else {
330                        self.invalid_enrollment_eval_state.remove(oid);
331                    }
332                    let _ = obj.set_enrollment_eval_source_internal(None);
333                } else {
334                    self.invalid_enrollment_eval_state.remove(oid);
335                }
336                self.name_index.remove(obj.object_name());
337                if let Some(type_set) = self.type_index.get_mut(&oid.object_type()) {
338                    type_set.retain(|o| o != oid);
339                }
340                self.audit_membership_changed(*oid, false);
341                self.membership_changed(*oid, false);
342                Ok(Some(obj))
343            }
344            _ => Ok(None),
345        }
346    }
347
348    /// List all object identifiers in the database.
349    pub fn list_objects(&self) -> Vec<ObjectIdentifier> {
350        self.objects.keys().copied().collect()
351    }
352
353    /// Find all objects of a given type.
354    ///
355    /// Returns a `Vec` of `ObjectIdentifier`s whose object type matches `object_type`.
356    /// Useful for WhoHas, object enumeration, and similar queries.
357    pub fn find_by_type(&self, object_type: ObjectType) -> Vec<ObjectIdentifier> {
358        self.type_index
359            .get(&object_type)
360            .cloned()
361            .unwrap_or_default()
362    }
363
364    /// Iterate over all `(ObjectIdentifier, &dyn BACnetObject)` pairs.
365    ///
366    /// Avoids the double-lookup pattern of `list_objects()` followed by `get()`.
367    pub fn iter_objects(&self) -> impl Iterator<Item = (ObjectIdentifier, &dyn BACnetObject)> {
368        self.objects.iter().map(|(&oid, obj)| (oid, obj.as_ref()))
369    }
370
371    /// Bind one clock reader to the database and every object it contains.
372    ///
373    /// Devices added after this call receive the same reader in [`add`](Self::add).
374    pub fn set_clock_reader(&mut self, clock: Option<Arc<dyn ClockReader>>) {
375        self.clock = clock;
376        for object in self.objects.values_mut() {
377            object.bind_clock_internal(self.clock.clone());
378        }
379    }
380
381    /// Bind one process-local monotonic source to every contained object.
382    #[doc(hidden)]
383    pub fn set_monotonic_clock_internal(&mut self, clock: Option<Arc<MonotonicClock>>) {
384        self.trend_poll.clear();
385        self.monotonic_clock = clock;
386        for object in self.objects.values_mut() {
387            object.bind_monotonic_clock_internal(self.monotonic_clock.clone());
388        }
389    }
390
391    /// Bind the waker objects call when a write arms a monotonic deadline to
392    /// every contained object, and to each one added later.
393    #[doc(hidden)]
394    pub fn set_deadline_waker_internal(&mut self, waker: Option<Arc<DeadlineWaker>>) {
395        self.deadline_waker = waker;
396        for object in self.objects.values_mut() {
397            object.bind_deadline_waker_internal(self.deadline_waker.clone());
398        }
399    }
400
401    /// Read one coherent sample from the database's shared clock.
402    pub fn clock_frame(&self) -> Option<ClockFrame> {
403        self.clock.as_ref()?.read_clock()
404    }
405
406    #[doc(hidden)]
407    pub fn event_sequence_internal(&self) -> Arc<EventSequence> {
408        Arc::clone(&self.event_sequence)
409    }
410
411    /// Consume the next Device-local EventNotification sequence number.
412    ///
413    /// The counter wraps modulo 65536 as required by the timestamp production.
414    #[doc(hidden)]
415    pub fn next_event_sequence_number(&mut self) -> u16 {
416        let reservation = self.reserve_event_sequence_number();
417        let number = reservation.number();
418        let confirmed = self.confirm_event_sequence_number(reservation);
419        debug_assert!(
420            confirmed,
421            "a reservation cannot become stale without mutation"
422        );
423        number
424    }
425
426    /// Reserve the current sequence number without consuming it.
427    #[doc(hidden)]
428    pub fn reserve_event_sequence_number(&self) -> EventSequenceReservation {
429        EventSequenceReservation {
430            number: self.event_sequence.current(),
431        }
432    }
433
434    /// Consume an exact reservation if it is still current.
435    #[doc(hidden)]
436    pub fn confirm_event_sequence_number(&mut self, reservation: EventSequenceReservation) -> bool {
437        self.event_sequence.confirm(reservation.number)
438    }
439
440    /// Visit every `(ObjectIdentifier, &mut dyn BACnetObject)` pair.
441    ///
442    /// Mutable counterpart to [`iter_objects`](Self::iter_objects), used by
443    /// the intrinsic-reporting tick task to advance pending transitions. A
444    /// callback is used (rather than a returned `impl Iterator`) so the
445    /// `&mut dyn BACnetObject` borrow is tied to this call's lifetime.
446    pub fn for_each_object_mut<F>(&mut self, mut f: F)
447    where
448        F: FnMut(ObjectIdentifier, &mut dyn BACnetObject),
449    {
450        for (&oid, obj) in self.objects.iter_mut() {
451            f(oid, obj.as_mut());
452        }
453    }
454
455    /// Number of objects in the database.
456    pub fn len(&self) -> usize {
457        self.objects.len()
458    }
459
460    /// Whether the database is empty.
461    pub fn is_empty(&self) -> bool {
462        self.objects.is_empty()
463    }
464}
465
466#[cfg(test)]
467mod tests;
468
469#[cfg(test)]
470mod network_port_tests;