1use std::{
2 collections::{BTreeMap, VecDeque},
3 sync::Arc,
4 time::Duration,
5};
6
7use async_trait::async_trait;
8use opcua_core::trace_read_lock;
9use opcua_nodes::DefaultTypeTree;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 address_space::AccessLevel,
14 node_manager::{
15 as_opaque_node_id, from_opaque_node_id, impl_translate_browse_paths_using_browse,
16 AddReferenceResult, BrowseNode, BrowsePathItem, DynNodeManager, ExternalReferenceRequest,
17 NodeManager, NodeManagerBuilder, NodeManagersRef, NodeMetadata, ReadNode, RequestContext,
18 ServerContext, SyncSampler,
19 },
20};
21use opcua_types::{
22 AccessLevelExType, AccessRestrictionType, AttributeId, BrowseDirection, DataTypeId, DataValue,
23 DateTime, ExpandedNodeId, ExtensionObject, IdType, LocalizedText, NodeClass, NodeId,
24 NumericRange, ObjectId, ObjectTypeId, QualifiedName, ReferenceDescription, ReferenceTypeId,
25 RolePermissionType, StatusCode, TimestampsToReturn, VariableTypeId, Variant,
26};
27
28pub struct DiagnosticsNodeManager {
32 sampler: SyncSampler,
33 node_managers: NodeManagersRef,
34 namespace_index: u16,
35}
36
37#[derive(Default, Clone, Debug)]
51pub struct NamespaceMetadata {
54 pub default_access_restrictions: AccessRestrictionType,
56 pub default_role_permissions: Option<Vec<RolePermissionType>>,
58 pub default_user_role_permissions: Option<Vec<RolePermissionType>>,
60 pub is_namespace_subset: Option<bool>,
62 pub namespace_publication_date: Option<DateTime>,
64 pub namespace_uri: String,
66 pub namespace_version: Option<String>,
68 pub static_node_id_types: Option<Vec<IdType>>,
70 pub static_numeric_node_id_range: Option<Vec<NumericRange>>,
72 pub static_string_node_id_pattern: Option<String>,
74 pub namespace_index: u16,
76}
77
78#[derive(Default)]
79struct BrowseContinuationPoint {
80 nodes: VecDeque<ReferenceDescription>,
81}
82
83#[derive(Serialize, Deserialize, Debug)]
84struct NamespaceNode {
85 namespace: String,
86 property: Option<String>,
87}
88
89#[derive(Serialize, Deserialize, Debug)]
90enum DiagnosticsNode {
91 Namespace(NamespaceNode),
92}
93
94pub struct DiagnosticsNodeManagerBuilder;
96
97impl NodeManagerBuilder for DiagnosticsNodeManagerBuilder {
98 fn build(self: Box<Self>, context: ServerContext) -> Arc<DynNodeManager> {
99 Arc::new(DiagnosticsNodeManager::new(context))
100 }
101}
102
103impl DiagnosticsNodeManager {
104 pub(crate) fn new(context: ServerContext) -> Self {
105 let namespace_index = {
106 let mut type_tree = context.type_tree.write();
107 type_tree
108 .namespaces_mut()
109 .add_namespace(context.info.application_uri.as_ref())
110 };
111 Self {
112 sampler: SyncSampler::new(),
113 node_managers: context.node_managers.clone(),
114 namespace_index,
115 }
116 }
117
118 fn namespaces(&self, context: &RequestContext) -> BTreeMap<String, NamespaceMetadata> {
119 self.node_managers
120 .iter()
121 .flat_map(move |nm| nm.namespaces_for_user(context))
122 .map(|ns| (ns.namespace_uri.clone(), ns))
123 .collect()
124 }
125
126 fn namespace_node_metadata(&self, ns: &NamespaceMetadata) -> NodeMetadata {
127 NodeMetadata {
128 node_id: ExpandedNodeId::new(
129 as_opaque_node_id(
130 &DiagnosticsNode::Namespace(NamespaceNode {
131 namespace: ns.namespace_uri.clone(),
132 property: None,
133 }),
134 self.namespace_index,
135 )
136 .unwrap(),
137 ),
138 type_definition: ExpandedNodeId::new(ObjectTypeId::NamespaceMetadataType),
139 browse_name: QualifiedName::new(ns.namespace_index, ns.namespace_uri.clone()),
140 display_name: LocalizedText::new("", &ns.namespace_uri),
141 node_class: NodeClass::Object,
142 }
143 }
144
145 fn property_node_metadata(&self, namespace: &str, name: &str) -> NodeMetadata {
146 NodeMetadata {
147 node_id: ExpandedNodeId::new(
148 as_opaque_node_id(
149 &DiagnosticsNode::Namespace(NamespaceNode {
150 namespace: namespace.to_owned(),
151 property: Some(name.to_owned()),
152 }),
153 self.namespace_index,
154 )
155 .unwrap_or_default(),
156 ),
157 type_definition: VariableTypeId::PropertyType.into(),
158 browse_name: QualifiedName::new(0, name),
159 display_name: LocalizedText::new("", name),
160 node_class: NodeClass::Variable,
161 }
162 }
163
164 fn browse_namespaces(
165 &self,
166 node_to_browse: &mut BrowseNode,
167 type_tree: &DefaultTypeTree,
168 namespaces: &BTreeMap<String, NamespaceMetadata>,
169 ) {
170 if !matches!(
172 node_to_browse.browse_direction(),
173 BrowseDirection::Forward | BrowseDirection::Both
174 ) {
175 return;
176 }
177
178 if !node_to_browse.allows_reference_type(&ReferenceTypeId::HasComponent.into(), type_tree) {
179 return;
180 }
181
182 let mut cp = BrowseContinuationPoint::default();
183
184 for namespace in namespaces.values() {
185 if namespace.namespace_index == 0 {
187 continue;
188 }
189 let metadata = self.namespace_node_metadata(namespace);
190 let ref_desc = ReferenceDescription {
191 reference_type_id: ReferenceTypeId::HasComponent.into(),
192 is_forward: true,
193 node_id: metadata.node_id,
194 browse_name: metadata.browse_name,
195 display_name: metadata.display_name,
196 node_class: metadata.node_class,
197 type_definition: metadata.type_definition,
198 };
199
200 if let AddReferenceResult::Full(c) = node_to_browse.add(type_tree, ref_desc) {
201 cp.nodes.push_back(c);
202 }
203 }
204
205 if !cp.nodes.is_empty() {
206 node_to_browse.set_next_continuation_point(Box::new(cp));
207 }
208 }
209
210 fn is_valid_property(prop: &str) -> bool {
211 matches!(
212 prop,
213 "DefaultAccessRestrictions"
214 | "DefaultRolePermissions"
215 | "DefaultUserRolePermissions"
216 | "IsNamespaceSubset"
217 | "NamespacePublicationDate"
218 | "NamespaceUri"
219 | "NamespaceVersion"
220 | "StaticNodeIdTypes"
221 | "StaticNumericNodeIdRange"
222 | "StaticStringNodeIdPattern"
223 )
224 }
225
226 fn browse_namespace_metadata_node(
227 &self,
228 node_to_browse: &mut BrowseNode,
229 type_tree: &DefaultTypeTree,
230 meta: &NamespaceMetadata,
231 ) {
232 let mut cp = BrowseContinuationPoint::default();
233
234 if matches!(
235 node_to_browse.browse_direction(),
236 BrowseDirection::Forward | BrowseDirection::Both
237 ) {
238 if node_to_browse.allows_reference_type(&ReferenceTypeId::HasProperty.into(), type_tree)
239 && node_to_browse.allows_node_class(NodeClass::Variable)
240 {
241 for prop in [
242 "DefaultAccessRestrictions",
243 "DefaultRolePermissions",
244 "DefaultUserRolePermissions",
245 "IsNamespaceSubset",
246 "NamespacePublicationDate",
247 "NamespaceUri",
248 "NamespaceVersion",
249 "StaticNodeIdTypes",
250 "StaticNumericNodeIdRange",
251 "StaticStringNodeIdPattern",
252 ] {
253 let meta = self.property_node_metadata(&meta.namespace_uri, prop);
254 let ref_desc = meta.into_ref_desc(true, ReferenceTypeId::HasProperty);
255
256 if let AddReferenceResult::Full(c) = node_to_browse.add(type_tree, ref_desc) {
257 cp.nodes.push_back(c);
258 }
259 }
260 }
261
262 if node_to_browse
263 .allows_reference_type(&ReferenceTypeId::HasTypeDefinition.into(), type_tree)
264 {
265 let ref_desc = ReferenceDescription {
266 reference_type_id: ReferenceTypeId::HasTypeDefinition.into(),
267 is_forward: true,
268 node_id: ObjectTypeId::NamespaceMetadataType.into(),
269 browse_name: QualifiedName::new(0, "NamespaceMetadataType"),
270 display_name: LocalizedText::new("", "NamespaceMetadataType"),
271 node_class: NodeClass::ObjectType,
272 type_definition: ExpandedNodeId::null(),
273 };
274 if let AddReferenceResult::Full(c) = node_to_browse.add(type_tree, ref_desc) {
275 cp.nodes.push_back(c);
276 }
277 }
278 }
279
280 if matches!(
281 node_to_browse.browse_direction(),
282 BrowseDirection::Inverse | BrowseDirection::Both
283 ) {
284 let ref_desc = ReferenceDescription {
285 reference_type_id: ReferenceTypeId::HasComponent.into(),
286 is_forward: false,
287 node_id: ObjectId::Server_Namespaces.into(),
288 browse_name: QualifiedName::new(0, "Namespaces"),
289 display_name: LocalizedText::new("", "Namespaces"),
290 node_class: NodeClass::Object,
291 type_definition: ObjectTypeId::NamespacesType.into(),
292 };
293 if let AddReferenceResult::Full(c) = node_to_browse.add(type_tree, ref_desc) {
294 cp.nodes.push_back(c);
295 }
296 }
297
298 if !cp.nodes.is_empty() {
299 node_to_browse.set_next_continuation_point(Box::new(cp));
300 }
301 }
302
303 fn browse_namespace_property_node(
304 &self,
305 node_to_browse: &mut BrowseNode,
306 type_tree: &DefaultTypeTree,
307 meta: &NamespaceMetadata,
308 ) {
309 let mut cp = BrowseContinuationPoint::default();
310
311 if matches!(
312 node_to_browse.browse_direction(),
313 BrowseDirection::Forward | BrowseDirection::Both
314 ) {
315 let ref_desc = ReferenceDescription {
316 reference_type_id: ReferenceTypeId::HasTypeDefinition.into(),
317 is_forward: true,
318 node_id: VariableTypeId::PropertyType.into(),
319 browse_name: QualifiedName::new(0, "PropertyType"),
320 display_name: LocalizedText::new("", "PropertyType"),
321 node_class: NodeClass::VariableType,
322 type_definition: ExpandedNodeId::null(),
323 };
324 if let AddReferenceResult::Full(c) = node_to_browse.add(type_tree, ref_desc) {
325 cp.nodes.push_back(c);
326 }
327 }
328
329 if matches!(
330 node_to_browse.browse_direction(),
331 BrowseDirection::Inverse | BrowseDirection::Both
332 ) {
333 let metadata = self.namespace_node_metadata(meta);
334 let ref_desc = metadata.into_ref_desc(false, ReferenceTypeId::HasComponent);
335
336 if let AddReferenceResult::Full(c) = node_to_browse.add(type_tree, ref_desc) {
337 cp.nodes.push_back(c);
338 }
339 }
340
341 if !cp.nodes.is_empty() {
342 node_to_browse.set_next_continuation_point(Box::new(cp));
343 }
344 }
345
346 fn browse_namespace_node(
347 &self,
348 node_to_browse: &mut BrowseNode,
349 type_tree: &DefaultTypeTree,
350 namespaces: &BTreeMap<String, NamespaceMetadata>,
351 ns_node: &NamespaceNode,
352 ) {
353 let Some(namespace) = namespaces.get(&ns_node.namespace) else {
354 node_to_browse.set_status(StatusCode::BadNodeIdUnknown);
355 return;
356 };
357
358 if ns_node.property.is_some() {
359 self.browse_namespace_property_node(node_to_browse, type_tree, namespace);
360 } else {
361 self.browse_namespace_metadata_node(node_to_browse, type_tree, namespace);
362 }
363 }
364
365 fn read_namespace_metadata_node(
366 &self,
367 start_time: DateTime,
368 node_to_read: &mut ReadNode,
369 namespace: &NamespaceMetadata,
370 ) {
371 let v: Variant = match node_to_read.node().attribute_id {
372 AttributeId::NodeId => as_opaque_node_id(
373 &DiagnosticsNode::Namespace(NamespaceNode {
374 namespace: namespace.namespace_uri.clone(),
375 property: None,
376 }),
377 self.namespace_index,
378 )
379 .unwrap()
380 .into(),
381 AttributeId::NodeClass => (NodeClass::Object as i32).into(),
382 AttributeId::BrowseName => {
383 QualifiedName::new(namespace.namespace_index, &namespace.namespace_uri).into()
384 }
385 AttributeId::DisplayName => LocalizedText::new("", &namespace.namespace_uri).into(),
386 AttributeId::EventNotifier => 0u8.into(),
387 AttributeId::WriteMask | AttributeId::UserWriteMask => 0u32.into(),
388 _ => {
389 node_to_read.set_error(StatusCode::BadAttributeIdInvalid);
390 return;
391 }
392 };
393
394 node_to_read.set_result(DataValue {
395 value: Some(v),
396 status: Some(StatusCode::Good),
397 source_timestamp: Some(start_time),
398 source_picoseconds: None,
399 server_timestamp: Some(start_time),
400 server_picoseconds: None,
401 });
402 }
403
404 fn read_namespace_property_node(
405 &self,
406 start_time: DateTime,
407 node_to_read: &mut ReadNode,
408 namespace: &NamespaceMetadata,
409 prop: &str,
410 ) {
411 if !Self::is_valid_property(prop) {
412 node_to_read.set_error(StatusCode::BadNodeIdUnknown);
413 return;
414 }
415
416 let v: Variant = match node_to_read.node().attribute_id {
417 AttributeId::NodeId => as_opaque_node_id(
418 &DiagnosticsNode::Namespace(NamespaceNode {
419 namespace: namespace.namespace_uri.clone(),
420 property: Some(prop.to_owned()),
421 }),
422 self.namespace_index,
423 )
424 .unwrap()
425 .into(),
426 AttributeId::NodeClass => (NodeClass::Object as i32).into(),
427 AttributeId::BrowseName => QualifiedName::new(0, prop).into(),
428 AttributeId::DisplayName => LocalizedText::new("", prop).into(),
429 AttributeId::Value => match prop {
430 "DefaultAccessRestrictions" => namespace.default_access_restrictions.bits().into(),
431 "DefaultRolePermissions" => namespace
432 .default_role_permissions
433 .as_ref()
434 .map(|r| {
435 r.iter()
436 .cloned()
437 .map(ExtensionObject::from_message)
438 .collect::<Vec<_>>()
439 })
440 .into(),
441 "DefaultUserRolePermissions" => namespace
442 .default_user_role_permissions
443 .as_ref()
444 .map(|r| {
445 r.iter()
446 .cloned()
447 .map(ExtensionObject::from_message)
448 .collect::<Vec<_>>()
449 })
450 .into(),
451 "IsNamespaceSubset" => namespace.is_namespace_subset.into(),
452 "NamespacePublicationDate" => namespace.namespace_publication_date.into(),
453 "NamespaceUri" => namespace.namespace_uri.clone().into(),
454 "NamespaceVersion" => namespace.namespace_version.clone().into(),
455 "StaticNodeIdTypes" => namespace
456 .static_node_id_types
457 .as_ref()
458 .map(|r| r.iter().map(|v| (*v) as u8).collect::<Vec<_>>())
459 .into(),
460 "StaticNumericNodeIdRange" => namespace
461 .static_numeric_node_id_range
462 .as_ref()
463 .map(|r| r.iter().map(|v| v.to_string()).collect::<Vec<_>>())
464 .into(),
465 "StaticStringNodeIdPattern" => {
466 namespace.static_string_node_id_pattern.clone().into()
467 }
468 _ => {
469 node_to_read.set_error(StatusCode::BadNodeIdUnknown);
470 return;
471 }
472 },
473 AttributeId::DataType => match prop {
474 "DefaultAccessRestrictions" => {
475 Variant::NodeId(Box::new(DataTypeId::AccessRestrictionType.into()))
476 }
477 "DefaultRolePermissions" | "DefaultUserRolePermissions" => {
478 Variant::NodeId(Box::new(DataTypeId::RolePermissionType.into()))
479 }
480 "IsNamespaceSubset" => Variant::NodeId(Box::new(DataTypeId::Boolean.into())),
481 "NamespacePublicationDate" => {
482 Variant::NodeId(Box::new(DataTypeId::DateTime.into()))
483 }
484 "NamespaceUri" | "NamespaceVersion" | "StaticStringNodeIdPattern" => {
485 Variant::NodeId(Box::new(DataTypeId::String.into()))
486 }
487 "StaticNodeIdTypes" => Variant::NodeId(Box::new(DataTypeId::IdType.into())),
488 "StaticNumericNodeIdRange" => {
489 Variant::NodeId(Box::new(DataTypeId::NumericRange.into()))
490 }
491 _ => {
492 node_to_read.set_error(StatusCode::BadNodeIdUnknown);
493 return;
494 }
495 },
496 AttributeId::ValueRank => match prop {
497 "DefaultRolePermissions" | "DefaultUserRolePermissions" | "StaticNodeIdTypes" => {
498 1.into()
499 }
500 _ => (-1).into(),
501 },
502 AttributeId::ArrayDimensions => match prop {
503 "DefaultRolePermissions" | "DefaultUserRolePermissions" | "StaticNodeIdTypes" => {
504 vec![0u32].into()
505 }
506 _ => Variant::Empty,
507 },
508 AttributeId::AccessLevel | AttributeId::UserAccessLevel => {
509 AccessLevel::CURRENT_READ.bits().into()
510 }
511 AttributeId::AccessLevelEx => (AccessLevelExType::CurrentRead.bits() as u32).into(),
512 AttributeId::MinimumSamplingInterval => 0.0.into(),
513 AttributeId::Historizing => false.into(),
514 AttributeId::WriteMask | AttributeId::UserWriteMask => 0u32.into(),
515 _ => {
516 node_to_read.set_error(StatusCode::BadAttributeIdInvalid);
517 return;
518 }
519 };
520
521 node_to_read.set_result(DataValue {
522 value: Some(v),
523 status: Some(StatusCode::Good),
524 source_timestamp: Some(start_time),
525 source_picoseconds: None,
526 server_timestamp: Some(start_time),
527 server_picoseconds: None,
528 });
529 }
530
531 fn read_namespace_node(
532 &self,
533 start_time: DateTime,
534 node_to_read: &mut ReadNode,
535 namespaces: &BTreeMap<String, NamespaceMetadata>,
536 ns_node: &NamespaceNode,
537 ) {
538 let Some(namespace) = namespaces.get(&ns_node.namespace) else {
539 node_to_read.set_error(StatusCode::BadNodeIdUnknown);
540 return;
541 };
542
543 if let Some(prop) = &ns_node.property {
544 self.read_namespace_property_node(start_time, node_to_read, namespace, prop);
545 } else {
546 self.read_namespace_metadata_node(start_time, node_to_read, namespace);
547 }
548 }
549}
550
551#[async_trait]
552impl NodeManager for DiagnosticsNodeManager {
553 fn owns_node(&self, id: &NodeId) -> bool {
554 id.namespace == self.namespace_index
555 }
556
557 fn name(&self) -> &str {
558 "diagnostics"
559 }
560
561 fn namespaces_for_user(&self, context: &RequestContext) -> Vec<NamespaceMetadata> {
562 vec![NamespaceMetadata {
563 namespace_uri: context.info.application_uri.as_ref().to_owned(),
564 is_namespace_subset: Some(false),
565 static_node_id_types: Some(vec![IdType::Opaque]),
566 namespace_index: self.namespace_index,
567 ..Default::default()
568 }]
569 }
570
571 async fn init(&self, _type_tree: &mut DefaultTypeTree, context: ServerContext) {
572 let interval = context
573 .info
574 .config
575 .limits
576 .subscriptions
577 .min_sampling_interval_ms
578 .floor() as u64;
579 let sampler_interval = if interval > 0 { interval } else { 100 };
580 self.sampler.run(
581 Duration::from_millis(sampler_interval),
582 context.subscriptions.clone(),
583 );
584 }
585
586 async fn resolve_external_references(
587 &self,
588 context: &RequestContext,
589 items: &mut [&mut ExternalReferenceRequest],
590 ) {
591 let mut lazy_namespaces = None::<BTreeMap<String, NamespaceMetadata>>;
592
593 for req in items {
594 let Some(node_desc) = from_opaque_node_id::<DiagnosticsNode>(req.node_id()) else {
595 continue;
596 };
597 let meta = match node_desc {
598 DiagnosticsNode::Namespace(ns) => {
599 let namespaces =
600 lazy_namespaces.get_or_insert_with(|| self.namespaces(context));
601 let Some(ns_node) = namespaces.get(&ns.namespace) else {
602 continue;
603 };
604
605 if let Some(prop) = &ns.property {
606 if !Self::is_valid_property(prop) {
607 continue;
608 }
609 self.property_node_metadata(&ns.namespace, prop)
610 } else {
611 self.namespace_node_metadata(ns_node)
612 }
613 }
614 };
615 req.set(meta);
616 }
617 }
618
619 async fn browse(
620 &self,
621 context: &RequestContext,
622 nodes_to_browse: &mut [BrowseNode],
623 ) -> Result<(), StatusCode> {
624 let mut lazy_namespaces = None::<BTreeMap<String, NamespaceMetadata>>;
625 let type_tree = trace_read_lock!(context.type_tree);
626
627 for node in nodes_to_browse {
628 if let Some(mut point) = node.take_continuation_point::<BrowseContinuationPoint>() {
629 if node.remaining() == 0 {
630 break;
631 }
632 let Some(ref_desc) = point.nodes.pop_back() else {
633 break;
634 };
635 node.add_unchecked(ref_desc);
637 continue;
638 }
639
640 if node.node_id().namespace == 0 {
641 let namespaces = lazy_namespaces.get_or_insert_with(|| self.namespaces(context));
642 let Ok(obj_id) = node.node_id().as_object_id() else {
643 continue;
644 };
645 match obj_id {
646 ObjectId::Server_Namespaces => {
647 self.browse_namespaces(node, &type_tree, namespaces);
648 }
649 _ => continue,
650 }
651 } else if node.node_id().namespace == self.namespace_index {
652 let Some(node_desc) = from_opaque_node_id::<DiagnosticsNode>(node.node_id()) else {
653 node.set_status(StatusCode::BadNodeIdUnknown);
654 continue;
655 };
656 match node_desc {
657 DiagnosticsNode::Namespace(ns) => {
658 let namespaces =
659 lazy_namespaces.get_or_insert_with(|| self.namespaces(context));
660 self.browse_namespace_node(node, &type_tree, namespaces, &ns);
661 }
662 }
663 }
664 }
665
666 Ok(())
667 }
668
669 async fn read(
670 &self,
671 context: &RequestContext,
672 _max_age: f64,
673 _timestamps_to_return: TimestampsToReturn,
674 nodes_to_read: &mut [&mut ReadNode],
675 ) -> Result<(), StatusCode> {
676 let mut lazy_namespaces = None::<BTreeMap<String, NamespaceMetadata>>;
677 let start_time = **context.info.start_time.load();
678
679 for node in nodes_to_read {
680 let Some(node_desc) = from_opaque_node_id::<DiagnosticsNode>(&node.node().node_id)
681 else {
682 node.set_error(StatusCode::BadNodeIdUnknown);
683 continue;
684 };
685 match node_desc {
686 DiagnosticsNode::Namespace(ns) => {
687 let namespaces =
688 lazy_namespaces.get_or_insert_with(|| self.namespaces(context));
689 self.read_namespace_node(start_time, node, namespaces, &ns);
690 }
691 }
692 }
693 Ok(())
694 }
695
696 async fn translate_browse_paths_to_node_ids(
697 &self,
698 context: &RequestContext,
699 nodes: &mut [&mut BrowsePathItem],
700 ) -> Result<(), StatusCode> {
701 impl_translate_browse_paths_using_browse(self, context, nodes).await
702 }
703}