Skip to main content

bacnet_objects/life_safety/
reset.rs

1//! Application-owned reset execution for built-in Life Safety objects.
2
3use std::panic::{catch_unwind, AssertUnwindSafe};
4use std::sync::Arc;
5
6use bacnet_types::enums::{
7    ErrorClass, ErrorCode, LifeSafetyOperation, LifeSafetyState, SilencedState,
8};
9use bacnet_types::error::Error;
10use bacnet_types::primitives::ObjectIdentifier;
11
12use crate::traits::LifeSafetyOperationEffect;
13
14use super::{
15    life_safety_error, valid_life_safety_state, LifeSafetyPointObject, LifeSafetyZoneObject,
16};
17
18/// Immutable Life Safety Point state supplied to a reset executor.
19///
20/// Network provenance is deliberately absent. The server-owned authorizer is
21/// the authority boundary; this context describes only truthful local state.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct LifeSafetyPointResetContext {
24    /// Object receiving the operation.
25    pub object_identifier: ObjectIdentifier,
26    /// Exact reset variant requested by the peer.
27    pub operation: LifeSafetyOperation,
28    /// Current `Present_Value`.
29    pub present_value: LifeSafetyState,
30    /// Current `Tracking_Value`, simulated or not.
31    pub tracking_value: LifeSafetyState,
32    /// Current `Silenced` value.
33    pub silenced: SilencedState,
34    /// Current `Operation_Expected`, equal to `operation` when invoked.
35    pub operation_expected: LifeSafetyOperation,
36}
37
38/// Atomic local-state proposal returned by a Life Safety Point reset executor.
39///
40/// Omitted properties remain unchanged. No value is inferred from another
41/// property, and successful application always clears `Operation_Expected`.
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub struct LifeSafetyPointResetCommit {
44    /// Replacement `Present_Value`, when application truth changed.
45    pub present_value: Option<LifeSafetyState>,
46    /// Replacement `Tracking_Value`, when application truth changed. While
47    /// `Out_Of_Service` is TRUE a client's simulated value stays in place and
48    /// this one is served from the return to service.
49    pub tracking_value: Option<LifeSafetyState>,
50    /// Replacement `Silenced`, when application truth changed.
51    pub silenced: Option<SilencedState>,
52}
53
54/// Immutable Life Safety Zone state supplied to a reset executor.
55///
56/// Network provenance is retained only by the server-owned authorization
57/// boundary.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct LifeSafetyZoneResetContext {
60    /// Object receiving the operation.
61    pub object_identifier: ObjectIdentifier,
62    /// Exact reset variant requested by the peer.
63    pub operation: LifeSafetyOperation,
64    /// Current `Present_Value`.
65    pub present_value: LifeSafetyState,
66    /// Current `Tracking_Value`, simulated or not.
67    pub tracking_value: LifeSafetyState,
68    /// Current `Silenced` value.
69    pub silenced: SilencedState,
70    /// Current `Operation_Expected`, equal to `operation` when invoked.
71    pub operation_expected: LifeSafetyOperation,
72}
73
74/// Atomic local-state proposal returned by a Life Safety Zone reset executor.
75///
76/// Omitted properties remain unchanged. No quiet state or unsilencing is
77/// implicit, and successful application always clears `Operation_Expected`.
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub struct LifeSafetyZoneResetCommit {
80    /// Replacement `Present_Value`, when application truth changed.
81    pub present_value: Option<LifeSafetyState>,
82    /// Replacement `Tracking_Value`, when application truth changed. While
83    /// `Out_Of_Service` is TRUE a client's simulated value stays in place and
84    /// this one is served from the return to service.
85    pub tracking_value: Option<LifeSafetyState>,
86    /// Replacement `Silenced`, when application truth changed.
87    pub silenced: Option<SilencedState>,
88}
89
90/// Protocol-classified failure reported by an application reset executor.
91///
92/// The closed set prevents callbacks from escaping arbitrary protocol errors.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum LifeSafetyResetError {
95    /// The application does not implement the exact recognized reset variant.
96    UnsupportedVariant,
97    /// Local physical or application state cannot accept this reset now.
98    InvalidOperationInThisState,
99    /// Authorization-like denial or an operational executor failure.
100    ServiceRequestDenied,
101}
102
103/// Synchronous application-owned Life Safety Point reset executor.
104///
105/// The server invokes this callback while holding the object-database write
106/// lock. It must be fast, nonblocking, non-reentrant, and panic-free. Panics are
107/// caught where unwinding is supported and fail closed. Because duplicate
108/// tracking is bounded and process-local, physical actuation must also be
109/// application-idempotent. The callback receives no mutable object reference.
110pub type LifeSafetyPointResetExecutor = Arc<
111    dyn Fn(&LifeSafetyPointResetContext) -> Result<LifeSafetyPointResetCommit, LifeSafetyResetError>
112        + Send
113        + Sync,
114>;
115
116/// Synchronous application-owned Life Safety Zone reset executor.
117///
118/// The server invokes this callback while holding the object-database write
119/// lock. It must be fast, nonblocking, non-reentrant, and panic-free. Panics are
120/// caught where unwinding is supported and fail closed. Because duplicate
121/// tracking is bounded and process-local, physical actuation must also be
122/// application-idempotent. The callback receives no mutable object reference.
123pub type LifeSafetyZoneResetExecutor = Arc<
124    dyn Fn(&LifeSafetyZoneResetContext) -> Result<LifeSafetyZoneResetCommit, LifeSafetyResetError>
125        + Send
126        + Sync,
127>;
128
129pub(super) fn is_reset_operation(operation: LifeSafetyOperation) -> bool {
130    matches!(
131        operation,
132        LifeSafetyOperation::RESET
133            | LifeSafetyOperation::RESET_ALARM
134            | LifeSafetyOperation::RESET_FAULT
135    )
136}
137
138fn reset_error(error: LifeSafetyResetError) -> Error {
139    match error {
140        LifeSafetyResetError::UnsupportedVariant => {
141            life_safety_error(ErrorCode::VALUE_OUT_OF_RANGE)
142        }
143        LifeSafetyResetError::InvalidOperationInThisState => {
144            life_safety_error(ErrorCode::INVALID_OPERATION_IN_THIS_STATE)
145        }
146        LifeSafetyResetError::ServiceRequestDenied => Error::Protocol {
147            class: ErrorClass::SERVICES.to_raw() as u32,
148            code: ErrorCode::SERVICE_REQUEST_DENIED.to_raw() as u32,
149        },
150    }
151}
152
153fn optional_functionality_error() -> Error {
154    life_safety_error(ErrorCode::OPTIONAL_FUNCTIONALITY_NOT_SUPPORTED)
155}
156
157fn invalid_operation_error() -> Error {
158    life_safety_error(ErrorCode::INVALID_OPERATION_IN_THIS_STATE)
159}
160
161fn commit_value_error() -> Error {
162    life_safety_error(ErrorCode::VALUE_OUT_OF_RANGE)
163}
164
165fn valid_silenced_state(state: SilencedState) -> bool {
166    SilencedState::ALL_NAMED
167        .iter()
168        .any(|&(_, named)| named == state)
169}
170
171impl LifeSafetyPointObject {
172    pub(super) fn apply_reset_operation(
173        &mut self,
174        operation: LifeSafetyOperation,
175    ) -> Result<LifeSafetyOperationEffect, Error> {
176        if !is_reset_operation(operation) || self.operation_expected != operation {
177            return Err(invalid_operation_error());
178        }
179        let executor = self
180            .reset_executor
181            .clone()
182            .ok_or_else(optional_functionality_error)?;
183        let context = LifeSafetyPointResetContext {
184            object_identifier: self.oid,
185            operation,
186            present_value: self.present_value,
187            tracking_value: self.tracking_value,
188            silenced: self.silenced,
189            operation_expected: self.operation_expected,
190        };
191        let result = match catch_unwind(AssertUnwindSafe(|| executor(&context))) {
192            Ok(result) => result,
193            Err(_) => return Err(reset_error(LifeSafetyResetError::ServiceRequestDenied)),
194        };
195        let commit = result.map_err(reset_error)?;
196
197        if commit
198            .present_value
199            .is_some_and(|value| !valid_life_safety_state(value))
200            || commit
201                .tracking_value
202                .is_some_and(|value| !valid_life_safety_state(value))
203            || commit
204                .silenced
205                .is_some_and(|value| !valid_silenced_state(value))
206        {
207            return Err(commit_value_error());
208        }
209
210        if let Some(value) = commit.present_value {
211            self.present_value = value;
212        }
213        if let Some(value) = commit.tracking_value {
214            self.simulation().track(value);
215        }
216        if let Some(value) = commit.silenced {
217            self.silenced = value;
218        }
219        self.operation_expected = LifeSafetyOperation::NONE;
220        Ok(LifeSafetyOperationEffect::Applied)
221    }
222}
223
224impl LifeSafetyZoneObject {
225    pub(super) fn apply_reset_operation(
226        &mut self,
227        operation: LifeSafetyOperation,
228    ) -> Result<LifeSafetyOperationEffect, Error> {
229        if !is_reset_operation(operation) || self.operation_expected != operation {
230            return Err(invalid_operation_error());
231        }
232        let executor = self
233            .reset_executor
234            .clone()
235            .ok_or_else(optional_functionality_error)?;
236        let context = LifeSafetyZoneResetContext {
237            object_identifier: self.oid,
238            operation,
239            present_value: self.present_value,
240            tracking_value: self.tracking_value,
241            silenced: self.silenced,
242            operation_expected: self.operation_expected,
243        };
244        let result = match catch_unwind(AssertUnwindSafe(|| executor(&context))) {
245            Ok(result) => result,
246            Err(_) => return Err(reset_error(LifeSafetyResetError::ServiceRequestDenied)),
247        };
248        let commit = result.map_err(reset_error)?;
249
250        if commit
251            .present_value
252            .is_some_and(|value| !valid_life_safety_state(value))
253            || commit
254                .tracking_value
255                .is_some_and(|value| !valid_life_safety_state(value))
256            || commit
257                .silenced
258                .is_some_and(|value| !valid_silenced_state(value))
259        {
260            return Err(commit_value_error());
261        }
262
263        if let Some(value) = commit.present_value {
264            self.present_value = value;
265        }
266        if let Some(value) = commit.tracking_value {
267            self.simulation().track(value);
268        }
269        if let Some(value) = commit.silenced {
270            self.silenced = value;
271        }
272        self.operation_expected = LifeSafetyOperation::NONE;
273        Ok(LifeSafetyOperationEffect::Applied)
274    }
275}