Skip to main content

bacnet_objects/
database.rs

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