Skip to main content

opcua_server/
server_status.rs

1use std::{
2    sync::{Arc, OnceLock},
3    time::{Duration, Instant},
4};
5
6use opcua_core::sync::Mutex;
7use opcua_types::{
8    AttributeId, BuildInfo, DataValue, DateTime, ExtensionObject, LocalizedText, MonitoringMode,
9    NodeId, ServerState, ServerStatusDataType, VariableId,
10};
11
12use crate::{node_manager::SyncSampler, SubscriptionCache};
13
14// Note: some of these are unused if the generated namespace feature is disabled.
15
16/// Wrapper for managing the `ServerStatus` variable on the server.
17pub struct ServerStatusWrapper {
18    status: Arc<Mutex<ServerStatusDataType>>,
19    subscriptions: Arc<SubscriptionCache>,
20    #[allow(unused)]
21    sampler: SyncSampler,
22    shutdown: Arc<OnceLock<ShutdownTarget>>,
23}
24
25struct ShutdownTarget {
26    reason: LocalizedText,
27    deadline: Instant,
28    #[allow(unused)]
29    time: DateTime,
30}
31
32#[allow(unused)]
33impl ServerStatusWrapper {
34    pub(crate) fn new(build_info: BuildInfo, subscriptions: Arc<SubscriptionCache>) -> Self {
35        let sampler = SyncSampler::new();
36        sampler.run(Duration::from_secs(1), subscriptions.clone());
37
38        Self {
39            status: Arc::new(Mutex::new(ServerStatusDataType {
40                start_time: DateTime::null(),
41                current_time: DateTime::now(),
42                state: opcua_types::ServerState::Shutdown,
43                build_info,
44                seconds_till_shutdown: 0,
45                shutdown_reason: LocalizedText::null(),
46            })),
47            subscriptions,
48            sampler,
49            shutdown: Arc::new(OnceLock::new()),
50        }
51    }
52
53    pub(crate) fn get_managed_id(&self, id: &NodeId) -> Option<VariableId> {
54        let Ok(var_id) = id.as_variable_id() else {
55            return None;
56        };
57        if matches!(
58            var_id,
59            VariableId::Server_ServerStatus
60                | VariableId::Server_ServerStatus_CurrentTime
61                | VariableId::Server_ServerStatus_SecondsTillShutdown
62                | VariableId::Server_ServerStatus_ShutdownReason
63        ) {
64            Some(var_id)
65        } else {
66            None
67        }
68    }
69
70    pub(crate) fn subscribe_to_component(
71        &self,
72        id: VariableId,
73        mode: MonitoringMode,
74        handle: crate::MonitoredItemHandle,
75        sampling_interval: Duration,
76    ) {
77        let status = self.status.clone();
78        let shutdown = self.shutdown.clone();
79        match id {
80            VariableId::Server_ServerStatus => self.sampler.add_sampler(
81                id.into(),
82                AttributeId::Value,
83                move || {
84                    let mut status = status.lock();
85                    status.current_time = DateTime::now();
86                    Some(DataValue::new_now(ExtensionObject::from_message(
87                        status.clone(),
88                    )))
89                },
90                mode,
91                handle,
92                sampling_interval,
93            ),
94            VariableId::Server_ServerStatus_CurrentTime => self.sampler.add_sampler(
95                id.into(),
96                AttributeId::Value,
97                || Some(DataValue::new_now(DateTime::now())),
98                mode,
99                handle,
100                sampling_interval,
101            ),
102            VariableId::Server_ServerStatus_SecondsTillShutdown => self.sampler.add_sampler(
103                id.into(),
104                AttributeId::Value,
105                move || {
106                    if let Some(v) = shutdown.get() {
107                        let now = Instant::now();
108                        let left = if now < v.deadline {
109                            (v.deadline - now).as_secs()
110                        } else {
111                            0
112                        };
113                        Some(DataValue::new_now(left as u32))
114                    } else {
115                        None
116                    }
117                },
118                mode,
119                handle,
120                sampling_interval,
121            ),
122            VariableId::Server_ServerStatus_ShutdownReason => self.sampler.add_sampler(
123                id.into(),
124                AttributeId::Value,
125                move || {
126                    shutdown
127                        .get()
128                        .map(|v| DataValue::new_at(v.reason.clone(), v.time))
129                },
130                mode,
131                handle,
132                sampling_interval,
133            ),
134            _ => (),
135        }
136    }
137
138    pub(crate) fn sampler(&self) -> &SyncSampler {
139        &self.sampler
140    }
141
142    fn notify_status_object_change(&self) {
143        self.subscriptions.maybe_notify(
144            [(&VariableId::Server_ServerStatus.into(), AttributeId::Value)].into_iter(),
145            |_, _, n, _| {
146                if n.has_range() {
147                    None
148                } else {
149                    Some(DataValue::new_now(ExtensionObject::from_message(
150                        self.status.lock().clone(),
151                    )))
152                }
153            },
154        )
155    }
156
157    /// Set the state of the server. Note that this is not necessarily reflected in server
158    /// behavior.
159    pub fn set_state(&self, state: ServerState) {
160        self.status.lock().state = state;
161        self.subscriptions.notify_data_change(
162            [(
163                DataValue::new_now(state as i32),
164                &VariableId::Server_ServerStatus_State.into(),
165                AttributeId::Value,
166            )]
167            .into_iter(),
168        );
169        self.notify_status_object_change();
170    }
171
172    pub(crate) fn set_start_time(&self, time: DateTime) {
173        self.status.lock().start_time = time;
174    }
175
176    pub(crate) fn set_server_started(&self) {
177        self.set_state(ServerState::Running);
178        self.set_start_time(DateTime::now());
179    }
180
181    pub(crate) fn schedule_shutdown(&self, reason: LocalizedText, deadline: Instant) {
182        let _ = self.shutdown.set(ShutdownTarget {
183            time: DateTime::now(),
184            reason,
185            deadline,
186        });
187    }
188
189    /// Get a copy of the current build info.
190    pub fn build_info(&self) -> BuildInfo {
191        self.status.lock().build_info.clone()
192    }
193
194    /// Get the current server state.
195    pub fn state(&self) -> ServerState {
196        self.status.lock().state
197    }
198
199    /// Get the start time of the server.
200    pub fn start_time(&self) -> DateTime {
201        self.status.lock().start_time
202    }
203
204    /// Get the current seconds till shutdown value.
205    pub fn seconds_till_shutdown(&self) -> Option<u32> {
206        self.shutdown.get().map(|v| {
207            let now = Instant::now();
208            let left = if now < v.deadline {
209                (v.deadline - now).as_secs()
210            } else {
211                0
212            };
213            left as u32
214        })
215    }
216
217    /// Get the current shutdown reason.
218    pub fn shutdown_reason(&self) -> Option<LocalizedText> {
219        self.shutdown.get().map(|v| v.reason.clone())
220    }
221
222    /// Get the full status object as an extension object.
223    pub fn full_status_obj(&self) -> ExtensionObject {
224        ExtensionObject::from_message(self.status.lock().clone())
225    }
226}