bacnet_objects/traits.rs
1//! BACnetObject trait — the interface all BACnet objects implement.
2
3use std::any::Any;
4use std::borrow::Cow;
5use std::sync::Arc;
6use std::time::Duration;
7
8use bacnet_types::constructed::BACnetLogRecord;
9use bacnet_types::enums::{
10 ErrorClass, ErrorCode, EventState, LifeSafetyOperation, PropertyIdentifier,
11};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{BACnetTimeStamp, ObjectIdentifier, PropertyValue};
14
15use crate::audit::{AuditLogNotificationSink, AuditLogStorage};
16use crate::clock::ClockReader;
17use crate::event::{
18 EnrollmentSummaryCapability, EventStateChange, EventTransitionCommit,
19 EventTransitionCommitError, TransitionOutcome,
20};
21use crate::event_enrollment::{
22 EventEnrollmentEvalState, EventEnrollmentMonitoredSource, EventEnrollmentReliabilityCommit,
23};
24use crate::file::{FileConfiguration, FileStorage};
25use crate::log_buffer::LogRecordIdentity;
26
27/// Process-local monotonic time source used by internal object lifecycles.
28#[doc(hidden)]
29pub type MonotonicClock = dyn Fn() -> Duration + Send + Sync;
30
31mod defaults;
32use defaults::{array_property_default, historical_writable_default};
33
34/// Object-owned snapshot state retained for compatibility and local use.
35///
36/// The bundled server no longer invokes these tokens from Service 16:
37/// WritePropertyMultiple retains its successful prefix as required by Clause
38/// 15.10. Existing object implementations and downstream users may still use
39/// the hooks directly to preserve state hidden by property readback, including
40/// event resets, fallback-backed values, destructive writes, and derived state.
41#[doc(hidden)]
42pub struct WritePropertyRollback(Box<dyn Any + Send + Sync>);
43
44/// Result of applying a LifeSafetyOperation to an object.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum LifeSafetyOperationEffect {
47 /// The operation changed object state.
48 Applied,
49 /// The requested idempotent state was already present.
50 AlreadyApplied,
51}
52
53/// Detailed result of applying a `LifeSafetyOperation` to object-owned state.
54///
55/// `changed_properties` is ordered by the object's stable reporting order and
56/// contains each property at most once. Custom objects that implement only
57/// [`BACnetObject::apply_life_safety_operation`] remain source-compatible: the
58/// default detailed hook delegates to that method and reports no properties
59/// because the trait cannot truthfully infer their object-private mutations.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct LifeSafetyOperationOutcome {
62 /// Existing coarse operation result.
63 pub effect: LifeSafetyOperationEffect,
64 /// Exact properties whose committed readback changed.
65 pub changed_properties: Vec<PropertyIdentifier>,
66}
67
68/// Result of one object-owned reliability evaluation pass.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum ReliabilityEvaluation {
71 /// The object made no reliability-related mutation.
72 Unchanged,
73 /// The object successfully mutated its `Reliability` value.
74 Changed {
75 /// Reliability before the successful mutation.
76 old_reliability: u32,
77 /// Reliability after the successful mutation.
78 new_reliability: u32,
79 },
80}
81
82impl WritePropertyRollback {
83 /// Wrap object-private rollback state.
84 #[doc(hidden)]
85 pub fn new<T: Any + Send + Sync>(state: T) -> Self {
86 Self(Box::new(state))
87 }
88
89 /// Recover object-private rollback state.
90 #[doc(hidden)]
91 pub fn downcast<T: Any + Send + Sync>(self) -> Result<T, Error> {
92 self.0.downcast::<T>().map(|state| *state).map_err(|_| {
93 Error::Encoding("object received an incompatible write rollback token".into())
94 })
95 }
96}
97
98/// The core trait for all BACnet objects.
99///
100/// Implementors represent a single BACnet object (Device, AnalogInput, etc.)
101/// and provide read/write access to their properties.
102pub trait BACnetObject: Send + Sync {
103 /// The object's identifier (type + instance).
104 fn object_identifier(&self) -> ObjectIdentifier;
105
106 /// The object's name.
107 fn object_name(&self) -> &str;
108
109 /// Read a property value.
110 fn read_property(
111 &self,
112 property: PropertyIdentifier,
113 array_index: Option<u32>,
114 ) -> Result<PropertyValue, Error>;
115
116 /// Write a property value.
117 ///
118 /// Returning `Err` MUST leave the object unchanged. WritePropertyMultiple
119 /// retains earlier successful writes and cannot undo a mutation made by the
120 /// currently failing write, including for a write-only property.
121 fn write_property(
122 &mut self,
123 property: PropertyIdentifier,
124 array_index: Option<u32>,
125 value: PropertyValue,
126 priority: Option<u8>,
127 ) -> Result<(), Error>;
128
129 /// Return canonical metadata for this object's effective property rows.
130 ///
131 /// Migrated implementations return every supported standard row for the
132 /// current instance, including `PROPERTY_LIST`. Borrowed rows cover static
133 /// or object-owned metadata; owned rows support dynamically assembled
134 /// per-instance sets. An empty borrowed default marks an object as unmigrated.
135 fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
136 Cow::Borrowed(&[])
137 }
138
139 /// List all properties this object supports in the legacy projection.
140 ///
141 /// For migrated objects this includes Object_Identifier, Object_Name, and
142 /// Object_Type but omits Property_List. Reading the BACnet Property_List
143 /// property applies the additional wire-level universal-property filter.
144 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]>;
145
146 /// Bind or remove the database's shared wall-clock reader.
147 ///
148 /// Objects that do not expose clock-derived state ignore this internal
149 /// lifecycle hook.
150 #[doc(hidden)]
151 fn bind_clock_internal(&mut self, _clock: Option<Arc<dyn ClockReader>>) {}
152
153 /// Advance object-owned operations that use monotonic elapsed time.
154 ///
155 /// The server calls this internal hook from a dedicated lifecycle task.
156 /// `true` means readable state changed and generic COV processing is
157 /// required. The default is a source-compatible no-op for downstream
158 /// object implementations.
159 #[doc(hidden)]
160 fn advance_time_internal(&mut self, _elapsed: Duration) -> bool {
161 false
162 }
163
164 /// Bind the process-local monotonic source used when operations arm.
165 #[doc(hidden)]
166 fn bind_monotonic_clock_internal(&mut self, _clock: Option<Arc<MonotonicClock>>) {}
167
168 /// Advance operations to an absolute process-local monotonic instant.
169 #[doc(hidden)]
170 fn advance_monotonic_time_internal(&mut self, _now: Duration) -> bool {
171 false
172 }
173
174 /// Return the next absolute process-local operation deadline, if any.
175 #[doc(hidden)]
176 fn next_monotonic_deadline_internal(&self) -> Option<Duration> {
177 None
178 }
179
180 /// Freeze COV-readable state while the object's mutation lock is held.
181 #[doc(hidden)]
182 fn cov_snapshot_internal(&self) -> Option<Box<dyn BACnetObject>> {
183 None
184 }
185
186 /// Return the retained logical blink-request observation, when modeled.
187 ///
188 /// This is an internal conformance-test channel, not a BACnet property,
189 /// host callback, or physical-output claim.
190 #[doc(hidden)]
191 fn binary_lighting_blink_count_internal(&self) -> u64 {
192 0
193 }
194
195 /// Whether `write_property` accepts `property` for this object.
196 ///
197 /// PICS generation and runtime dispatch MUST consult this (or
198 /// `write_property` itself) rather than a separate heuristic, so the PICS
199 /// writable flags cannot drift from the actual write routes. The default
200 /// reproduces the historical PICS heuristic (see
201 /// [`historical_writable_default`]) so unmigrated object types keep their
202 /// current PICS output. Object implementations override to mirror their
203 /// real `write_property` arms exactly.
204 ///
205 /// Universal read-only properties (`OBJECT_IDENTIFIER`, `OBJECT_TYPE`,
206 /// `PROPERTY_LIST`, `STATUS_FLAGS`) are always non-writable and are
207 /// excluded by the default; overrides should preserve that invariant.
208 fn is_writable_property(&self, property: PropertyIdentifier) -> bool {
209 let metadata = self.property_metadata();
210 if metadata.is_empty() {
211 historical_writable_default(self.object_identifier().object_type(), property)
212 } else {
213 crate::property_metadata::is_writable_in_metadata(metadata.as_ref(), property)
214 }
215 }
216
217 /// Whether `property` accepts an array index on this object.
218 ///
219 /// Per Clause 12.1.5.1, only BACnetARRAY (and BACnetARRAY of BACnetLIST)
220 /// properties accept an array index; Clause 12.1.5.2 makes ReadRange the
221 /// only positional access to a BACnetLIST. The RP/RPM/WP/WPM service
222 /// handlers gate the request's array index on this query and reject a
223 /// supplied index on a non-array property with PROPERTY /
224 /// PROPERTY_IS_NOT_AN_ARRAY (Clause 15.5.1.3, Clause 15.9.1.3).
225 ///
226 /// The default reproduces the standard's classification (see
227 /// [`array_property_default`]): identifier-stable arrays are admitted
228 /// without consulting the object type, the identifiers whose datatype
229 /// changes with the object type (ALARM_VALUES / FAULT_VALUES,
230 /// LIST_OF_OBJECT_PROPERTY_REFERENCES, PRESENT_VALUE) classify by
231 /// `object_identifier().object_type()`, and everything else — scalars and
232 /// BACnetLIST properties — rejects the index. Object implementations with
233 /// vendor or per-instance array properties override.
234 fn is_array_property(&self, property: PropertyIdentifier) -> bool {
235 array_property_default(self.object_identifier().object_type(), property)
236 }
237
238 /// Capture compatibility state that property readback cannot preserve.
239 ///
240 /// The default returns `None`. Service 16 no longer calls this hook because
241 /// WritePropertyMultiple retains successful prefix writes. Implementations
242 /// and downstream callers may still use a token directly for object-local
243 /// snapshot/restore flows involving destructive or fallback-backed state.
244 /// Returning `None` MUST leave the object unchanged.
245 #[doc(hidden)]
246 fn capture_write_property_rollback(
247 &mut self,
248 _property: PropertyIdentifier,
249 _value: &PropertyValue,
250 ) -> Option<WritePropertyRollback> {
251 None
252 }
253
254 /// Restore a compatibility token returned by
255 /// [`capture_write_property_rollback`](Self::capture_write_property_rollback).
256 ///
257 /// The bundled server's Service 16 path does not invoke this hook.
258 #[doc(hidden)]
259 fn restore_write_property_rollback(
260 &mut self,
261 _rollback: WritePropertyRollback,
262 ) -> Result<(), Error> {
263 Err(Error::Encoding(
264 "object does not support this write rollback token".into(),
265 ))
266 }
267
268 /// Whether this object type can be created at runtime via CreateObject.
269 ///
270 /// Default `false`; override `true` only for types the network factory
271 /// (`handle_create_object`) actually constructs, so PICS createability
272 /// matches the runtime factory with no separate list to drift.
273 fn is_createable(&self) -> bool {
274 false
275 }
276
277 /// Whether this object type can be deleted at runtime via DeleteObject.
278 ///
279 /// Default `true`; override `false` on object types that are not
280 /// deleteable (e.g. `Device`, `NetworkPort`).
281 fn is_deleteable(&self) -> bool {
282 true
283 }
284
285 /// List the REQUIRED properties for this object type.
286 ///
287 /// Migrated objects derive this set from canonical `R` and `W` rows,
288 /// including Property_List. Unmigrated objects retain the historical four
289 /// universal properties. Service-specific consumers may exclude
290 /// Property_List where their protocol contract requires it.
291 fn required_properties(&self) -> Cow<'static, [PropertyIdentifier]> {
292 let metadata = self.property_metadata();
293 if !metadata.is_empty() {
294 return crate::property_metadata::required_properties_from_metadata(metadata.as_ref());
295 }
296 static UNIVERSAL: [PropertyIdentifier; 4] = [
297 PropertyIdentifier::OBJECT_IDENTIFIER,
298 PropertyIdentifier::OBJECT_NAME,
299 PropertyIdentifier::OBJECT_TYPE,
300 PropertyIdentifier::PROPERTY_LIST,
301 ];
302 Cow::Borrowed(&UNIVERSAL)
303 }
304
305 /// Whether this object type supports COV notifications.
306 ///
307 /// Override to return `true` for object types that can generate COV
308 /// notifications (analog, binary, multi-state I/O/V). Default is `false`.
309 fn supports_cov(&self) -> bool {
310 false
311 }
312
313 /// Take pending local target work from a Staging object.
314 ///
315 /// The default keeps every non-Staging object source-compatible. The
316 /// bundled server calls this only while it owns the database mutation
317 /// guard, then performs the returned writes after releasing that guard.
318 #[doc(hidden)]
319 fn take_staging_write_plan_internal(&mut self) -> Option<crate::staging::StagingWritePlan> {
320 None
321 }
322
323 /// Return the current Staging transition generation, if applicable.
324 #[doc(hidden)]
325 fn staging_generation_internal(&self) -> Option<u64> {
326 None
327 }
328
329 /// Commit one guarded Staging plan's success or failure to Reliability.
330 ///
331 /// Returns whether readable source state changed. Implementations ignore a
332 /// stale generation so older work cannot fault a newer transition.
333 #[doc(hidden)]
334 fn complete_staging_write_plan_internal(&mut self, _generation: u64, _success: bool) -> bool {
335 false
336 }
337
338 /// Return this object's GetEnrollmentSummary event capability.
339 ///
340 /// The default opts custom and downstream objects out. Implementations opt
341 /// in only when they own an actual configured or implied event algorithm
342 /// and shared committed-transition history.
343 #[doc(hidden)]
344 fn enrollment_summary_capability_internal(&self) -> Option<EnrollmentSummaryCapability> {
345 None
346 }
347
348 /// Whether a readable property supports property-specific COV.
349 ///
350 /// The default preserves the existing behavior of every other COV-capable
351 /// object family while enforcing the bounded standardized Life Safety
352 /// surface for source-compatible custom Point and Zone implementations.
353 fn supports_cov_property(&self, property: PropertyIdentifier) -> bool {
354 use bacnet_types::enums::ObjectType;
355
356 match self.object_identifier().object_type() {
357 ObjectType::LIFE_SAFETY_POINT => matches!(
358 property,
359 PropertyIdentifier::PRESENT_VALUE
360 | PropertyIdentifier::STATUS_FLAGS
361 | PropertyIdentifier::TRACKING_VALUE
362 | PropertyIdentifier::SILENCED
363 | PropertyIdentifier::OPERATION_EXPECTED
364 ),
365 ObjectType::LIFE_SAFETY_ZONE => matches!(
366 property,
367 PropertyIdentifier::PRESENT_VALUE
368 | PropertyIdentifier::STATUS_FLAGS
369 | PropertyIdentifier::SILENCED
370 | PropertyIdentifier::OPERATION_EXPECTED
371 ),
372 _ => self.supports_cov(),
373 }
374 }
375
376 /// COV increment for this object (analog objects only).
377 ///
378 /// Returns `Some(increment)` for objects that use COV_Increment filtering
379 /// (e.g., AnalogInput, AnalogOutput, AnalogValue). A notification fires only
380 /// when `|current_value - last_notified_value| >= increment`.
381 ///
382 /// Returns `None` for objects that notify on any state change (binary, multi-state).
383 fn cov_increment(&self) -> Option<f32> {
384 None
385 }
386
387 /// Set the OVERRIDDEN bit in StatusFlags.
388 ///
389 /// For software-only objects this is always FALSE per spec. Hardware
390 /// integrations can override to set TRUE when present_value is overridden
391 /// by physical means (e.g., a manual switch on an output).
392 fn set_overridden(&mut self, _overridden: bool) {}
393
394 /// Evaluate intrinsic reporting after a present_value change.
395 ///
396 /// This is the per-write entry point: it seeds (or cancels) a pending
397 /// delayed transition and fires immediately only when `Time_Delay == 0`.
398 /// It never advances the `Time_Delay` countdown — repeated writes to the
399 /// same value do not shorten the delay (per ASHRAE 135-2020 §13.2.4 the
400 /// countdown advances once per elapsed second via
401 /// [`tick_intrinsic_reporting`](Self::tick_intrinsic_reporting)).
402 ///
403 /// Returns `Some(TransitionOutcome)` whenever a transition is ready to be
404 /// committed, or
405 /// `None` when none did (no change, delay seeded, or the object does not
406 /// support intrinsic reporting). A cleared `Event_Enable` bit sets the
407 /// outcome's `distribute` flag to false rather than withholding the
408 /// transition. Built-in object families leave `Event_State`,
409 /// `Acked_Transitions`, event history, and fire-ready detector state
410 /// unchanged until [`commit_event_transition_internal`](Self::commit_event_transition_internal)
411 /// succeeds. Clause 13.2.2.1.4's transition actions run either way, and
412 /// `Event_Enable` disables only external distribution, downstream in the
413 /// notification-distribution process (Clause 13.2.5).
414 fn evaluate_intrinsic_reporting(&mut self) -> Option<TransitionOutcome> {
415 None
416 }
417
418 /// Advance the `Time_Delay` countdown for a pending transition.
419 ///
420 /// Called by the server's one-second intrinsic-reporting task. Fires the
421 /// pending transition when its delay elapses this tick, cancels it if the
422 /// triggering condition reverted, and returns `Some(TransitionOutcome)`
423 /// when a transition is ready. A fire-ready built-in proposal remains
424 /// retryable until the commit hook succeeds. As with
425 /// [`evaluate_intrinsic_reporting`](Self::evaluate_intrinsic_reporting),
426 /// `Event_Enable` is reported via `distribute`, not by returning `None`.
427 /// Objects without a delayed transition return `None`.
428 fn tick_intrinsic_reporting(&mut self) -> Option<TransitionOutcome> {
429 None
430 }
431
432 /// Whether intrinsic-reporting outcomes require the atomic commit hook.
433 ///
434 /// The default preserves the legacy contract used by downstream objects
435 /// and [`crate::impl_intrinsic_reporting!`]: evaluation mutates detector
436 /// state immediately, so the server must distribute its outcome without
437 /// attempting another commit. Built-in proposal-based objects override
438 /// this to return `true`; their outcomes are uncommitted and must be
439 /// rejected unless [`commit_event_transition_internal`](Self::commit_event_transition_internal)
440 /// succeeds.
441 #[doc(hidden)]
442 fn intrinsic_reporting_requires_atomic_commit(&self) -> bool {
443 false
444 }
445
446 /// Atomically commit all object-owned state for one event transition.
447 ///
448 /// The caller supplies an exact state change, its transition coordinate,
449 /// the resolved Notification Class `Ack_Required` value, a typed
450 /// timestamp, and an optional message. Implementations must validate the
451 /// coordinate and source state before changing `Event_State`,
452 /// `Acked_Transitions`, `Event_Time_Stamps`, or stored message state, and
453 /// must leave every value unchanged on error. A successful built-in
454 /// implementation also finalizes the detector's pending and fault-edge
455 /// state; a failed commit leaves that state retryable.
456 ///
457 /// This internal channel is deliberately separate from network property
458 /// writes and notification distribution. The default fails closed so an
459 /// object family participates only after it can lend all required state to
460 /// the shared commit kernel.
461 #[doc(hidden)]
462 fn commit_event_transition_internal(
463 &mut self,
464 _commit: EventTransitionCommit,
465 ) -> Result<(), EventTransitionCommitError> {
466 Err(EventTransitionCommitError::Unsupported)
467 }
468
469 /// Atomically commit Event Enrollment Reliability and transition state.
470 ///
471 /// This Event Enrollment-specific channel joins `Reliability` to the
472 /// existing atomic `Event_State`, `Acked_Transitions`, and
473 /// `Event_Time_Stamps` commit. Implementations must stage every supplied
474 /// value before assigning object-owned fields and leave them unchanged on
475 /// error. The default fails closed for custom objects that have not adopted
476 /// the stronger contract.
477 #[doc(hidden)]
478 fn commit_event_enrollment_reliability_internal(
479 &mut self,
480 _commit: EventEnrollmentReliabilityCommit,
481 ) -> Result<(), EventTransitionCommitError> {
482 Err(EventTransitionCommitError::Unsupported)
483 }
484
485 /// Evaluate this object's schedule for the given time.
486 ///
487 /// Returns `Some((new_value, refs))` if the present value changed, where `refs`
488 /// is the list of (object_identifier, property_identifier) pairs to write to.
489 /// Only meaningful for Schedule objects; default returns `None`.
490 fn tick_schedule(
491 &mut self,
492 _day_of_week: u8,
493 _hour: u8,
494 _minute: u8,
495 ) -> Option<(PropertyValue, Vec<(ObjectIdentifier, u32)>)> {
496 None
497 }
498
499 /// Acknowledge an alarm transition. Sets the corresponding bit in acked_transitions.
500 /// Returns Ok(()) if the object supports event detection, Err otherwise.
501 fn acknowledge_alarm(&mut self, _transition_bit: u8) -> Result<(), bacnet_types::error::Error> {
502 Err(bacnet_types::error::Error::Protocol {
503 class: bacnet_types::enums::ErrorClass::OBJECT.to_raw() as u32,
504 code: bacnet_types::enums::ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw()
505 as u32,
506 })
507 }
508
509 /// Correlate an acknowledgment with the latest committed transition.
510 #[doc(hidden)]
511 fn acknowledge_alarm_correlated_internal(
512 &mut self,
513 _event_state: EventState,
514 _timestamp: &BACnetTimeStamp,
515 ) -> Result<(), Error> {
516 Err(Error::Protocol {
517 class: ErrorClass::OBJECT.to_raw() as u32,
518 code: ErrorCode::NO_ALARM_CONFIGURED.to_raw() as u32,
519 })
520 }
521
522 /// Correlate an acknowledgment and report exact object-owned history.
523 ///
524 /// The default delegates to the source-compatible coarse hook and returns
525 /// no notification context. Custom objects that implement only the coarse
526 /// hook therefore retain their accepted-acknowledgment behavior without the
527 /// bundled server guessing historical states for an ACK notification.
528 #[doc(hidden)]
529 fn acknowledge_alarm_correlated_detailed_internal(
530 &mut self,
531 event_state: EventState,
532 timestamp: &BACnetTimeStamp,
533 ) -> Result<Option<EventStateChange>, Error> {
534 self.acknowledge_alarm_correlated_internal(event_state, timestamp)
535 .map(|()| None)
536 }
537
538 /// Apply a LifeSafetyOperation atomically to this object.
539 ///
540 /// Implementations must leave the object unchanged when returning `Err`.
541 /// They run synchronously under the object-database write lock and must be
542 /// fast, nonblocking, and panic-free. External or irreversible actuation
543 /// also needs an application-owned idempotency/replay contract. The default
544 /// reports that the object does not support this service.
545 fn apply_life_safety_operation(
546 &mut self,
547 _operation: LifeSafetyOperation,
548 ) -> Result<LifeSafetyOperationEffect, Error> {
549 Err(Error::Protocol {
550 class: ErrorClass::OBJECT.to_raw() as u32,
551 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
552 })
553 }
554
555 /// Apply a LifeSafetyOperation and report exact known property deltas.
556 ///
557 /// The default delegates to the source-compatible coarse hook and reports
558 /// no known properties rather than guessing about custom object state.
559 fn apply_life_safety_operation_detailed(
560 &mut self,
561 operation: LifeSafetyOperation,
562 ) -> Result<LifeSafetyOperationOutcome, Error> {
563 self.apply_life_safety_operation(operation)
564 .map(|effect| LifeSafetyOperationOutcome {
565 effect,
566 changed_properties: Vec::new(),
567 })
568 }
569
570 /// Set the next LifeSafetyOperation expected by trusted local logic.
571 ///
572 /// This is an application-facing state channel, not a network property
573 /// write. The default reports that the object does not support the state.
574 fn set_life_safety_operation_expected_internal(
575 &mut self,
576 _operation: LifeSafetyOperation,
577 ) -> Result<(), Error> {
578 Err(Error::Protocol {
579 class: ErrorClass::OBJECT.to_raw() as u32,
580 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
581 })
582 }
583
584 /// Apply an internally-detected `Event_State` transition.
585 ///
586 /// This is the **internal** lifecycle path for the algorithmically-derived
587 /// `Event_State` on objects such as Event Enrollment (ASHRAE 135-2020
588 /// Clause 12.12). It is deliberately distinct from the network
589 /// [`write_property`](Self::write_property) route: `Event_State` is
590 /// read-only over the network, so network writes are rejected while the
591 /// server's evaluator reaches the field through this method. Objects
592 /// without an algorithmic `Event_State` return
593 /// `OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED`.
594 ///
595 /// The **default** returns `Err`, so objects without an algorithmic
596 /// `Event_State` opt out. Objects that do model one (e.g.
597 /// `EventEnrollmentObject`) override this to store the value verbatim:
598 /// the only caller is a trusted internal evaluator that passes a modeled
599 /// [`EventState`], mirroring the inherent `set_event_state` builder and
600 /// the existing read arm. Network-facing validation — rejecting all
601 /// `Event_State` writes — lives in [`write_property`](Self::write_property),
602 /// not here. Implementations must leave `Event_State` unchanged when they
603 /// return `Err`.
604 fn set_event_state_internal(&mut self, _state: EventState) -> Result<(), Error> {
605 Err(Error::Protocol {
606 class: ErrorClass::OBJECT.to_raw() as u32,
607 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
608 })
609 }
610
611 /// Snapshot this object's Event Enrollment evaluation state, if it models one.
612 ///
613 /// This is the read half of the internal channel the server's Event
614 /// Enrollment evaluator uses to persist per-enrollment algorithm state
615 /// across evaluation cycles: the pending (delayed) transition countdown,
616 /// the CHANGE_OF_VALUE detection baseline (Clause 13.3.3: "the value of
617 /// the monitored value when a transition to NORMAL is indicated"), and
618 /// the value that caused the last transition to OFFNORMAL (Clause 13.3.2
619 /// condition (c)). Like [`set_event_state_internal`](Self::set_event_state_internal)
620 /// it deliberately bypasses the network property model: none of the three
621 /// slots is a BACnet property, and 135-2020 assigns their initialization
622 /// to local matters.
623 ///
624 /// The default returns `None` — objects without algorithmic event
625 /// detection carry no such state, and the evaluator treats `None` as an
626 /// empty state it cannot write back (delay honoring and the COV baseline
627 /// then stay unavailable, matching this crate's pre-delay behavior).
628 fn enrollment_eval_state_internal(&self) -> Option<EventEnrollmentEvalState> {
629 None
630 }
631
632 /// Store this object's Event Enrollment evaluation state.
633 ///
634 /// The write half of [`enrollment_eval_state_internal`](Self::enrollment_eval_state_internal).
635 /// The only caller is the trusted server evaluator, passing a state it
636 /// derived from a prior snapshot plus the current cycle's evaluation.
637 /// Implementations enforce the Clause 13.2.2.1 invariant by construction:
638 /// while `Event_Detection_Enable` is FALSE "no transitions shall occur",
639 /// so a write arriving then is refused rather than queued (and the
640 /// detection-disable reset has already cleared the fields).
641 ///
642 /// The **default** returns `Err`, so objects without enrollment evaluation
643 /// state opt out and the evaluator's write-back is dropped, never stored
644 /// into an object that does not model it.
645 fn set_enrollment_eval_state_internal(
646 &mut self,
647 _state: EventEnrollmentEvalState,
648 ) -> Result<(), Error> {
649 Err(Error::Protocol {
650 class: ErrorClass::OBJECT.to_raw() as u32,
651 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
652 })
653 }
654
655 /// Snapshot the monitored source that owns Event Enrollment private state.
656 ///
657 /// The outer `Option` indicates whether the object supports this channel;
658 /// the inner `Option` is empty before a source has been established.
659 /// The server stores source ownership in its object database when an
660 /// object implements evaluation state but leaves this channel unsupported.
661 fn enrollment_eval_source_internal(&self) -> Option<Option<EventEnrollmentMonitoredSource>> {
662 None
663 }
664
665 /// Store or clear the monitored source that owns Event Enrollment state.
666 fn set_enrollment_eval_source_internal(
667 &mut self,
668 _source: Option<EventEnrollmentMonitoredSource>,
669 ) -> Result<(), Error> {
670 Err(Error::Protocol {
671 class: ErrorClass::OBJECT.to_raw() as u32,
672 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
673 })
674 }
675
676 /// Set or clear one `Acked_Transitions` bit on a received event-state
677 /// transition.
678 ///
679 /// Implements the alarm-acknowledgment half of Clause 13.2.2.1.4's fourth
680 /// transition action ("indicate the transition to the Alarm-Acknowledgment
681 /// process"), per Clause 13.2.3: "When an event state transition is
682 /// received, the corresponding bit in Acked_Transitions is either set or
683 /// cleared. If the corresponding bit in Ack_Required is set, then the bit
684 /// in Acked_Transitions is cleared, otherwise it is set." The caller (the
685 /// server evaluator) resolves `Ack_Required` from the referenced
686 /// Notification Class object and passes the outcome as `acknowledged`;
687 /// this method performs only the bit maintenance.
688 ///
689 /// `transition_bit` is the transition direction's bit mask in
690 /// `Acked_Transitions`' internal bit0-first form (`0x01` TO_OFFNORMAL,
691 /// `0x02` TO_FAULT, `0x04` TO_NORMAL). The set half overlaps the
692 /// network-reachable [`acknowledge_alarm`](Self::acknowledge_alarm), which
693 /// also ORs the bit in per Clause 13.2.3's acknowledgment-indication
694 /// paragraph; the clear half has no network route by design (a property
695 /// write could fabricate or erase acknowledgments — see the
696 /// `write_generic_event_properties!` denial comment).
697 ///
698 /// The **default** returns `Err`, so objects without an algorithmic
699 /// `Acked_Transitions` opt out.
700 fn set_acked_transitions_internal(
701 &mut self,
702 _transition_bit: u8,
703 _acknowledged: bool,
704 ) -> Result<(), Error> {
705 Err(Error::Protocol {
706 class: ErrorClass::OBJECT.to_raw() as u32,
707 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
708 })
709 }
710
711 /// Evaluate and, when necessary, mutate this object's `Reliability`.
712 ///
713 /// Reliability evaluation is object-owned: an implementation decides
714 /// whether and how its internal conditions affect `Reliability`, performs
715 /// the mutation itself, and returns [`ReliabilityEvaluation::Changed`] only
716 /// after that mutation succeeds. [`ReliabilityEvaluation::Unchanged`] means
717 /// the object made no mutation. Returning `Err` MUST leave all object state
718 /// unchanged.
719 ///
720 /// The default opts out without changing state, preserving source
721 /// compatibility for object implementations that do not own a reliability
722 /// evaluation algorithm.
723 fn evaluate_reliability_internal(&mut self) -> Result<ReliabilityEvaluation, Error> {
724 Ok(ReliabilityEvaluation::Unchanged)
725 }
726
727 /// Whether periodic object-owned reliability evaluation is currently
728 /// inhibited.
729 ///
730 /// The default is FALSE so existing and downstream object implementations
731 /// remain source-compatible. Objects that elect the optional
732 /// Reliability_Evaluation_Inhibit property override this internal
733 /// predicate; reporting of an already-applied Reliability transition is
734 /// intentionally unaffected.
735 #[doc(hidden)]
736 fn reliability_evaluation_inhibited_internal(&self) -> bool {
737 false
738 }
739
740 /// Apply an internally-derived `Reliability` value.
741 ///
742 /// This is the **internal** reliability-evaluation path, distinct from the
743 /// network [`write_property`](Self::write_property) route. Implementations
744 /// enforce symmetric ownership: clients may write while `Out_Of_Service`
745 /// is TRUE, and internal evaluation may write while it is FALSE. ASHRAE
746 /// 135-2020 Clause 3.2 defines reliability evaluation as "the process by
747 /// which an object determines its reliability and thus the value to set
748 /// into its Reliability property."
749 ///
750 /// The default rejects the operation, so object types without an internal
751 /// reliability-evaluation process remain unaffected.
752 fn set_reliability_internal(&mut self, _reliability: u32) -> Result<(), Error> {
753 Err(Error::Protocol {
754 class: ErrorClass::OBJECT.to_raw() as u32,
755 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
756 })
757 }
758
759 /// Apply the logical `Present_Value` supplied by an Input's application.
760 ///
761 /// This narrow internal hook is distinct from the network
762 /// [`write_property`](Self::write_property) route. Built-in Analog Input,
763 /// Binary Input, and Multi-state Input objects opt in. Their implementations
764 /// accept application updates only while in service; rejecting them while
765 /// `Out_Of_Service` is TRUE is a local ownership policy that protects the
766 /// client's simulation value, not a requirement imposed by the Standard.
767 ///
768 /// The default fails closed so commandable and other object families do not
769 /// acquire privileged `Present_Value` write authority through this hook.
770 fn set_present_value_internal(&mut self, _value: PropertyValue) -> Result<(), Error> {
771 Err(Error::Protocol {
772 class: ErrorClass::OBJECT.to_raw() as u32,
773 code: ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.to_raw() as u32,
774 })
775 }
776
777 /// Borrow this object's Audit Log query storage, if it has any.
778 ///
779 /// This read-only, type-erased channel lets the bundled server execute an
780 /// AuditLogQuery against an object's already-loaded retained records. It
781 /// deliberately does not expose persistence, mutation, or object
782 /// downcasting. The **default** returns `None`; an Audit Log object that
783 /// does not opt in is reported as SERVICES /
784 /// OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED.
785 fn audit_log_storage_internal(&self) -> Option<&dyn AuditLogStorage> {
786 None
787 }
788
789 /// Mutably borrow this object's Audit notification receiver capability.
790 ///
791 /// The default opts out. Implementations own their persistence transaction
792 /// and must leave memory unchanged when the batch cannot be committed.
793 fn audit_log_notification_sink_internal(
794 &mut self,
795 ) -> Option<&mut dyn AuditLogNotificationSink> {
796 None
797 }
798
799 /// Borrow this object's trusted local File configuration capability.
800 ///
801 /// The default returns `None`. The built-in [`crate::file::FileObject`]
802 /// opts in so bindings can configure its pending payload, access method,
803 /// and growth limits without general object downcasting or a parallel
804 /// state cache.
805 #[doc(hidden)]
806 fn file_configuration_internal(&self) -> Option<&dyn FileConfiguration> {
807 None
808 }
809
810 /// Mutably borrow this object's trusted local File configuration capability.
811 ///
812 /// The default returns `None`; payload mutations remain object-owned and
813 /// must preserve the same accounting and metadata behavior as the built-in
814 /// File setters.
815 #[doc(hidden)]
816 fn file_configuration_internal_mut(&mut self) -> Option<&mut dyn FileConfiguration> {
817 None
818 }
819
820 /// Borrow this object's File storage, if it has any.
821 ///
822 /// This is the read half of the **internal** channel the server's
823 /// AtomicReadFile handler uses to reach file contents. Like
824 /// [`set_event_state_internal`](Self::set_event_state_internal) it
825 /// bypasses the property model on purpose: Table 12-16 (ASHRAE 135-2020
826 /// Clause 12.13) defines no File Data property, so file contents are
827 /// reachable only through the Clause 14 File Access Services.
828 ///
829 /// The **default** returns `None`, so object types without a file opt
830 /// out; the server reports `None` on a File-typed object as SERVICES /
831 /// FILE_ACCESS_DENIED (Clause 18: "a file that is currently locked or
832 /// otherwise not accessible") rather than reading it as empty.
833 /// Applications backing a File object with their own storage — a disk
834 /// file, a firmware partition — implement [`FileStorage`] and return
835 /// `Some`.
836 fn file_storage_internal(&self) -> Option<&dyn FileStorage> {
837 None
838 }
839
840 /// Mutably borrow this object's File storage, if it has any.
841 ///
842 /// The write half of
843 /// [`file_storage_internal`](Self::file_storage_internal), used by the
844 /// AtomicWriteFile handler after the read-only and access-method gates
845 /// have passed. The **default** returns `None`.
846 fn file_storage_internal_mut(&mut self) -> Option<&mut dyn FileStorage> {
847 None
848 }
849
850 /// Return stable identities aligned with this object's resident log records.
851 ///
852 /// Implementing log objects return identities oldest-to-newest, in the
853 /// same order as their public resident-record view and `LOG_BUFFER`
854 /// projection. Sequence numbers are object-owned metadata and never part
855 /// of a projected BACnet record payload. Non-log objects return `None`.
856 fn log_record_identities_internal(&self) -> Option<Vec<LogRecordIdentity>> {
857 None
858 }
859
860 /// Add a trend log record (only meaningful for TrendLog / TrendLogMultiple).
861 ///
862 /// Default is a no-op. TrendLog objects override to append to their buffer.
863 fn add_trend_record(&mut self, _record: BACnetLogRecord) {}
864
865 /// Fallible internal trend-record insertion used by the server poller.
866 ///
867 /// The default preserves source compatibility with existing implementors by
868 /// invoking the legacy void hook and reporting success. Built-in log objects
869 /// override this to surface mandatory status-timestamp failures atomically.
870 #[doc(hidden)]
871 fn try_add_trend_record_internal(&mut self, record: BACnetLogRecord) -> Result<(), Error> {
872 self.add_trend_record(record);
873 Ok(())
874 }
875}