1use std::str::FromStr;
2use std::sync::Arc;
3
4use anyhow::{anyhow, Result};
5use opcua::client::custom_types::DataTypeTreeBuilder;
6use opcua::client::{ClientBuilder, IdentityToken, Session};
7use opcua::crypto::SecurityPolicy;
8use opcua::types::custom::{DynamicStructure, DynamicTypeLoader};
9use opcua::types::json::{JsonEncodable, JsonStreamWriter, JsonWriter};
10use opcua::types::{
11 AttributeId, BrowseDescription, BrowseDescriptionResultMask, BrowseDirection, DataValue,
12 EndpointDescription, ExpandedNodeId, MessageSecurityMode, NodeClass, NodeClassMask, NodeId,
13 QualifiedName, ReadValueId, ReferenceDescription, ReferenceTypeId, StatusCode,
14 TimestampsToReturn, TypeLoader, UserTokenPolicy, UserTokenType, Variant,
15};
16use tokio::sync::Mutex;
17use tokio::task::JoinHandle;
18
19use crate::types::{
20 AuthMode, AuthSpec, EndpointInfo, NodeAttribute, NodeSummary, ReferenceRow, SecurityMode,
21 TreeChild, ValueTree,
22};
23
24struct Connected {
25 session: Arc<Session>,
26 event_loop: JoinHandle<StatusCode>,
27}
28
29enum State {
30 Disconnected,
31 Connected(Connected),
32}
33
34pub struct UaClient {
35 state: Mutex<State>,
36}
37
38impl Default for UaClient {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl UaClient {
45 pub fn new() -> Self {
46 Self {
47 state: Mutex::new(State::Disconnected),
48 }
49 }
50
51 pub async fn connect(
52 &self,
53 endpoint_url: &str,
54 endpoint: Option<&EndpointInfo>,
55 auth: &AuthSpec,
56 ) -> Result<()> {
57 let mut guard = self.state.lock().await;
58 if matches!(*guard, State::Connected(_)) {
59 return Err(anyhow!("already connected"));
60 }
61
62 let mut client = build_client()?;
63
64 let (policy_uri, mode) = match endpoint {
65 Some(ep) => (
66 ep.security_policy_uri.clone(),
67 security_mode_to_message_mode(ep.security_mode),
68 ),
69 None => (
70 SecurityPolicy::None.to_uri().to_string(),
71 MessageSecurityMode::None,
72 ),
73 };
74 let target_url = match endpoint {
75 Some(ep) if !ep.endpoint_url.is_empty() => ep.endpoint_url.clone(),
76 _ => endpoint_url.to_string(),
77 };
78 let identity = build_identity_token(auth)?;
79 if mode != MessageSecurityMode::None {
80 log_client_cert_hint();
81 }
82
83 let (session, event_loop) = client
84 .connect_to_matching_endpoint(
85 (
86 target_url.as_str(),
87 policy_uri.as_str(),
88 mode,
89 UserTokenPolicy::anonymous(),
90 ),
91 identity,
92 )
93 .await
94 .map_err(|e| {
95 let msg = e.to_string();
96 let lower = msg.to_lowercase();
97 if lower.contains("uriinvalid") {
98 tracing::error!(
99 "certificate URI mismatch (BadCertificateUriInvalid). \
100 Delete the pki/ folder and reconnect to regenerate the cert with the current application URI \"{}\".",
101 APPLICATION_URI
102 );
103 } else if looks_like_cert_trust_error(&lower) {
104 tracing::error!(
105 "server rejected the client certificate. \
106 Mark pki/own/cert.der as trusted in the server's PKI store and try again."
107 );
108 }
109 anyhow!("connect_to_matching_endpoint failed: {e}")
110 })?;
111
112 let mut handle = event_loop.spawn();
113 let session_for_wait = session.clone();
114 let connected = tokio::select! {
115 res = &mut handle => {
116 return Err(anyhow!(
117 "session ended before connection was established: {res:?}"
118 ));
119 }
120 c = session_for_wait.wait_for_connection() => c,
121 };
122 if !connected {
123 handle.abort();
124 return Err(anyhow!("failed to establish connection"));
125 }
126
127 if let Err(e) = register_dynamic_type_loader(&session).await {
128 tracing::warn!("dynamic type loader setup failed: {e}");
129 }
130
131 *guard = State::Connected(Connected {
132 session,
133 event_loop: handle,
134 });
135 Ok(())
136 }
137
138 pub async fn browse_path(&self, node_id: &NodeId) -> Result<String> {
141 const MAX_DEPTH: usize = 64;
142 let session = self.session().await?;
143 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
144
145 let mut segments: Vec<String> = Vec::new();
146 let mut current = node_id.clone();
147 for _ in 0..MAX_DEPTH {
148 if current == root {
149 break;
150 }
151 let bn = read_browse_name(&session, ¤t).await?;
152 segments.push(bn);
153 match read_inverse_parent(&session, ¤t).await? {
154 Some(p) => current = p,
155 None => break,
156 }
157 }
158 segments.reverse();
159 Ok(if segments.is_empty() {
160 "/".to_string()
161 } else {
162 format!("/{}", segments.join("/"))
163 })
164 }
165
166 pub async fn node_path(&self, node_id: &NodeId) -> Result<Vec<NodeId>> {
169 const MAX_DEPTH: usize = 64;
170 let session = self.session().await?;
171 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
172
173 let mut path = vec![node_id.clone()];
174 let mut current = node_id.clone();
175 for _ in 0..MAX_DEPTH {
176 if current == root {
177 break;
178 }
179 match read_inverse_parent(&session, ¤t).await? {
180 Some(parent) => {
181 path.push(parent.clone());
182 current = parent;
183 }
184 None => break,
185 }
186 }
187 path.reverse();
188 Ok(path)
189 }
190
191 pub async fn resolve_browse_path(&self, text: &str) -> Result<NodeId> {
196 let session = self.session().await?;
197 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
198
199 let mut segments: Vec<&str> = text.split('/').filter(|s| !s.is_empty()).collect();
200 if segments
201 .first()
202 .is_some_and(|s| s.eq_ignore_ascii_case("Root"))
203 {
204 segments.remove(0);
205 }
206 if segments.is_empty() {
207 return Ok(root);
208 }
209
210 let mut current = root;
211 let mut walked = String::new();
212 for seg in &segments {
213 let target = parse_qualified_name(seg);
214 match find_child_by_browse_name(&session, ¤t, &target).await? {
215 Some(next) => {
216 walked.push('/');
217 walked.push_str(seg);
218 current = next;
219 }
220 None => {
221 return Err(anyhow!(
222 "no child '{seg}' under {current} (resolved {walked} so far)"
223 ));
224 }
225 }
226 }
227 Ok(current)
228 }
229
230 pub async fn discover_endpoints(&self, endpoint_url: &str) -> Result<Vec<EndpointInfo>> {
231 let client = build_client()?;
232 let descriptions = client
233 .get_server_endpoints_from_url(endpoint_url)
234 .await
235 .map_err(|e| anyhow!("get_server_endpoints failed: {e}"))?;
236 Ok(descriptions
237 .into_iter()
238 .map(endpoint_description_to_info)
239 .collect())
240 }
241
242 pub async fn disconnect(&self) -> Result<()> {
243 let mut guard = self.state.lock().await;
244 let connected = match std::mem::replace(&mut *guard, State::Disconnected) {
245 State::Connected(c) => c,
246 State::Disconnected => return Ok(()),
247 };
248 let _ = connected.session.disconnect().await;
249 let _ = connected.event_loop.await;
250 Ok(())
251 }
252
253 async fn session(&self) -> Result<Arc<Session>> {
254 let guard = self.state.lock().await;
255 match &*guard {
256 State::Connected(c) => Ok(c.session.clone()),
257 State::Disconnected => Err(anyhow!("not connected")),
258 }
259 }
260
261 pub async fn browse_children(&self, node_id: &NodeId) -> Result<Vec<TreeChild>> {
262 let session = self.session().await?;
263 let desc = browse_hierarchical(node_id.clone());
264 let mut results = session
265 .browse(&[desc], 0, None)
266 .await
267 .map_err(|s| anyhow!("browse failed: {s}"))?;
268 let result = results
269 .pop()
270 .ok_or_else(|| anyhow!("empty browse result"))?;
271 let refs = result.references.unwrap_or_default();
272
273 let mut seen: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
274 let mut children: Vec<TreeChild> = Vec::with_capacity(refs.len());
275 for r in &refs {
276 if is_excluded_tree_reference(&r.reference_type_id) {
277 continue;
278 }
279 let child = reference_to_tree_child(r);
280 if seen.insert(child.node_id.clone()) {
281 children.push(child);
282 }
283 }
284 let target_ids: Vec<NodeId> = children.iter().map(|c| c.node_id.clone()).collect();
285 let has_kids = has_children_batch(&session, &target_ids).await;
286 for (child, hk) in children.iter_mut().zip(has_kids.into_iter()) {
287 child.has_children = hk;
288 }
289 Ok(children)
290 }
291
292 pub async fn read_node_summary(&self, node_id: &NodeId) -> Result<NodeSummary> {
293 let session = self.session().await?;
294 let to_read: Vec<ReadValueId> = ALL_ATTRIBUTES
295 .iter()
296 .map(|(a, _)| ReadValueId::new(node_id.clone(), *a))
297 .collect();
298 let values = session
299 .read(&to_read, TimestampsToReturn::Both, 0.0)
300 .await
301 .map_err(|s| anyhow!("read failed: {s}"))?;
302
303 let mut attributes: Vec<NodeAttribute> = Vec::new();
304 for ((attr_id, name), dv) in ALL_ATTRIBUTES.iter().zip(values.iter()) {
305 if !attribute_status_ok(dv) {
306 continue;
307 }
308 let Some(v) = dv.value.as_ref() else { continue };
309 let tree = format_attribute_value(*attr_id, v, &session);
310 attributes.push(NodeAttribute {
311 name: name.to_string(),
312 value: tree,
313 });
314 if matches!(attr_id, AttributeId::Value) {
315 if let Some(s) = dv.status.map(|s| s.to_string()) {
316 attributes.push(NodeAttribute {
317 name: "StatusCode".to_string(),
318 value: ValueTree::Leaf(s),
319 });
320 }
321 if let Some(t) = dv.source_timestamp.as_ref() {
322 attributes.push(NodeAttribute {
323 name: "SourceTimestamp".to_string(),
324 value: ValueTree::Leaf(t.to_string()),
325 });
326 }
327 if let Some(t) = dv.server_timestamp.as_ref() {
328 attributes.push(NodeAttribute {
329 name: "ServerTimestamp".to_string(),
330 value: ValueTree::Leaf(t.to_string()),
331 });
332 }
333 }
334 }
335 attributes.sort_by(|a, b| a.name.cmp(&b.name));
336
337 Ok(NodeSummary {
338 node_id: node_id.clone(),
339 attributes,
340 })
341 }
342
343 pub async fn browse_references(&self, node_id: &NodeId) -> Result<Vec<ReferenceRow>> {
344 let session = self.session().await?;
345 let desc = BrowseDescription {
346 node_id: node_id.clone(),
347 browse_direction: BrowseDirection::Both,
348 reference_type_id: NodeId::new(0, ReferenceTypeId::References as u32),
349 include_subtypes: true,
350 node_class_mask: NodeClassMask::all().bits(),
351 result_mask: BrowseDescriptionResultMask::all().bits(),
352 };
353 let mut results = session
354 .browse(&[desc], 0, None)
355 .await
356 .map_err(|s| anyhow!("browse failed: {s}"))?;
357 let result = results
358 .pop()
359 .ok_or_else(|| anyhow!("empty browse result"))?;
360 let refs = result.references.unwrap_or_default();
361
362 let mut rows = Vec::with_capacity(refs.len());
363 for r in refs {
364 rows.push(reference_to_row(&session, r).await);
365 }
366 Ok(rows)
367 }
368}
369
370async fn read_browse_name(session: &Session, node_id: &NodeId) -> Result<String> {
371 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::BrowseName)];
372 let values = session
373 .read(&to_read, TimestampsToReturn::Neither, 0.0)
374 .await
375 .map_err(|s| anyhow!("read BrowseName failed: {s}"))?;
376 let q = values
377 .into_iter()
378 .next()
379 .and_then(|v| v.value)
380 .and_then(|v| match v {
381 Variant::QualifiedName(q) => Some(*q),
382 _ => None,
383 });
384 Ok(match q {
385 Some(q) => format_path_segment(q.namespace_index, q.name.as_ref()),
386 None => node_id.to_string(),
387 })
388}
389
390async fn read_inverse_parent(session: &Session, node_id: &NodeId) -> Result<Option<NodeId>> {
391 let desc = BrowseDescription {
392 node_id: node_id.clone(),
393 browse_direction: BrowseDirection::Inverse,
394 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
395 include_subtypes: true,
396 node_class_mask: NodeClassMask::all().bits(),
397 result_mask: BrowseDescriptionResultMask::all().bits(),
398 };
399 let mut results = session
400 .browse(&[desc], 0, None)
401 .await
402 .map_err(|s| anyhow!("browse inverse failed: {s}"))?;
403 let parent = results
404 .pop()
405 .and_then(|r| r.references)
406 .and_then(|refs| {
407 refs.into_iter()
408 .find(|rd| !is_excluded_tree_reference(&rd.reference_type_id))
409 })
410 .map(|r| r.node_id.node_id);
411 Ok(parent)
412}
413
414fn format_path_segment(ns: u16, name: &str) -> String {
415 let escaped = escape_browse_name(name);
416 if ns == 0 {
417 escaped
418 } else {
419 format!("{ns}:{escaped}")
420 }
421}
422
423fn escape_browse_name(s: &str) -> String {
424 let mut out = String::with_capacity(s.len());
425 for c in s.chars() {
426 if matches!(c, '&' | '/' | '.' | '<' | '>' | ':' | '#' | '!' | ';') {
427 out.push('&');
428 }
429 out.push(c);
430 }
431 out
432}
433
434fn is_excluded_tree_reference(ref_type: &NodeId) -> bool {
435 if ref_type.namespace != 0 {
436 return false;
437 }
438 let id = match &ref_type.identifier {
439 opcua::types::Identifier::Numeric(n) => *n,
440 _ => return false,
441 };
442 matches!(
443 id,
444 x if x == ReferenceTypeId::HasEventSource as u32
445 || x == ReferenceTypeId::HasNotifier as u32
446 )
447}
448
449fn browse_hierarchical(node_id: NodeId) -> BrowseDescription {
450 BrowseDescription {
451 node_id,
452 browse_direction: BrowseDirection::Forward,
453 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
454 include_subtypes: true,
455 node_class_mask: NodeClassMask::all().bits(),
456 result_mask: BrowseDescriptionResultMask::all().bits(),
457 }
458}
459
460fn reference_to_tree_child(r: &ReferenceDescription) -> TreeChild {
461 TreeChild {
462 node_id: expanded_to_local(&r.node_id),
463 browse_name: r.browse_name.name.to_string(),
464 display_name: r.display_name.text.to_string(),
465 node_class: r.node_class,
466 has_children: false,
467 }
468}
469
470async fn reference_to_row(session: &Session, r: ReferenceDescription) -> ReferenceRow {
471 let reference_type = resolve_reference_type_name(session, &r.reference_type_id).await;
472 ReferenceRow {
473 reference_type,
474 is_forward: r.is_forward,
475 target_node_id: expanded_to_local(&r.node_id),
476 target_browse_name: r.browse_name.name.to_string(),
477 target_display_name: r.display_name.text.to_string(),
478 target_node_class: r.node_class,
479 }
480}
481
482async fn resolve_reference_type_name(session: &Session, ref_type: &NodeId) -> String {
483 let read = vec![ReadValueId::new(ref_type.clone(), AttributeId::DisplayName)];
484 match session.read(&read, TimestampsToReturn::Neither, 0.0).await {
485 Ok(vals) => vals
486 .into_iter()
487 .next()
488 .and_then(|v| v.value)
489 .and_then(|v| match v {
490 Variant::LocalizedText(t) => Some(t.text.to_string()),
491 _ => None,
492 })
493 .unwrap_or_else(|| ref_type.to_string()),
494 Err(_) => ref_type.to_string(),
495 }
496}
497
498async fn has_children_batch(session: &Session, ids: &[NodeId]) -> Vec<bool> {
499 if ids.is_empty() {
500 return Vec::new();
501 }
502 let descs: Vec<BrowseDescription> = ids
503 .iter()
504 .map(|id| BrowseDescription {
505 node_id: id.clone(),
506 browse_direction: BrowseDirection::Forward,
507 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
508 include_subtypes: true,
509 node_class_mask: NodeClassMask::all().bits(),
510 result_mask: BrowseDescriptionResultMask::RESULT_MASK_REFERENCE_TYPE.bits(),
511 })
512 .collect();
513 match session.browse(&descs, 0, None).await {
514 Ok(results) => results
515 .into_iter()
516 .map(|r| {
517 r.references
518 .map(|refs| {
519 refs.iter()
520 .any(|rd| !is_excluded_tree_reference(&rd.reference_type_id))
521 })
522 .unwrap_or(false)
523 })
524 .collect(),
525 Err(_) => vec![false; ids.len()],
526 }
527}
528
529fn expanded_to_local(eid: &ExpandedNodeId) -> NodeId {
530 eid.node_id.clone()
531}
532
533fn log_client_cert_hint() {
534 let path = std::env::current_dir()
535 .unwrap_or_default()
536 .join("pki/own/cert.der");
537 tracing::info!(
538 "encrypted connection as \"{}\" ({}); client certificate at {}",
539 APPLICATION_NAME,
540 APPLICATION_URI,
541 path.display()
542 );
543 tracing::info!(
544 "if the server rejects the connection, copy that file into the server's trusted certs folder"
545 );
546}
547
548fn looks_like_cert_trust_error(msg: &str) -> bool {
549 let lower = msg.to_lowercase();
550 lower.contains("badsecurity")
551 || lower.contains("badcertificate")
552 || lower.contains("certificatevalidation")
553 || lower.contains("untrusted")
554 || lower.contains("rejected")
555}
556
557fn build_identity_token(auth: &AuthSpec) -> Result<IdentityToken> {
558 match auth.mode {
559 AuthMode::Anonymous => Ok(IdentityToken::Anonymous),
560 AuthMode::UserName => {
561 if auth.username.is_empty() {
562 return Err(anyhow!("username required"));
563 }
564 Ok(IdentityToken::new_user_name(
565 auth.username.clone(),
566 auth.password.clone(),
567 ))
568 }
569 AuthMode::Certificate => {
570 if auth.cert_path.is_empty() || auth.key_path.is_empty() {
571 return Err(anyhow!("certificate and private-key paths required"));
572 }
573 IdentityToken::new_x509_path(&auth.cert_path, &auth.key_path)
574 .map_err(|e| anyhow!("failed to load certificate/key: {e}"))
575 }
576 }
577}
578
579const APPLICATION_NAME: &str = "Rust OPC UA Client from FreeOpcUa";
580const APPLICATION_URI: &str = "urn:FreeOpcUa:ua-client";
581
582fn build_client() -> Result<opcua::client::Client> {
583 ClientBuilder::new()
584 .application_name(APPLICATION_NAME)
585 .application_uri(APPLICATION_URI)
586 .product_uri(APPLICATION_URI)
587 .trust_server_certs(true)
588 .create_sample_keypair(true)
589 .session_retry_limit(0)
590 .client()
591 .map_err(|errs| anyhow!("failed to build OPC UA client: {errs:?}"))
592}
593
594fn security_mode_to_message_mode(m: SecurityMode) -> MessageSecurityMode {
595 match m {
596 SecurityMode::None => MessageSecurityMode::None,
597 SecurityMode::Sign => MessageSecurityMode::Sign,
598 SecurityMode::SignAndEncrypt => MessageSecurityMode::SignAndEncrypt,
599 }
600}
601
602fn message_mode_to_security_mode(m: MessageSecurityMode) -> SecurityMode {
603 match m {
604 MessageSecurityMode::Sign => SecurityMode::Sign,
605 MessageSecurityMode::SignAndEncrypt => SecurityMode::SignAndEncrypt,
606 _ => SecurityMode::None,
607 }
608}
609
610fn endpoint_description_to_info(ep: EndpointDescription) -> EndpointInfo {
611 let policy_uri = ep.security_policy_uri.to_string();
612 let policy_short = SecurityPolicy::from_str(&policy_uri)
613 .map(|p| p.to_string())
614 .unwrap_or_else(|_| policy_uri.clone());
615 let tokens = ep.user_identity_tokens.unwrap_or_default();
616 let supports_anonymous = tokens
617 .iter()
618 .any(|t| matches!(t.token_type, UserTokenType::Anonymous));
619 let supports_username = tokens
620 .iter()
621 .any(|t| matches!(t.token_type, UserTokenType::UserName));
622 let supports_certificate = tokens
623 .iter()
624 .any(|t| matches!(t.token_type, UserTokenType::Certificate));
625
626 EndpointInfo {
627 endpoint_url: ep.endpoint_url.to_string(),
628 security_policy: policy_short,
629 security_policy_uri: policy_uri,
630 security_mode: message_mode_to_security_mode(ep.security_mode),
631 security_level: ep.security_level,
632 supports_anonymous,
633 supports_username,
634 supports_certificate,
635 }
636}
637
638const ALL_ATTRIBUTES: &[(AttributeId, &str)] = &[
639 (AttributeId::AccessLevel, "AccessLevel"),
640 (AttributeId::AccessLevelEx, "AccessLevelEx"),
641 (AttributeId::AccessRestrictions, "AccessRestrictions"),
642 (AttributeId::ArrayDimensions, "ArrayDimensions"),
643 (AttributeId::BrowseName, "BrowseName"),
644 (AttributeId::ContainsNoLoops, "ContainsNoLoops"),
645 (AttributeId::DataType, "DataType"),
646 (AttributeId::DataTypeDefinition, "DataTypeDefinition"),
647 (AttributeId::Description, "Description"),
648 (AttributeId::DisplayName, "DisplayName"),
649 (AttributeId::EventNotifier, "EventNotifier"),
650 (AttributeId::Executable, "Executable"),
651 (AttributeId::Historizing, "Historizing"),
652 (AttributeId::InverseName, "InverseName"),
653 (AttributeId::IsAbstract, "IsAbstract"),
654 (AttributeId::MinimumSamplingInterval, "MinimumSamplingInterval"),
655 (AttributeId::NodeClass, "NodeClass"),
656 (AttributeId::NodeId, "NodeId"),
657 (AttributeId::RolePermissions, "RolePermissions"),
658 (AttributeId::Symmetric, "Symmetric"),
659 (AttributeId::UserAccessLevel, "UserAccessLevel"),
660 (AttributeId::UserExecutable, "UserExecutable"),
661 (AttributeId::UserRolePermissions, "UserRolePermissions"),
662 (AttributeId::UserWriteMask, "UserWriteMask"),
663 (AttributeId::Value, "Value"),
664 (AttributeId::ValueRank, "ValueRank"),
665 (AttributeId::WriteMask, "WriteMask"),
666];
667
668fn attribute_status_ok(dv: &DataValue) -> bool {
669 match dv.status {
670 None => dv.value.is_some(),
671 Some(s) => s.is_good(),
672 }
673}
674
675fn format_attribute_value(attr: AttributeId, v: &Variant, session: &Session) -> ValueTree {
676 if matches!(attr, AttributeId::NodeClass)
677 && let Variant::Int32(i) = v
678 && let Ok(nc) = NodeClass::try_from(*i)
679 {
680 return ValueTree::Leaf(format!("{nc:?}"));
681 }
682 variant_to_tree(session, v)
683}
684
685fn variant_to_tree(session: &Session, v: &Variant) -> ValueTree {
686 match v {
687 Variant::Empty => ValueTree::Null,
688 Variant::Boolean(b) => ValueTree::Leaf(b.to_string()),
689 Variant::SByte(n) => ValueTree::Leaf(n.to_string()),
690 Variant::Byte(n) => ValueTree::Leaf(n.to_string()),
691 Variant::Int16(n) => ValueTree::Leaf(n.to_string()),
692 Variant::UInt16(n) => ValueTree::Leaf(n.to_string()),
693 Variant::Int32(n) => ValueTree::Leaf(n.to_string()),
694 Variant::UInt32(n) => ValueTree::Leaf(n.to_string()),
695 Variant::Int64(n) => ValueTree::Leaf(n.to_string()),
696 Variant::UInt64(n) => ValueTree::Leaf(n.to_string()),
697 Variant::Float(n) => ValueTree::Leaf(n.to_string()),
698 Variant::Double(n) => ValueTree::Leaf(n.to_string()),
699 Variant::String(s) => ValueTree::Leaf(s.to_string()),
700 Variant::DateTime(d) => ValueTree::Leaf(d.to_string()),
701 Variant::Guid(g) => ValueTree::Leaf(format!("{g:?}")),
702 Variant::StatusCode(s) => ValueTree::Leaf(s.to_string()),
703 Variant::ByteString(b) => match b.value.as_ref() {
704 Some(bytes) => ValueTree::Leaf(format!("<{} bytes>", bytes.len())),
705 None => ValueTree::Null,
706 },
707 Variant::XmlElement(_) => ValueTree::Leaf("XmlElement(…)".to_string()),
708 Variant::QualifiedName(q) => ValueTree::Leaf(q.name.to_string()),
709 Variant::LocalizedText(t) => ValueTree::Leaf(t.text.to_string()),
710 Variant::NodeId(n) => ValueTree::Leaf(n.to_string()),
711 Variant::ExpandedNodeId(n) => ValueTree::Leaf(format!("{n}")),
712 Variant::ExtensionObject(obj) => extension_object_to_tree(session, obj),
713 Variant::Variant(inner) => variant_to_tree(session, inner),
714 Variant::DataValue(_) => ValueTree::Leaf("DataValue(…)".to_string()),
715 Variant::DiagnosticInfo(_) => ValueTree::Leaf("DiagnosticInfo(…)".to_string()),
716 Variant::Array(arr) => {
717 ValueTree::Array(arr.values.iter().map(|i| variant_to_tree(session, i)).collect())
718 }
719 }
720}
721
722fn extension_object_to_tree(session: &Session, obj: &opcua::types::ExtensionObject) -> ValueTree {
723 if obj.inner_as::<DynamicStructure>().is_none() {
724 let label = obj
725 .type_name()
726 .map(|n| format!("ExtensionObject ({n})"))
727 .unwrap_or_else(|| "ExtensionObject".to_string());
728 return ValueTree::Leaf(label);
729 }
730 match dynamic_struct_to_tree(session, obj) {
731 Some(tree) => tree,
732 None => ValueTree::Leaf("ExtensionObject (decode failed)".to_string()),
733 }
734}
735
736fn dynamic_struct_to_tree(
737 session: &Session,
738 obj: &opcua::types::ExtensionObject,
739) -> Option<ValueTree> {
740 let ds = obj.inner_as::<DynamicStructure>()?;
741 let ctx_owned = session.context();
742 let ctx_guard = ctx_owned.read();
743 let ctx = ctx_guard.context();
744 let mut buf = Vec::new();
745 {
746 let writer_ref: &mut dyn std::io::Write = &mut buf;
747 let mut writer = JsonStreamWriter::new(writer_ref);
748 ds.encode(&mut writer, &ctx).ok()?;
749 writer.finish_document().ok()?;
750 }
751 let json: serde_json::Value = serde_json::from_slice(&buf).ok()?;
752 Some(json_to_tree(&json))
753}
754
755fn json_to_tree(v: &serde_json::Value) -> ValueTree {
756 match v {
757 serde_json::Value::Null => ValueTree::Null,
758 serde_json::Value::Bool(b) => ValueTree::Leaf(b.to_string()),
759 serde_json::Value::Number(n) => ValueTree::Leaf(n.to_string()),
760 serde_json::Value::String(s) => ValueTree::Leaf(s.clone()),
761 serde_json::Value::Array(arr) => ValueTree::Array(arr.iter().map(json_to_tree).collect()),
762 serde_json::Value::Object(map) => ValueTree::Object(
763 map.iter()
764 .map(|(k, v)| (k.clone(), json_to_tree(v)))
765 .collect(),
766 ),
767 }
768}
769
770async fn find_child_by_browse_name(
771 session: &Session,
772 parent: &NodeId,
773 target: &QualifiedName,
774) -> Result<Option<NodeId>> {
775 let desc = browse_hierarchical(parent.clone());
776 let mut results = session
777 .browse(&[desc], 0, None)
778 .await
779 .map_err(|s| anyhow!("browse failed: {s}"))?;
780 let refs = results
781 .pop()
782 .and_then(|r| r.references)
783 .unwrap_or_default();
784 for r in refs {
785 if is_excluded_tree_reference(&r.reference_type_id) {
786 continue;
787 }
788 if r.browse_name.namespace_index == target.namespace_index
789 && r.browse_name.name.as_ref() == target.name.as_ref()
790 {
791 return Ok(Some(r.node_id.node_id));
792 }
793 }
794 Ok(None)
795}
796
797fn parse_qualified_name(segment: &str) -> QualifiedName {
802 let body = segment.strip_prefix("ns=").unwrap_or(segment);
803 if let Some((head, rest)) = body.split_once(':')
804 && let Ok(ns) = head.parse::<u16>()
805 {
806 return QualifiedName::new(ns, rest);
807 }
808 QualifiedName::new(0, segment)
809}
810
811async fn register_dynamic_type_loader(session: &Session) -> Result<()> {
812 let type_tree = DataTypeTreeBuilder::new(|_| true)
813 .build(session)
814 .await
815 .map_err(|e| anyhow!("DataTypeTreeBuilder failed: {e}"))?;
816 let loader: Arc<dyn TypeLoader> = Arc::new(DynamicTypeLoader::new(Arc::new(type_tree)));
817 session.add_type_loader(loader);
818 Ok(())
819}