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