Skip to main content

bacnet_server/server/
local_writes.rs

1use super::cov_notify_context::CovNotifyContext;
2use super::*;
3use crate::command_lists::TakenRuns;
4use crate::handlers::{WriteCommitObserver, WriteTarget};
5use bacnet_objects::staging::StagingWritePlan;
6use bacnet_types::constructed::BACnetRecipient;
7use futures_util::FutureExt;
8use tracing::error;
9
10#[path = "local_write_finish.rs"]
11mod finish;
12pub(super) use finish::Committed;
13
14#[cfg(test)]
15#[path = "input_present_value_tests.rs"]
16mod input_present_value_tests;
17
18#[cfg(test)]
19#[path = "staging_local_writes_tests.rs"]
20mod staging_local_writes_tests;
21
22#[cfg(test)]
23#[path = "local_array_index_tests.rs"]
24mod local_array_index_tests;
25
26#[cfg(test)]
27#[path = "local_index_staged_release_tests.rs"]
28mod local_index_staged_release_tests;
29
30/// What a local mutation is, and on whose behalf.
31///
32/// Inputs and noncommandable Values distinguish application updates from
33/// network-equivalent writes, including their Out_Of_Service ownership checks.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(super) enum LocalWrite<'s> {
36    /// A trusted local program performing a network-equivalent property write.
37    Property {
38        property: PropertyIdentifier,
39        array_index: Option<u32>,
40        priority: Option<u8>,
41    },
42    /// The application supplying a supported object's logical `Present_Value`.
43    ApplicationPresentValue,
44    /// The application supplying a Loop's measured `Controlled_Variable_Value`.
45    ApplicationControlledVariableValue,
46    /// The application supplying one sample to an Averaging object.
47    ApplicationAveragingSample,
48    /// The application reporting an Averaging sample attempt that produced no
49    /// value.
50    ApplicationAveragingMiss,
51    /// The application supplying a Life Safety object's `Tracking_Value`.
52    ApplicationTrackingValue,
53    /// One value of an inbound WriteGroup for a Channel's `Present_Value`, at
54    /// the priority the request gave it (Clause 15.11). Under the write guard
55    /// the Channel takes it only while its Control_Groups still holds `group`
56    /// and its Channel_Number is still `number`; otherwise the write is
57    /// skipped and queues nothing. With `inhibit_delay`, the run it queues
58    /// loses its delays when Allow_Group_Delay_Inhibit is TRUE at that moment.
59    /// It comes from the network: `requester`, the device that sent the
60    /// WriteGroup, is the source its Audit record names, with no invoke ID
61    /// (#1318).
62    WriteGroup {
63        group: u32,
64        number: u16,
65        priority: u8,
66        inhibit_delay: bool,
67        requester: &'s BACnetRecipient,
68    },
69}
70
71impl<T: TransportPort + 'static> BACnetServer<T> {
72    /// Arm or rearm a Life Safety object from trusted local application logic.
73    ///
74    /// This uses the object-internal state channel under the database write
75    /// lock. Network WriteProperty and WritePropertyMultiple remain unable to
76    /// forge `Operation_Expected`. After the lock is released, an actual
77    /// `Operation_Expected` readback change uses the exact Life Safety COV path;
78    /// rearming to the current value emits no notification.
79    pub async fn set_life_safety_operation_expected_local(
80        &self,
81        oid: &ObjectIdentifier,
82        operation: LifeSafetyOperation,
83    ) -> Result<(), Error> {
84        self.active_network()?;
85        let changes = {
86            let mut db = self.db.write().await;
87            let snapshots = crate::life_safety_cov::LifeSafetyCovSnapshots::capture_oid(&db, *oid);
88            let object = db.get_mut(oid).ok_or_else(|| Error::Protocol {
89                class: ErrorClass::OBJECT.to_raw() as u32,
90                code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
91            })?;
92            object.set_life_safety_operation_expected_internal(operation)?;
93            let changes = snapshots.changes(&db, std::slice::from_ref(oid));
94            let capture = self.cov_table.read().await.timed_capture_exact(&changes);
95            capture.run(&db);
96            changes
97        };
98        for change in changes {
99            Self::fire_life_safety_cov_notifications(
100                &self.local_cov_context(),
101                &change.object_identifier,
102                &change.changed_properties,
103            )
104            .await;
105        }
106        Ok(())
107    }
108
109    /// Write a property on a local object and fire the same post-write COV
110    /// and event notifications that a network [`WriteProperty`] does. When target
111    /// Audit reporting is configured, the same observer records eligible local
112    /// writes with local Device provenance and no invoke ID; Device recipient
113    /// changes retain their sole old/new pair owner. Eligible actual AV/BV Audit
114    /// policy changes reserve immediate delivery before assignment; unavailable
115    /// resources return SERVICES/SERVICE_REQUEST_DENIED without a policy change.
116    ///
117    /// This is the server-owned local-mutation entry point: it performs the
118    /// write under the database lock — routing `OBJECT_NAME` through the name
119    /// uniqueness check and index refresh, exactly like the network handler —
120    /// then releases the lock and runs the COV/event trigger path so a
121    /// subscription observes a local mutation just as it would a network one.
122    /// `source` explicitly selects the Device or an existing initiating object.
123    /// Tracked commands require a concrete selected Device; the selected Device
124    /// retains correction ownership regardless of the local initiator. Unrelated
125    /// properties preserve their behavior without a usable command origin.
126    /// Low-level object setters deliberately bypass this notification owner.
127    ///
128    /// A NULL to a property that isn't commandable and has no NULL in its
129    /// datatype succeeds and changes nothing, as over the network (#1396):
130    /// Audit records it, and no COV, event or Schedule work follows it.
131    ///
132    /// An array index is checked first, as the WriteProperty handler checks
133    /// it (#1426): UNKNOWN_PROPERTY for a property the object doesn't hold,
134    /// PROPERTY_IS_NOT_AN_ARRAY for one that isn't an array. Neither the
135    /// object nor Audit sees such a write. The writes a Command or Channel
136    /// run makes here get the same check.
137    ///
138    /// # Cancellation
139    ///
140    /// Dropped before the write commits, the future makes no change. Once it
141    /// has committed, the work the write owes (the event pass, the COV
142    /// fanout, a re-evaluated Schedule's target fanout, a Staging plan, and
143    /// the Command or Channel runs it started) runs as a task of its own in
144    /// the server's request task set, which this future only waits for, so
145    /// dropping the future skips none of it (#1367): subscribers hear of the
146    /// change, and a run ends as it would have, reported to its property
147    /// subscribers. `stop()` aborts that task with the other request tasks,
148    /// as it does a request handler: what it hadn't sent by then is not
149    /// sent, and a run it hadn't started ends as if none of its writes were
150    /// made (#1324), In_Process FALSE with each command unsuccessful, or
151    /// Write_Status FAILED. The call still returns `Ok(())` then: the write
152    /// was made. A panic in that work is raised again in the caller, as it
153    /// would be were the work done in place.
154    ///
155    /// The future must be awaited inside a Tokio runtime, which the task is
156    /// spawned onto: outside one it fails with [`Error::Encoding`] before
157    /// anything is written.
158    ///
159    /// [`WriteProperty`]: bacnet_services::write_property::WritePropertyRequest
160    pub async fn write_local(
161        &self,
162        oid: &ObjectIdentifier,
163        property: PropertyIdentifier,
164        array_index: Option<u32>,
165        value: PropertyValue,
166        priority: Option<u8>,
167        source: crate::LocalCommandSource,
168    ) -> Result<(), Error> {
169        self.write_local_as(
170            oid,
171            LocalWrite::Property {
172                property,
173                array_index,
174                priority,
175            },
176            value,
177            Some(source),
178        )
179        .await
180    }
181
182    /// Supply a supported object's logical `Present_Value` from Rust application code.
183    ///
184    /// Analog, Binary and Multi-state Inputs, noncommandable Values, Loop
185    /// (the control algorithm's output) and Life Safety Point and Zone opt in.
186    /// Binary Input values are logical INACTIVE/ACTIVE states after Polarity,
187    /// not raw physical states. The update runs the existing post-write
188    /// intrinsic-event and COV processing after the database lock is released;
189    /// configured delays and distribution policy still control delivery, and a
190    /// COV_Increment still filters Present_Value reports.
191    ///
192    /// Applications are denied while `Out_Of_Service` is TRUE to protect a
193    /// client's simulation value. This is local policy for Inputs and required
194    /// by the object clauses for the supported Values and Loop. NULL is an
195    /// invalid application value. Other object families fail closed; use
196    /// [`BACnetServer::write_local`] for network-equivalent writes and sourced
197    /// commands on commandable objects. The Python binding exposes this as
198    /// `BACnetServer.set_present_value_local`.
199    ///
200    /// A Life Safety Point or Zone takes an Enumerated BACnetLifeSafetyState,
201    /// a standard value or one from 256 to 65535, in or out of service, since
202    /// clients never write its Present_Value. The update leaves
203    /// Tracking_Value, Silenced and Operation_Expected alone, so any latching
204    /// until reset is the application's (see
205    /// [`BACnetServer::set_tracking_value_local`]). A change notifies
206    /// SubscribeCOV and Present_Value property subscribers through the Life
207    /// Safety COV path. The built-in objects run no intrinsic reporting, so
208    /// the post-write event pass raises nothing for them.
209    ///
210    /// Like [`write_local`](Self::write_local), it must be awaited inside a
211    /// Tokio runtime, failing before anything is written outside one, and a
212    /// caller dropped once the write has committed skips none of the work
213    /// the write owes (#1367).
214    ///
215    /// [`BACnetObject::set_present_value_internal`]: bacnet_objects::traits::BACnetObject::set_present_value_internal
216    pub async fn set_present_value_local(
217        &self,
218        oid: &ObjectIdentifier,
219        value: PropertyValue,
220    ) -> Result<(), Error> {
221        self.write_local_as(oid, LocalWrite::ApplicationPresentValue, value, None)
222            .await
223    }
224
225    /// Supply a Loop's measured `Controlled_Variable_Value` from the
226    /// application that runs its control algorithm.
227    ///
228    /// The value is a finite REAL: another datatype fails with PROPERTY /
229    /// INVALID_DATA_TYPE and NaN or an infinity with PROPERTY /
230    /// VALUE_OUT_OF_RANGE. An unknown object fails with OBJECT /
231    /// UNKNOWN_OBJECT and any object other than a Loop with OBJECT /
232    /// OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, the same errors as
233    /// [`BACnetServer::set_present_value_local`]. Unlike Present_Value it is
234    /// accepted while `Out_Of_Service` is TRUE, because Out_Of_Service
235    /// decouples only the output and Reliability from the algorithm.
236    ///
237    /// The update runs the same post-write COV processing as
238    /// `set_present_value_local` once the database lock is released. A
239    /// SubscribeCOVProperty on `Controlled_Variable_Value` is notified of the
240    /// change; a SubscribeCOV on the Loop is not, since the Loop's COV report
241    /// carries this value without being triggered by it, so its next report
242    /// carries the new value. The property stays read-only over the network.
243    ///
244    /// Like [`write_local`](Self::write_local), it must be awaited inside a
245    /// Tokio runtime, failing before anything is written outside one, and a
246    /// caller dropped once the write has committed skips none of the work
247    /// the write owes (#1367).
248    pub async fn set_controlled_variable_value_local(
249        &self,
250        oid: &ObjectIdentifier,
251        value: PropertyValue,
252    ) -> Result<(), Error> {
253        self.write_local_as(
254            oid,
255            LocalWrite::ApplicationControlledVariableValue,
256            value,
257            None,
258        )
259        .await
260    }
261
262    /// Record one sample for an Averaging object, taken by the application.
263    ///
264    /// The server samples an object that holds an `Object_Property_Reference`
265    /// itself, every Window_Interval / Window_Samples seconds (#1144). For an
266    /// object without one, the application samples whatever it averages,
267    /// spacing its readings that far apart, and passes each result here. On an
268    /// object the server samples, a call is one more attempt in the window
269    /// and doesn't move the server's schedule. A value may be
270    /// a BOOLEAN (FALSE and TRUE count as 0 and 1), Signed, Unsigned,
271    /// Enumerated or finite REAL; the object keeps its statistics in REAL
272    /// (Clause 12.5). `None` records an attempt that produced no value, such
273    /// as a failed read: it counts toward Attempted_Samples but not
274    /// Valid_Samples. Another datatype, Double included, fails with PROPERTY /
275    /// INVALID_DATA_TYPE and NaN or an infinity with PROPERTY /
276    /// VALUE_OUT_OF_RANGE, and a refused sample counts as neither attempted
277    /// nor valid. An unknown object fails with OBJECT / UNKNOWN_OBJECT and any
278    /// object other than an Averaging object with OBJECT /
279    /// OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as with
280    /// [`BACnetServer::set_present_value_local`].
281    ///
282    /// Each call fills the next slot of the object's window, dropping the
283    /// oldest once Window_Samples are held, and Minimum_Value, Maximum_Value,
284    /// Average_Value, Attempted_Samples and Valid_Samples change together
285    /// under the database lock; the server's COV processing runs once the
286    /// lock is released. Averaging has no Table 13-1 row, so SubscribeCOV on
287    /// it is refused, but SubscribeCOVProperty and
288    /// SubscribeCOVPropertyMultiple are admitted: a numeric property is
289    /// reported when it moves by the subscription's COV increment, or on any
290    /// change when the subscription gives none (Table 13-1a). A move to or
291    /// from the NaN or infinite value of an empty window is always reported,
292    /// and staying at it never is. The report has no Status_Flags because the
293    /// object has none. The Python binding exposes this as
294    /// `BACnetServer.add_averaging_sample_local`.
295    ///
296    /// Like [`write_local`](Self::write_local), it must be awaited inside a
297    /// Tokio runtime, failing before anything is written outside one, and a
298    /// caller dropped once the write has committed skips none of the work
299    /// the write owes (#1367).
300    pub async fn add_averaging_sample_local(
301        &self,
302        oid: &ObjectIdentifier,
303        sample: Option<PropertyValue>,
304    ) -> Result<(), Error> {
305        match sample {
306            Some(value) => {
307                self.write_local_as(oid, LocalWrite::ApplicationAveragingSample, value, None)
308                    .await
309            }
310            None => {
311                self.write_local_as(
312                    oid,
313                    LocalWrite::ApplicationAveragingMiss,
314                    PropertyValue::Null,
315                    None,
316                )
317                .await
318            }
319        }
320    }
321
322    /// Supply a Life Safety Point's or Zone's `Tracking_Value`, the live state
323    /// the application derived, while the server holds the object.
324    ///
325    /// The value is an Enumerated naming a standard BACnetLifeSafetyState or
326    /// one from the proprietary range 256..=65535. A reserved or larger number
327    /// fails with PROPERTY / VALUE_OUT_OF_RANGE and another datatype with
328    /// PROPERTY / INVALID_DATA_TYPE. An unknown object fails with OBJECT /
329    /// UNKNOWN_OBJECT and any object other than a Life Safety Point or Zone
330    /// with OBJECT / OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED, as with
331    /// [`BACnetServer::set_present_value_local`]. Present_Value, Silenced and
332    /// Operation_Expected are left as they are: the object derives nothing
333    /// from Tracking_Value.
334    ///
335    /// While `Out_Of_Service` is TRUE the value replaces the one set aside for
336    /// the return to service, and a client's simulated Tracking_Value keeps
337    /// being served (#1108); the return to service then serves the latest
338    /// application value and notifies its subscribers. In service, a change
339    /// reaches SubscribeCOVProperty and SubscribeCOVPropertyMultiple
340    /// subscribers of Tracking_Value through the Life Safety COV path once the
341    /// database lock is released; whole-object SubscribeCOV reports don't
342    /// carry it. The property stays read-only over the network in service.
343    /// The Python binding exposes this as `BACnetServer.set_tracking_value_local`.
344    ///
345    /// Like [`write_local`](Self::write_local), it must be awaited inside a
346    /// Tokio runtime, failing before anything is written outside one, and a
347    /// caller dropped once the write has committed skips none of the work
348    /// the write owes (#1367).
349    pub async fn set_tracking_value_local(
350        &self,
351        oid: &ObjectIdentifier,
352        value: PropertyValue,
353    ) -> Result<(), Error> {
354        self.write_local_as(oid, LocalWrite::ApplicationTrackingValue, value, None)
355            .await
356    }
357
358    /// Commit the write, then hand what it owes to a task of its own in the
359    /// request task set and wait for that task (#1367). The commit holds the
360    /// database guard to the hand-off with no wait in between, so a caller
361    /// dropped once the write has committed leaves the task running: its
362    /// event pass, COV fanout, Schedule fanout, Staging plan and runs all go
363    /// ahead. `stop()` aborts the task with the other request tasks. With
364    /// no Tokio runtime to run that task on, nothing is written.
365    async fn write_local_as(
366        &self,
367        oid: &ObjectIdentifier,
368        write: LocalWrite<'_>,
369        value: PropertyValue,
370        source: Option<crate::LocalCommandSource>,
371    ) -> Result<(), Error> {
372        let runtime = tokio::runtime::Handle::try_current()
373            .map_err(|_| Error::Encoding("Local writes require a Tokio runtime".into()))?;
374        self.active_network()?;
375        let Some(committed) = self
376            .local_writer()
377            .commit(oid, write, value, source)
378            .await?
379        else {
380            return Ok(());
381        };
382        let runner = super::command_runs::CommandRunner::for_server(self);
383        let (done, finished) = oneshot::channel();
384        // A closed set (the server is stopping) drops the task, and with it
385        // the guard and the runs, which then end unsuccessful (#1324).
386        self.request_tasks.spawn_on(
387            async move {
388                let work = std::panic::AssertUnwindSafe(async {
389                    let runs = runner.writer().finish(committed).await;
390                    if !runs.is_empty() {
391                        runner.start(runs);
392                    }
393                });
394                // A panic goes back to the caller. With the caller gone, it
395                // is logged here, as this write's, and goes no further.
396                if let Err(Err(panic)) = done.send(work.catch_unwind().await) {
397                    let message = panic
398                        .downcast_ref::<&str>()
399                        .copied()
400                        .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
401                        .unwrap_or("a non-text payload");
402                    error!(
403                        panic = message,
404                        "A local write's post-commit work panicked after its caller went away"
405                    );
406                }
407            },
408            &runtime,
409        );
410        match finished.await {
411            Ok(Err(panic)) => std::panic::resume_unwind(panic),
412            // Done, or aborted by `stop()`: either way the write was made.
413            Ok(Ok(())) | Err(_) => Ok(()),
414        }
415    }
416
417    /// Borrow the handles a local write uses, once the caller has checked
418    /// that the network is still active.
419    pub(super) fn local_writer(&self) -> LocalWriter<'_, T> {
420        LocalWriter {
421            db: &self.db,
422            network: self
423                .network
424                .as_ref()
425                .expect("running local mutation owns network"),
426            cov_table: &self.cov_table,
427            cov_in_flight: &self.cov_in_flight,
428            notification_transactions: &self.notification_transactions,
429            comm_state: &self.comm_state,
430            learned_routers: &self.learned_routers,
431            device_bindings: &self.device_bindings,
432            event_suppressions: &self.event_suppressions,
433            config: &self.config,
434        }
435    }
436
437    /// Borrow the COV notification handles for a local mutation.
438    fn local_cov_context(&self) -> CovNotifyContext<'_, T> {
439        self.local_writer().cov_context()
440    }
441
442    /// Borrow the EventNotification handles for a local mutation.
443    fn local_event_delivery(&self) -> EventDelivery<'_, T> {
444        self.local_writer().event_delivery()
445    }
446
447    pub(super) async fn execute_initial_staging_plans(&self) {
448        let staging_oids = {
449            let database = self.db.read().await;
450            database.find_by_type(ObjectType::STAGING)
451        };
452        let staging_plans = {
453            let mut database = self.db.write().await;
454            Self::take_staging_plans(&mut database, &staging_oids)
455        };
456        Self::execute_staging_plans(
457            &self.local_event_delivery(),
458            &self.local_cov_context(),
459            staging_plans,
460        )
461        .await;
462    }
463
464    pub(super) fn take_staging_plans(
465        db: &mut ObjectDatabase,
466        oids: &[ObjectIdentifier],
467    ) -> Vec<StagingWritePlan> {
468        oids.iter()
469            .filter_map(|oid| {
470                db.get_mut(oid)
471                    .and_then(|object| object.take_staging_write_plan_internal())
472            })
473            .collect()
474    }
475
476    /// Execute local Staging targets one mutation guard at a time.
477    ///
478    /// Every target mutation is generation-checked in the same database guard
479    /// that applies it. Event/COV work runs only after that guard is released.
480    pub(super) async fn execute_staging_plans(
481        delivery: &EventDelivery<'_, T>,
482        cov: &CovNotifyContext<'_, T>,
483        plans: Vec<StagingWritePlan>,
484    ) {
485        let db = delivery.db;
486        for plan in plans {
487            let mut all_succeeded = true;
488            for target in &plan.writes {
489                enum TargetResult {
490                    Applied,
491                    Failed,
492                    Stale,
493                }
494
495                let result = {
496                    let mut database = db.write().await;
497                    let current = database
498                        .get(&plan.source)
499                        .and_then(|source| source.staging_generation_internal())
500                        == Some(plan.generation);
501                    if !current {
502                        TargetResult::Stale
503                    } else {
504                        let origin = crate::command_source::resolve_local(
505                            &database,
506                            crate::LocalCommandSource::Object(plan.source),
507                        )
508                        .ok();
509                        match database.get_mut(&target.object_identifier) {
510                            Some(object) => match crate::command_source::write_target(
511                                object,
512                                PropertyIdentifier::PRESENT_VALUE,
513                                None,
514                                PropertyValue::Enumerated(u32::from(target.active)),
515                                Some(plan.priority),
516                                origin.as_ref(),
517                            ) {
518                                Ok(()) => {
519                                    // Timestamped references capture the
520                                    // target change under this guard (#856).
521                                    let capture = cov
522                                        .cov_table
523                                        .read()
524                                        .await
525                                        .timed_capture(target.object_identifier);
526                                    capture.run(&database);
527                                    TargetResult::Applied
528                                }
529                                Err(_) => TargetResult::Failed,
530                            },
531                            None => TargetResult::Failed,
532                        }
533                    }
534                };
535
536                match result {
537                    TargetResult::Stale => break,
538                    TargetResult::Failed => all_succeeded = false,
539                    TargetResult::Applied => {
540                        Self::fire_event_notifications_with_bindings(
541                            delivery,
542                            cov.cov_table,
543                            &target.object_identifier,
544                        )
545                        .await;
546                        Self::fire_cov_notifications(cov, &target.object_identifier).await;
547                    }
548                }
549            }
550
551            let reliability_changed = {
552                let mut database = db.write().await;
553                let changed = database.get_mut(&plan.source).is_some_and(|source| {
554                    source.complete_staging_write_plan_internal(plan.generation, all_succeeded)
555                });
556                if changed {
557                    let capture = cov.cov_table.read().await.timed_capture(plan.source);
558                    capture.run(&database);
559                }
560                changed
561            };
562            if reliability_changed {
563                Self::fire_event_notifications_with_bindings(delivery, cov.cov_table, &plan.source)
564                    .await;
565                Self::fire_cov_notifications(cov, &plan.source).await;
566            }
567        }
568    }
569}
570
571/// The handles one local write borrows: the mutation under the database
572/// guard, then the event, COV and Staging work it owes once the guard is
573/// dropped. `write_local` writes through it, and so does each write of a
574/// Command object's run (Clause 12.10).
575pub(super) struct LocalWriter<'a, T: TransportPort + 'static> {
576    pub(super) db: &'a Arc<RwLock<ObjectDatabase>>,
577    pub(super) network: &'a Arc<NetworkLayer<T>>,
578    pub(super) cov_table: &'a Arc<RwLock<CovSubscriptionTable>>,
579    pub(super) cov_in_flight: &'a Arc<Semaphore>,
580    pub(super) notification_transactions: &'a Arc<NotificationTransactions>,
581    pub(super) comm_state: &'a Arc<CommState>,
582    pub(super) learned_routers: &'a Arc<Mutex<LearnedRouterCache>>,
583    pub(super) device_bindings: &'a Arc<RwLock<DeviceBindingTable>>,
584    pub(super) event_suppressions: &'a Arc<super::event_suppression::EventSuppressions>,
585    pub(super) config: &'a ServerConfig,
586}
587
588impl<'a, T: TransportPort + 'static> LocalWriter<'a, T> {
589    /// The COV notification view of these handles.
590    pub(super) fn cov_context(&self) -> CovNotifyContext<'a, T> {
591        CovNotifyContext {
592            db: self.db,
593            network: self.network,
594            cov_table: self.cov_table,
595            cov_in_flight: self.cov_in_flight,
596            notification_transactions: self.notification_transactions,
597            comm_state: self.comm_state,
598            config: self.config,
599        }
600    }
601
602    /// The EventNotification delivery view of these handles.
603    pub(super) fn event_delivery(&self) -> EventDelivery<'a, T> {
604        EventDelivery {
605            db: self.db,
606            network: self.network,
607            comm_state: self.comm_state,
608            learned_routers: self.learned_routers,
609            notification_transactions: self.notification_transactions,
610            device_bindings: self.device_bindings,
611            suppressions: self.event_suppressions,
612            retry_timeout_ms: self.config.cov_retry_timeout_ms,
613            local_apdu_capacity: self.config.max_apdu_length,
614        }
615    }
616
617    /// Make one local write and the post-write work it owes. Returns the
618    /// Command and Channel runs it queued, directly or through a Schedule it
619    /// changed, for the caller to start. They are taken under the write's
620    /// guard and held in a [`TakenRuns`] until then, so dropping this future
621    /// part way, after the commit, ends them rather than leaving their
622    /// objects busy (#1324). `write_local` runs the two halves itself, the
623    /// second as a task of its own (#1367).
624    pub(super) async fn write(
625        &self,
626        oid: &ObjectIdentifier,
627        write: LocalWrite<'_>,
628        value: PropertyValue,
629        source: Option<crate::LocalCommandSource>,
630    ) -> Result<TakenRuns, Error> {
631        match self.commit(oid, write, value, source).await? {
632            Some(committed) => Ok(self.finish(committed).await),
633            None => Ok(TakenRuns::default()),
634        }
635    }
636
637    /// The first half of [`Self::write`]: the checks and the commit, under a
638    /// database guard it keeps. `None` for a write that changed nothing and
639    /// owes nothing more; otherwise the guard and what the write owes, for
640    /// [`Self::finish`]. Nothing here waits once the object has the value,
641    /// so a caller dropped before this returns made no change.
642    pub(super) async fn commit(
643        &self,
644        oid: &ObjectIdentifier,
645        write: LocalWrite<'_>,
646        value: PropertyValue,
647        source: Option<crate::LocalCommandSource>,
648    ) -> Result<Option<Committed>, Error> {
649        // Only a property write can carry OBJECT_NAME, so only it needs the name
650        // index kept in step.
651        let renaming = matches!(
652            write,
653            LocalWrite::Property { property, .. } if property == PropertyIdentifier::OBJECT_NAME
654        );
655        // A Pulse Converter's Input_Reference is judged as it commits (#1341).
656        let input_reference = matches!(
657            write,
658            LocalWrite::Property { property, .. } if property == PropertyIdentifier::INPUT_REFERENCE
659        );
660        let life_safety = crate::life_safety_cov::is_life_safety_object(*oid);
661        // A write the object saves first saves here, without the guard.
662        let durable = match write {
663            LocalWrite::Property {
664                property,
665                array_index,
666                ..
667            } => super::durable_writes::DurableTarget::local(*oid, property, array_index, &value),
668            _ => Vec::new(),
669        };
670        let staged = super::durable_writes::stage(self.db, durable).await;
671        {
672            // Owned, so the rest of the write can take it to a task of its
673            // own with no wait between the commit and the hand-off (#1367).
674            let mut db = Arc::clone(self.db).write_owned().await;
675            let snapshots = crate::life_safety_cov::LifeSafetyCovSnapshots::capture_oid(&db, *oid);
676            if let Err(error) = precheck(&db, oid, write, &value) {
677                // Nothing reached the object: what was staged goes back.
678                staged.release(&mut db);
679                return Err(error);
680            }
681            // A WriteGroup was planned under an earlier guard; the Channel
682            // may have left the group or changed number since.
683            let mut group_skips_delays = false;
684            if let LocalWrite::WriteGroup {
685                group,
686                number,
687                inhibit_delay,
688                ..
689            } = write
690            {
691                let object = db.get(oid).expect("existence checked above");
692                if !super::write_group::qualifies(object, group, number) {
693                    debug!(
694                        channel = %oid,
695                        group,
696                        number,
697                        "WriteGroup skips a Channel no longer in the group with that number"
698                    );
699                    return Ok(None);
700                }
701                group_skips_delays =
702                    inhibit_delay && super::write_group::allows_delay_inhibit(object);
703            }
704            let mut audit = match write {
705                LocalWrite::Property {
706                    property,
707                    array_index,
708                    priority,
709                } => {
710                    // Device's recipient sink exclusively owns the old/new pair.
711                    let recipient_sink = property
712                        == PropertyIdentifier::AUDIT_NOTIFICATION_RECIPIENT
713                        && db
714                            .get_mut(oid)
715                            .and_then(|object| object.device_authority_internal())
716                            .is_some();
717                    if recipient_sink {
718                        None
719                    } else {
720                        let mut audit = audit_reporter::WriteAudit::local(
721                            self.config,
722                            self.network,
723                            self.notification_transactions,
724                            &db,
725                        );
726                        let encoded = audit_reporter::small_value(&value).unwrap_or_default();
727                        if let Some(audit) = &mut audit {
728                            audit.before(
729                                &db,
730                                WriteTarget {
731                                    oid: *oid,
732                                    property,
733                                    array_index,
734                                    priority,
735                                    value: &encoded,
736                                },
737                            );
738                        }
739                        audit
740                    }
741                }
742                // Audited as the WriteProperty of the Channel's Present_Value
743                // it amounts to, from the device that sent the WriteGroup.
744                LocalWrite::WriteGroup {
745                    priority,
746                    requester,
747                    ..
748                } => {
749                    let mut audit = audit_reporter::WriteAudit::requested_by(
750                        self.config,
751                        self.network,
752                        self.notification_transactions,
753                        requester.clone(),
754                        None,
755                    );
756                    let encoded = audit_reporter::small_value(&value).unwrap_or_default();
757                    audit.before(
758                        &db,
759                        WriteTarget {
760                            oid: *oid,
761                            property: PropertyIdentifier::PRESENT_VALUE,
762                            array_index: None,
763                            priority: Some(priority),
764                            value: &encoded,
765                        },
766                    );
767                    Some(audit)
768                }
769                LocalWrite::ApplicationPresentValue
770                | LocalWrite::ApplicationControlledVariableValue
771                | LocalWrite::ApplicationAveragingSample
772                | LocalWrite::ApplicationAveragingMiss
773                | LocalWrite::ApplicationTrackingValue => None,
774            };
775            let value = match write {
776                LocalWrite::Property { property, .. } => {
777                    crate::local_references::localize(&db, *oid, property, value)
778                }
779                _ => value,
780            };
781            let null = crate::handlers::relinquish::is_null_value(&value);
782            let prepared = match write {
783                LocalWrite::Property {
784                    property,
785                    array_index,
786                    priority,
787                } => {
788                    let encoded = audit_reporter::small_value(&value).unwrap_or_default();
789                    audit.as_mut().and_then(|audit| {
790                        audit.commit_policy(
791                            &mut db,
792                            WriteTarget {
793                                oid: *oid,
794                                property,
795                                array_index,
796                                priority,
797                                value: &encoded,
798                            },
799                            &value,
800                        )
801                    })
802                }
803                LocalWrite::ApplicationPresentValue
804                | LocalWrite::ApplicationControlledVariableValue
805                | LocalWrite::ApplicationAveragingSample
806                | LocalWrite::ApplicationAveragingMiss
807                | LocalWrite::ApplicationTrackingValue
808                | LocalWrite::WriteGroup { .. } => None,
809            };
810            let command_origin =
811                source.and_then(|source| crate::command_source::resolve_local(&db, source).ok());
812            let result = prepared.unwrap_or_else(|| {
813                let object = db.get_mut(oid).expect("existence checked above");
814                match write {
815                    LocalWrite::Property {
816                        property,
817                        array_index,
818                        priority,
819                    } => {
820                        crate::device_view::check_executor_owned_write(*oid, property)?;
821                        crate::command_source::write_target(
822                            object,
823                            property,
824                            array_index,
825                            value,
826                            priority,
827                            command_origin.as_ref(),
828                        )
829                    }
830                    LocalWrite::ApplicationPresentValue => object.set_present_value_internal(value),
831                    LocalWrite::ApplicationControlledVariableValue => {
832                        object.set_controlled_variable_value_internal(value)
833                    }
834                    LocalWrite::ApplicationAveragingSample => {
835                        object.add_averaging_sample_internal(Some(value))
836                    }
837                    LocalWrite::ApplicationAveragingMiss => {
838                        object.add_averaging_sample_internal(None)
839                    }
840                    LocalWrite::ApplicationTrackingValue => {
841                        object.set_tracking_value_internal(value)
842                    }
843                    LocalWrite::WriteGroup { priority, .. } => object.write_property(
844                        PropertyIdentifier::PRESENT_VALUE,
845                        None,
846                        value,
847                        Some(priority),
848                    ),
849                }
850            });
851            staged.release(&mut db);
852            // A NULL the property left as it was succeeds unchanged
853            // (`handlers::relinquish`): audited as the write it is, with no
854            // post-write work, since nothing changed.
855            if let (
856                Err(error),
857                LocalWrite::Property {
858                    property,
859                    array_index,
860                    ..
861                },
862            ) = (&result, write)
863            {
864                let object = db.get(oid).expect("existence checked above");
865                if null
866                    && crate::handlers::relinquish::leaves_unchanged(
867                        object,
868                        property,
869                        array_index,
870                        error,
871                    )
872                {
873                    if let Some(audit) = &mut audit {
874                        audit.committed(&mut db);
875                    }
876                    return Ok(None);
877                }
878            }
879            if let Err(error) = result {
880                if let Some(audit) = &mut audit {
881                    audit.failed(&mut db, &error);
882                }
883                return Err(error);
884            }
885            if let Some(audit) = &mut audit {
886                audit.committed(&mut db);
887            }
888            if renaming {
889                db.update_name_index(oid);
890            }
891            if input_reference {
892                db.check_input_reference(oid);
893            }
894            let staging_plans =
895                BACnetServer::<T>::take_staging_plans(&mut db, std::slice::from_ref(oid));
896            let mut command_runs = TakenRuns::take(self.db, &mut db, std::slice::from_ref(oid));
897            if group_skips_delays {
898                super::write_group::skip_delays(&mut command_runs, *oid);
899            }
900            let changes = snapshots.changes(&db, std::slice::from_ref(oid));
901            Ok(Some(Committed {
902                db,
903                oid: *oid,
904                life_safety,
905                changes,
906                staging_plans,
907                command_runs,
908            }))
909        }
910    }
911}
912
913/// The checks made under the write's guard before the audit hooks or the
914/// object see the value, in WriteProperty's order: the object exists, and
915/// for a property write, the array index fits the property (WriteProperty's
916/// own [`check_write_array_index`](crate::handlers::check_write_array_index))
917/// and a new Object_Name is free. The name check calls
918/// [`ObjectDatabase::check_name_available`] directly, which is what
919/// WriteProperty's `check_and_prepare_name_write` runs for a CharacterString.
920/// A Command's or Channel's write takes this path too; a Schedule's target
921/// writes run the same index gate in `schedule::deliver`.
922fn precheck(
923    db: &ObjectDatabase,
924    oid: &ObjectIdentifier,
925    write: LocalWrite<'_>,
926    value: &PropertyValue,
927) -> Result<(), Error> {
928    let object = db.get(oid).ok_or(Error::Protocol {
929        class: ErrorClass::OBJECT.to_raw() as u32,
930        code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
931    })?;
932    let LocalWrite::Property {
933        property,
934        array_index,
935        ..
936    } = write
937    else {
938        return Ok(());
939    };
940    crate::handlers::check_write_array_index(object, property, array_index)?;
941    match value {
942        PropertyValue::CharacterString(new_name) if property == PropertyIdentifier::OBJECT_NAME => {
943            db.check_name_available(oid, new_name)
944        }
945        _ => Ok(()),
946    }
947}