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}