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