Skip to main content

bacnet_server/server/
local_writes.rs

1use super::*;
2use bacnet_objects::staging::StagingWritePlan;
3
4#[cfg(test)]
5#[path = "input_present_value_tests.rs"]
6mod input_present_value_tests;
7
8#[cfg(test)]
9#[path = "staging_local_writes_tests.rs"]
10mod staging_local_writes_tests;
11
12/// What a local mutation is, and on whose behalf.
13///
14/// An Input gates `Present_Value` on `Out_Of_Service` in opposite directions
15/// for the two, so this decides which check applies.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum LocalWrite {
18    /// A client on the network, writing any property.
19    Network {
20        property: PropertyIdentifier,
21        array_index: Option<u32>,
22        priority: Option<u8>,
23    },
24    /// The application supplying a supported Input's logical `Present_Value`.
25    ApplicationInputPresentValue,
26}
27
28impl<T: TransportPort + 'static> BACnetServer<T> {
29    /// Arm or rearm a Life Safety object from trusted local application logic.
30    ///
31    /// This uses the object-internal state channel under the database write
32    /// lock. Network WriteProperty and WritePropertyMultiple remain unable to
33    /// forge `Operation_Expected`. After the lock is released, an actual
34    /// `Operation_Expected` readback change uses the exact Life Safety COV path;
35    /// rearming to the current value emits no notification.
36    pub async fn set_life_safety_operation_expected_local(
37        &self,
38        oid: &ObjectIdentifier,
39        operation: LifeSafetyOperation,
40    ) -> Result<(), Error> {
41        let changes = {
42            let mut db = self.db.write().await;
43            let snapshots = crate::life_safety_cov::LifeSafetyCovSnapshots::capture_oid(&db, *oid);
44            let object = db.get_mut(oid).ok_or_else(|| Error::Protocol {
45                class: ErrorClass::OBJECT.to_raw() as u32,
46                code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
47            })?;
48            object.set_life_safety_operation_expected_internal(operation)?;
49            snapshots.changes(&db, std::slice::from_ref(oid))
50        };
51        for change in changes {
52            Self::fire_life_safety_cov_notifications(
53                &self.db,
54                &self.network,
55                &self.cov_table,
56                &self.cov_in_flight,
57                &self.notification_transactions,
58                &self.comm_state,
59                &self.config,
60                &change.object_identifier,
61                &change.changed_properties,
62            )
63            .await;
64        }
65        Ok(())
66    }
67
68    /// Write a property on a local object and fire the same post-write COV
69    /// and event notifications that a network [`WriteProperty`] does.
70    ///
71    /// This is the server-owned local-mutation entry point: it performs the
72    /// write under the database lock — routing `OBJECT_NAME` through the name
73    /// uniqueness check and index refresh, exactly like the network handler —
74    /// then releases the lock and runs the COV/event trigger path so a
75    /// subscription observes a local mutation just as it would a network one.
76    /// Low-level object setters deliberately bypass this notification owner.
77    ///
78    /// [`WriteProperty`]: bacnet_services::write_property::WritePropertyRequest
79    pub async fn write_local(
80        &self,
81        oid: &ObjectIdentifier,
82        property: PropertyIdentifier,
83        array_index: Option<u32>,
84        value: PropertyValue,
85        priority: Option<u8>,
86    ) -> Result<(), Error> {
87        self.write_local_as(
88            oid,
89            LocalWrite::Network {
90                property,
91                array_index,
92                priority,
93            },
94            value,
95        )
96        .await
97    }
98
99    /// Supply a supported Input's logical `Present_Value` from Rust application code.
100    ///
101    /// Analog Input, Binary Input, and Multi-state Input opt in. Binary values
102    /// are logical INACTIVE/ACTIVE states after Polarity, not raw physical
103    /// states. The update runs the existing post-write intrinsic-event and COV
104    /// processing after the database lock is released; configured delays and
105    /// distribution policy still determine when notifications are delivered.
106    ///
107    /// Applications are denied while `Out_Of_Service` is TRUE to protect a
108    /// client's simulation value. That ownership rule is local policy, not a
109    /// Standard mandate. Other object families fail closed; use
110    /// [`BACnetServer::write_local`] for ordinary network-equivalent writes and
111    /// commandable objects. Python exposure is tracked separately in #503.
112    ///
113    /// [`BACnetObject::set_present_value_internal`]: bacnet_objects::traits::BACnetObject::set_present_value_internal
114    pub async fn set_present_value_local(
115        &self,
116        oid: &ObjectIdentifier,
117        value: PropertyValue,
118    ) -> Result<(), Error> {
119        self.write_local_as(oid, LocalWrite::ApplicationInputPresentValue, value)
120            .await
121    }
122
123    async fn write_local_as(
124        &self,
125        oid: &ObjectIdentifier,
126        write: LocalWrite,
127        value: PropertyValue,
128    ) -> Result<(), Error> {
129        // Only a network write can carry OBJECT_NAME, so only it needs the name
130        // index kept in step.
131        let renaming = matches!(
132            write,
133            LocalWrite::Network { property, .. } if property == PropertyIdentifier::OBJECT_NAME
134        );
135        let life_safety = crate::life_safety_cov::is_life_safety_object(*oid);
136        let (exact_changes, staging_plans) = {
137            let mut db = self.db.write().await;
138            let snapshots = crate::life_safety_cov::LifeSafetyCovSnapshots::capture_oid(&db, *oid);
139            if db.get(oid).is_none() {
140                return Err(Error::Protocol {
141                    class: ErrorClass::OBJECT.to_raw() as u32,
142                    code: ErrorCode::UNKNOWN_OBJECT.to_raw() as u32,
143                });
144            }
145            if renaming {
146                if let PropertyValue::CharacterString(ref new_name) = value {
147                    db.check_name_available(oid, new_name)?;
148                }
149            }
150            let object = db.get_mut(oid).expect("existence checked above");
151            match write {
152                LocalWrite::Network {
153                    property,
154                    array_index,
155                    priority,
156                } => object.write_property(property, array_index, value, priority)?,
157                LocalWrite::ApplicationInputPresentValue => {
158                    object.set_present_value_internal(value)?
159                }
160            }
161            if renaming {
162                db.update_name_index(oid);
163            }
164            let staging_plans = Self::take_staging_plans(&mut db, std::slice::from_ref(oid));
165            (
166                snapshots.changes(&db, std::slice::from_ref(oid)),
167                staging_plans,
168            )
169        };
170
171        Self::fire_event_notifications_with_bindings(
172            &self.db,
173            &self.network,
174            &self.comm_state,
175            &self.server_tsm,
176            &self.notification_transactions,
177            &self.device_bindings,
178            oid,
179            self.config.cov_retry_timeout_ms,
180        )
181        .await;
182        if life_safety {
183            for change in exact_changes {
184                Self::fire_life_safety_cov_notifications(
185                    &self.db,
186                    &self.network,
187                    &self.cov_table,
188                    &self.cov_in_flight,
189                    &self.notification_transactions,
190                    &self.comm_state,
191                    &self.config,
192                    &change.object_identifier,
193                    &change.changed_properties,
194                )
195                .await;
196            }
197        } else {
198            Self::fire_cov_notifications(
199                &self.db,
200                &self.network,
201                &self.cov_table,
202                &self.cov_in_flight,
203                &self.notification_transactions,
204                &self.comm_state,
205                &self.config,
206                oid,
207            )
208            .await;
209        }
210        Self::execute_staging_plans(
211            &self.db,
212            &self.network,
213            &self.cov_table,
214            &self.cov_in_flight,
215            &self.server_tsm,
216            &self.notification_transactions,
217            &self.device_bindings,
218            &self.comm_state,
219            &self.config,
220            staging_plans,
221        )
222        .await;
223        Ok(())
224    }
225
226    pub(super) fn take_staging_plans(
227        db: &mut ObjectDatabase,
228        oids: &[ObjectIdentifier],
229    ) -> Vec<StagingWritePlan> {
230        oids.iter()
231            .filter_map(|oid| {
232                db.get_mut(oid)
233                    .and_then(|object| object.take_staging_write_plan_internal())
234            })
235            .collect()
236    }
237
238    /// Execute local Staging targets one mutation guard at a time.
239    ///
240    /// Every target mutation is generation-checked in the same database guard
241    /// that applies it. Event/COV work runs only after that guard is released.
242    #[allow(clippy::too_many_arguments)]
243    pub(super) async fn execute_staging_plans(
244        db: &Arc<RwLock<ObjectDatabase>>,
245        network: &Arc<NetworkLayer<T>>,
246        cov_table: &Arc<RwLock<CovSubscriptionTable>>,
247        cov_in_flight: &Arc<Semaphore>,
248        server_tsm: &Arc<Mutex<ServerTsm>>,
249        notification_transactions: &Arc<NotificationTransactions>,
250        device_bindings: &Arc<RwLock<DeviceBindingTable>>,
251        comm_state: &Arc<AtomicU8>,
252        config: &ServerConfig,
253        plans: Vec<StagingWritePlan>,
254    ) {
255        for plan in plans {
256            let mut all_succeeded = true;
257            for target in &plan.writes {
258                enum TargetResult {
259                    Applied,
260                    Failed,
261                    Stale,
262                }
263
264                let result = {
265                    let mut database = db.write().await;
266                    let current = database
267                        .get(&plan.source)
268                        .and_then(|source| source.staging_generation_internal())
269                        == Some(plan.generation);
270                    if !current {
271                        TargetResult::Stale
272                    } else {
273                        match database.get_mut(&target.object_identifier) {
274                            Some(object) => match object.write_property(
275                                PropertyIdentifier::PRESENT_VALUE,
276                                None,
277                                PropertyValue::Enumerated(u32::from(target.active)),
278                                Some(plan.priority),
279                            ) {
280                                Ok(()) => TargetResult::Applied,
281                                Err(_) => TargetResult::Failed,
282                            },
283                            None => TargetResult::Failed,
284                        }
285                    }
286                };
287
288                match result {
289                    TargetResult::Stale => break,
290                    TargetResult::Failed => all_succeeded = false,
291                    TargetResult::Applied => {
292                        Self::fire_event_notifications_with_bindings(
293                            db,
294                            network,
295                            comm_state,
296                            server_tsm,
297                            notification_transactions,
298                            device_bindings,
299                            &target.object_identifier,
300                            config.cov_retry_timeout_ms,
301                        )
302                        .await;
303                        Self::fire_cov_notifications(
304                            db,
305                            network,
306                            cov_table,
307                            cov_in_flight,
308                            notification_transactions,
309                            comm_state,
310                            config,
311                            &target.object_identifier,
312                        )
313                        .await;
314                    }
315                }
316            }
317
318            let reliability_changed = {
319                let mut database = db.write().await;
320                database.get_mut(&plan.source).is_some_and(|source| {
321                    source.complete_staging_write_plan_internal(plan.generation, all_succeeded)
322                })
323            };
324            if reliability_changed {
325                Self::fire_event_notifications_with_bindings(
326                    db,
327                    network,
328                    comm_state,
329                    server_tsm,
330                    notification_transactions,
331                    device_bindings,
332                    &plan.source,
333                    config.cov_retry_timeout_ms,
334                )
335                .await;
336                Self::fire_cov_notifications(
337                    db,
338                    network,
339                    cov_table,
340                    cov_in_flight,
341                    notification_transactions,
342                    comm_state,
343                    config,
344                    &plan.source,
345                )
346                .await;
347            }
348        }
349    }
350}