Skip to main content

bacnet_objects/life_safety/
reset.rs

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