1use std::collections::HashMap;
2use std::str::FromStr;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7use anyhow::{Result, anyhow};
8use opcua::client::custom_types::DataTypeTreeBuilder;
9use opcua::client::{ClientBuilder, DataChangeCallback, IdentityToken, Session};
10use opcua::crypto::SecurityPolicy;
11use opcua::types::custom::{DynamicStructure, DynamicTypeLoader};
12use opcua::types::json::{JsonEncodable, JsonStreamWriter, JsonWriter};
13use opcua::types::{
14 Argument, Array, AttributeId, BrowseDescription, BrowseDescriptionResultMask, BrowseDirection,
15 CallMethodRequest, DataTypeId, DataValue, EndpointDescription, ExpandedNodeId, Guid,
16 Identifier, LocalizedText, MessageSecurityMode, MonitoredItemCreateRequest, MonitoringMode,
17 MonitoringParameters, NodeClass, NodeClassMask, NodeId, NumericRange, QualifiedName,
18 ReadValueId, ReferenceDescription, ReferenceTypeId, StatusCode, TimestampsToReturn,
19 TryFromVariant, TypeLoader, UAString, UserTokenType, Variant, VariantScalarTypeId, WriteValue,
20};
21use tokio::sync::Mutex;
22use tokio::sync::mpsc;
23use tokio::task::JoinHandle;
24
25use crate::messages::UiUpdate;
26
27use crate::types::{
28 AttrSpec, AuthMode, AuthSpec, EndpointInfo, MethodArgument, MethodCallOutcome, MethodSignature,
29 NodeAttribute, NodeSummary, ReferenceRow, SecurityMode, TreeChild, ValueTree, WriteTarget,
30};
31
32struct Connected {
33 session: Arc<Session>,
34 event_loop: JoinHandle<StatusCode>,
35 sub: Option<SubState>,
36}
37
38struct SubState {
39 sub_id: u32,
40 items: HashMap<NodeId, u32>,
41 next_handle: u32,
42}
43
44enum State {
45 Disconnected,
46 Connected(Connected),
47}
48
49pub struct UaClient {
50 state: Mutex<State>,
51 verify_certificate_metadata: AtomicBool,
58}
59
60impl Default for UaClient {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl UaClient {
67 pub fn set_verify_cert_metadata(&self, on: bool) {
68 self.verify_certificate_metadata
69 .store(on, Ordering::Relaxed);
70 }
71
72 pub fn new() -> Self {
73 warn_insecure_default();
74 Self {
75 state: Mutex::new(State::Disconnected),
76 verify_certificate_metadata: AtomicBool::new(false),
77 }
78 }
79
80 pub async fn connect(
81 &self,
82 endpoint_url: &str,
83 endpoint: Option<&EndpointInfo>,
84 auth: &AuthSpec,
85 ) -> Result<()> {
86 let mut guard = self.state.lock().await;
87 if matches!(*guard, State::Connected(_)) {
88 return Err(anyhow!("already connected"));
89 }
90
91 let mut client = build_client(self.verify_certificate_metadata.load(Ordering::Relaxed))?;
92
93 let (policy_uri, mode) = match endpoint {
94 Some(ep) => (
95 ep.security_policy_uri.clone(),
96 security_mode_to_message_mode(ep.security_mode),
97 ),
98 None => (
99 SecurityPolicy::None.to_uri().to_string(),
100 MessageSecurityMode::None,
101 ),
102 };
103 let identity = build_identity_token(auth)?;
104 if mode != MessageSecurityMode::None {
105 log_client_cert_hint();
106 }
107
108 let descriptions = client
116 .get_server_endpoints_from_url(endpoint_url)
117 .await
118 .map_err(|e| anyhow!("get_server_endpoints failed: {e}"))?;
119 let mut matched = descriptions
120 .into_iter()
121 .find(|d| d.security_policy_uri.as_ref() == policy_uri && d.security_mode == mode)
122 .ok_or_else(|| {
123 anyhow!(
124 "server has no endpoint with policy '{}' and mode {:?}",
125 policy_uri,
126 mode
127 )
128 })?;
129 let reported_url = matched.endpoint_url.as_ref().to_string();
130 if !reported_url.is_empty() && reported_url != endpoint_url {
131 tracing::info!(
132 "server endpoint URL is {reported_url}; forcing transport to typed URL {endpoint_url}"
133 );
134 }
135 matched.endpoint_url = endpoint_url.into();
136
137 let (session, event_loop) = client
138 .connect_to_endpoint_directly(matched, identity)
139 .map_err(|e| {
140 let msg = e.to_string();
141 let lower = msg.to_lowercase();
142 if lower.contains("uriinvalid") {
143 tracing::error!(
144 "certificate URI mismatch (BadCertificateUriInvalid). \
145 Delete the pki/ folder and reconnect to regenerate the cert with the current application URI \"{}\".",
146 APPLICATION_URI
147 );
148 } else if looks_like_cert_trust_error(&lower) {
149 tracing::error!(
150 "server rejected the client certificate. \
151 Mark pki/own/cert.der as trusted in the server's PKI store and try again."
152 );
153 }
154 anyhow!("connect_to_endpoint_directly failed: {e}")
155 })?;
156
157 let mut handle = event_loop.spawn();
158 let session_for_wait = session.clone();
159 let connected = tokio::select! {
160 res = &mut handle => {
161 return Err(anyhow!(
162 "session ended before connection was established: {res:?}"
163 ));
164 }
165 c = session_for_wait.wait_for_connection() => c,
166 };
167 if !connected {
168 handle.abort();
169 return Err(anyhow!("failed to establish connection"));
170 }
171
172 if let Err(e) = register_dynamic_type_loader(&session).await {
173 tracing::warn!("dynamic type loader setup failed: {e}");
174 }
175
176 *guard = State::Connected(Connected {
177 session,
178 event_loop: handle,
179 sub: None,
180 });
181 Ok(())
182 }
183
184 pub async fn browse_path(&self, node_id: &NodeId) -> Result<String> {
187 const MAX_DEPTH: usize = 64;
188 let session = self.session().await?;
189 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
190
191 let mut segments: Vec<String> = Vec::new();
192 let mut current = node_id.clone();
193 for _ in 0..MAX_DEPTH {
194 if current == root {
195 break;
196 }
197 let bn = read_browse_name(&session, ¤t).await?;
198 segments.push(bn);
199 match read_inverse_parent(&session, ¤t).await? {
200 Some(p) => current = p,
201 None => break,
202 }
203 }
204 segments.reverse();
205 Ok(if segments.is_empty() {
206 "/".to_string()
207 } else {
208 format!("/{}", segments.join("/"))
209 })
210 }
211
212 pub async fn node_path(&self, node_id: &NodeId) -> Result<Vec<NodeId>> {
215 const MAX_DEPTH: usize = 64;
216 let session = self.session().await?;
217 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
218
219 let mut path = vec![node_id.clone()];
220 let mut current = node_id.clone();
221 for _ in 0..MAX_DEPTH {
222 if current == root {
223 break;
224 }
225 match read_inverse_parent(&session, ¤t).await? {
226 Some(parent) => {
227 path.push(parent.clone());
228 current = parent;
229 }
230 None => break,
231 }
232 }
233 path.reverse();
234 Ok(path)
235 }
236
237 pub async fn resolve_browse_path(&self, text: &str) -> Result<NodeId> {
242 let session = self.session().await?;
243 let root = NodeId::new(0, opcua::types::ObjectId::RootFolder as u32);
244
245 let mut segments: Vec<&str> = text.split('/').filter(|s| !s.is_empty()).collect();
246 if segments
247 .first()
248 .is_some_and(|s| s.eq_ignore_ascii_case("Root"))
249 {
250 segments.remove(0);
251 }
252 if segments.is_empty() {
253 return Ok(root);
254 }
255
256 let mut current = root;
257 let mut walked = String::new();
258 for seg in &segments {
259 let target = parse_qualified_name(seg);
260 match find_child_by_browse_name(&session, ¤t, &target).await? {
261 Some(next) => {
262 walked.push('/');
263 walked.push_str(seg);
264 current = next;
265 }
266 None => {
267 return Err(anyhow!(
268 "no child '{seg}' under {current} (resolved {walked} so far)"
269 ));
270 }
271 }
272 }
273 Ok(current)
274 }
275
276 pub async fn discover_endpoints(&self, endpoint_url: &str) -> Result<Vec<EndpointInfo>> {
277 let client = build_client(self.verify_certificate_metadata.load(Ordering::Relaxed))?;
278 let descriptions = client
279 .get_server_endpoints_from_url(endpoint_url)
280 .await
281 .map_err(|e| anyhow!("get_server_endpoints failed: {e}"))?;
282 Ok(descriptions
283 .into_iter()
284 .map(endpoint_description_to_info)
285 .collect())
286 }
287
288 pub async fn disconnect(&self) -> Result<()> {
289 let mut guard = self.state.lock().await;
290 let connected = match std::mem::replace(&mut *guard, State::Disconnected) {
291 State::Connected(c) => c,
292 State::Disconnected => return Ok(()),
293 };
294 let _ = connected.session.disconnect().await;
295 let _ = connected.event_loop.await;
296 Ok(())
297 }
298
299 async fn session(&self) -> Result<Arc<Session>> {
300 let guard = self.state.lock().await;
301 match &*guard {
302 State::Connected(c) => Ok(c.session.clone()),
303 State::Disconnected => Err(anyhow!("not connected")),
304 }
305 }
306
307 pub async fn browse_children(&self, node_id: &NodeId) -> Result<Vec<TreeChild>> {
308 let session = self.session().await?;
309 let desc = browse_hierarchical(node_id.clone());
310 let mut results = session
311 .browse(&[desc], 0, None)
312 .await
313 .map_err(|s| anyhow!("browse failed: {s}"))?;
314 let result = results
315 .pop()
316 .ok_or_else(|| anyhow!("empty browse result"))?;
317 let refs = result.references.unwrap_or_default();
318
319 let mut seen: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
320 let mut children: Vec<TreeChild> = Vec::with_capacity(refs.len());
321 for r in &refs {
322 if is_excluded_tree_reference(&r.reference_type_id) {
323 continue;
324 }
325 let child = reference_to_tree_child(r);
326 if seen.insert(child.node_id.clone()) {
327 children.push(child);
328 }
329 }
330 let target_ids: Vec<NodeId> = children.iter().map(|c| c.node_id.clone()).collect();
331 let has_kids = has_children_batch(&session, &target_ids).await;
332 for (child, hk) in children.iter_mut().zip(has_kids.into_iter()) {
333 child.has_children = hk;
334 }
335 Ok(children)
336 }
337
338 pub async fn read_node_summary(&self, node_id: &NodeId) -> Result<NodeSummary> {
339 let session = self.session().await?;
340 let to_read: Vec<ReadValueId> = ALL_ATTRIBUTES
341 .iter()
342 .map(|(a, _)| ReadValueId::new(node_id.clone(), *a))
343 .collect();
344 let values = session
345 .read(&to_read, TimestampsToReturn::Both, 0.0)
346 .await
347 .map_err(|s| anyhow!("read failed: {s}"))?;
348
349 let mut attributes: Vec<NodeAttribute> = Vec::new();
350 for ((attr_id, name), dv) in ALL_ATTRIBUTES.iter().zip(values.iter()) {
351 if !attribute_status_ok(dv) {
352 continue;
353 }
354 let Some(v) = dv.value.as_ref() else { continue };
355 let tree = format_attribute_value(*attr_id, v, &session);
356 attributes.push(NodeAttribute {
357 name: name.to_string(),
358 value: tree,
359 });
360 if matches!(attr_id, AttributeId::Value) {
361 if let Some(s) = dv.status.map(|s| s.to_string()) {
362 attributes.push(NodeAttribute {
363 name: "StatusCode".to_string(),
364 value: ValueTree::Leaf(s),
365 });
366 }
367 if let Some(t) = dv.source_timestamp.as_ref() {
368 attributes.push(NodeAttribute {
369 name: "SourceTimestamp".to_string(),
370 value: ValueTree::Leaf(t.to_string()),
371 });
372 }
373 if let Some(t) = dv.server_timestamp.as_ref() {
374 attributes.push(NodeAttribute {
375 name: "ServerTimestamp".to_string(),
376 value: ValueTree::Leaf(t.to_string()),
377 });
378 }
379 }
380 }
381 attributes.sort_by(|a, b| {
382 let rank = |n: &str| if n == "Value" { 0 } else { 1 };
383 rank(&a.name).cmp(&rank(&b.name)).then_with(|| a.name.cmp(&b.name))
384 });
385
386 Ok(NodeSummary {
387 node_id: node_id.clone(),
388 attributes,
389 })
390 }
391
392 pub async fn read_method_signature(&self, method_node_id: &NodeId) -> Result<MethodSignature> {
393 let session = self.session().await?;
394 let node_class = read_node_class(&session, method_node_id).await?;
395 if node_class != NodeClass::Method {
396 return Err(anyhow!("node {method_node_id} is not a Method ({node_class:?})"));
397 }
398 let parent_object = read_inverse_parent(&session, method_node_id)
399 .await?
400 .ok_or_else(|| anyhow!("method has no parent object"))?;
401 let method_display_name = read_display_name(&session, method_node_id)
402 .await
403 .unwrap_or_else(|_| method_node_id.to_string());
404
405 let (inputs_node, outputs_node) = find_argument_properties(&session, method_node_id).await?;
406 let inputs = match inputs_node {
407 Some(n) => read_argument_list(&session, &n).await?,
408 None => Vec::new(),
409 };
410 let outputs = match outputs_node {
411 Some(n) => read_argument_list(&session, &n).await?,
412 None => Vec::new(),
413 };
414
415 let mut input_args = Vec::with_capacity(inputs.len());
416 for a in inputs {
417 input_args.push(argument_to_method_argument(&session, a).await);
418 }
419 let mut output_args = Vec::with_capacity(outputs.len());
420 for a in outputs {
421 output_args.push(argument_to_method_argument(&session, a).await);
422 }
423
424 Ok(MethodSignature {
425 parent_object,
426 method_node: method_node_id.clone(),
427 method_display_name,
428 inputs: input_args,
429 outputs: output_args,
430 })
431 }
432
433 pub async fn call_method(
434 &self,
435 parent_object: &NodeId,
436 method_node_id: &NodeId,
437 inputs: Vec<Variant>,
438 ) -> Result<MethodCallOutcome> {
439 let session = self.session().await?;
440 let request = CallMethodRequest {
441 object_id: parent_object.clone(),
442 method_id: method_node_id.clone(),
443 input_arguments: Some(inputs),
444 };
445 let r = session
446 .call_one(request)
447 .await
448 .map_err(|s| anyhow!("call failed: {s}"))?;
449 let status = r.status_code.to_string();
450 let outputs = r
451 .output_arguments
452 .unwrap_or_default()
453 .iter()
454 .map(|v| variant_to_tree(&session, v))
455 .collect();
456 let input_arg_errors = r
457 .input_argument_results
458 .unwrap_or_default()
459 .into_iter()
460 .map(|s| if s.is_good() { None } else { Some(s.to_string()) })
461 .collect();
462 Ok(MethodCallOutcome {
463 status,
464 outputs,
465 input_arg_errors,
466 })
467 }
468
469 pub async fn subscribe(
470 &self,
471 node: NodeId,
472 tx: mpsc::UnboundedSender<UiUpdate>,
473 ) -> Result<String> {
474 let mut guard = self.state.lock().await;
475 let connected = match &mut *guard {
476 State::Connected(c) => c,
477 State::Disconnected => return Err(anyhow!("not connected")),
478 };
479 let session = connected.session.clone();
480
481 if connected.sub.is_none() {
482 let session_for_cb = session.clone();
483 let tx_for_cb = tx.clone();
484 let callback = DataChangeCallback::new(move |dv, item| {
485 let node = item.item_to_monitor().node_id.clone();
486 let (value, status, timestamp) = format_data_change(&session_for_cb, &dv);
487 let _ = tx_for_cb.send(UiUpdate::DataChange {
488 node,
489 value,
490 status,
491 timestamp,
492 });
493 });
494 let sub_id = session
495 .create_subscription(Duration::from_millis(500), 1200, 100, 0, 0, true, callback)
496 .await
497 .map_err(|s| anyhow!("create_subscription failed: {s}"))?;
498 connected.sub = Some(SubState {
499 sub_id,
500 items: HashMap::new(),
501 next_handle: 1,
502 });
503 }
504
505 let sub = connected.sub.as_mut().unwrap();
506 if sub.items.contains_key(&node) {
507 drop(guard);
508 let name = read_display_name(&session, &node).await.unwrap_or_else(|_| node.to_string());
509 return Ok(name);
510 }
511 let handle = sub.next_handle;
512 sub.next_handle = sub.next_handle.wrapping_add(1).max(1);
513 let sub_id = sub.sub_id;
514
515 let request = MonitoredItemCreateRequest {
516 item_to_monitor: ReadValueId {
517 node_id: node.clone(),
518 attribute_id: AttributeId::Value as u32,
519 ..Default::default()
520 },
521 monitoring_mode: MonitoringMode::Reporting,
522 requested_parameters: MonitoringParameters {
523 client_handle: handle,
524 sampling_interval: 0.0,
525 queue_size: 10,
526 discard_oldest: true,
527 ..Default::default()
528 },
529 };
530
531 let results = session
532 .create_monitored_items(sub_id, TimestampsToReturn::Both, vec![request])
533 .await
534 .map_err(|s| anyhow!("create_monitored_items failed: {s}"))?;
535 let created = results
536 .into_iter()
537 .next()
538 .ok_or_else(|| anyhow!("empty create_monitored_items result"))?;
539 let mi_status = created.result.status_code;
540 if !mi_status.is_good() {
541 return Err(anyhow!("monitored item rejected: {mi_status}"));
542 }
543 let mi_id = created.result.monitored_item_id;
544 sub.items.insert(node.clone(), mi_id);
545 drop(guard);
546
547 let name = read_display_name(&session, &node).await.unwrap_or_else(|_| node.to_string());
548 Ok(name)
549 }
550
551 pub async fn unsubscribe(&self, node: &NodeId) -> Result<()> {
552 let mut guard = self.state.lock().await;
553 let connected = match &mut *guard {
554 State::Connected(c) => c,
555 State::Disconnected => return Err(anyhow!("not connected")),
556 };
557 let session = connected.session.clone();
558 let Some(sub) = connected.sub.as_mut() else {
559 return Err(anyhow!("no active subscription"));
560 };
561 let Some(mi_id) = sub.items.remove(node) else {
562 return Err(anyhow!("node {node} is not subscribed"));
563 };
564 let sub_id = sub.sub_id;
565 let items_empty = sub.items.is_empty();
566
567 session
568 .delete_monitored_items(sub_id, &[mi_id])
569 .await
570 .map_err(|s| anyhow!("delete_monitored_items failed: {s}"))?;
571
572 if items_empty {
573 connected.sub = None;
574 session
575 .delete_subscription(sub_id)
576 .await
577 .map_err(|s| anyhow!("delete_subscription failed: {s}"))?;
578 }
579 Ok(())
580 }
581
582 pub async fn read_write_target(
583 &self,
584 node: &NodeId,
585 attr_name: &str,
586 ) -> Result<WriteTarget> {
587 let session = self.session().await?;
588 if attr_name == "Value" {
589 return read_value_write_target(&session, node).await;
590 }
591 let Some((attr_id, spec)) = fixed_attribute_spec(attr_name) else {
592 return Err(anyhow!("attribute {attr_name} is not editable yet"));
593 };
594 let to_read = vec![ReadValueId::new(node.clone(), attr_id)];
595 let values = session
596 .read(&to_read, TimestampsToReturn::Neither, 0.0)
597 .await
598 .map_err(|s| anyhow!("read failed: {s}"))?;
599 let dv = values
600 .into_iter()
601 .next()
602 .ok_or_else(|| anyhow!("missing attribute value"))?;
603 let current_value = format_attribute_current(&session, &spec, dv.value.as_ref());
604 let type_label = fixed_type_label(&spec).to_string();
605 Ok(WriteTarget {
606 spec,
607 type_label,
608 current_value,
609 })
610 }
611
612 pub async fn write_attribute(
613 &self,
614 node: &NodeId,
615 attr_name: &str,
616 value: Variant,
617 ) -> Result<()> {
618 let attr_id = if attr_name == "Value" {
619 AttributeId::Value
620 } else {
621 fixed_attribute_spec(attr_name)
622 .map(|(id, _)| id)
623 .ok_or_else(|| anyhow!("attribute {attr_name} is not editable yet"))?
624 };
625 let session = self.session().await?;
626 let wv = WriteValue {
627 node_id: node.clone(),
628 attribute_id: attr_id as u32,
629 index_range: NumericRange::default(),
630 value: DataValue {
631 value: Some(value),
632 ..Default::default()
633 },
634 };
635 let results = session
636 .write(&[wv])
637 .await
638 .map_err(|s| anyhow!("write failed: {s}"))?;
639 let status = results
640 .into_iter()
641 .next()
642 .ok_or_else(|| anyhow!("empty write result"))?;
643 if !status.is_good() {
644 return Err(anyhow!("write status: {status}"));
645 }
646 Ok(())
647 }
648
649 pub async fn browse_references(&self, node_id: &NodeId) -> Result<Vec<ReferenceRow>> {
650 let session = self.session().await?;
651 let desc = BrowseDescription {
652 node_id: node_id.clone(),
653 browse_direction: BrowseDirection::Both,
654 reference_type_id: NodeId::new(0, ReferenceTypeId::References as u32),
655 include_subtypes: true,
656 node_class_mask: NodeClassMask::all().bits(),
657 result_mask: BrowseDescriptionResultMask::all().bits(),
658 };
659 let mut results = session
660 .browse(&[desc], 0, None)
661 .await
662 .map_err(|s| anyhow!("browse failed: {s}"))?;
663 let result = results
664 .pop()
665 .ok_or_else(|| anyhow!("empty browse result"))?;
666 let refs = result.references.unwrap_or_default();
667
668 let mut rows = Vec::with_capacity(refs.len());
669 for r in refs {
670 rows.push(reference_to_row(&session, r).await);
671 }
672 Ok(rows)
673 }
674}
675
676async fn read_node_class(session: &Session, node_id: &NodeId) -> Result<NodeClass> {
677 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::NodeClass)];
678 let values = session
679 .read(&to_read, TimestampsToReturn::Neither, 0.0)
680 .await
681 .map_err(|s| anyhow!("read NodeClass failed: {s}"))?;
682 let v = values
683 .into_iter()
684 .next()
685 .and_then(|v| v.value)
686 .ok_or_else(|| anyhow!("NodeClass attribute missing for {node_id}"))?;
687 match v {
688 Variant::Int32(i) => NodeClass::try_from(i)
689 .map_err(|_| anyhow!("invalid NodeClass {i} for {node_id}")),
690 other => Err(anyhow!("unexpected NodeClass variant: {other:?}")),
691 }
692}
693
694async fn read_display_name(session: &Session, node_id: &NodeId) -> Result<String> {
695 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::DisplayName)];
696 let values = session
697 .read(&to_read, TimestampsToReturn::Neither, 0.0)
698 .await
699 .map_err(|s| anyhow!("read DisplayName failed: {s}"))?;
700 let text = values
701 .into_iter()
702 .next()
703 .and_then(|v| v.value)
704 .and_then(|v| match v {
705 Variant::LocalizedText(t) => Some(t.text.to_string()),
706 _ => None,
707 });
708 Ok(text.unwrap_or_else(|| node_id.to_string()))
709}
710
711async fn find_argument_properties(
712 session: &Session,
713 method_node_id: &NodeId,
714) -> Result<(Option<NodeId>, Option<NodeId>)> {
715 let desc = BrowseDescription {
716 node_id: method_node_id.clone(),
717 browse_direction: BrowseDirection::Forward,
718 reference_type_id: NodeId::new(0, ReferenceTypeId::HasProperty as u32),
719 include_subtypes: true,
720 node_class_mask: NodeClassMask::VARIABLE.bits(),
721 result_mask: BrowseDescriptionResultMask::all().bits(),
722 };
723 let mut results = session
724 .browse(&[desc], 0, None)
725 .await
726 .map_err(|s| anyhow!("browse properties failed: {s}"))?;
727 let refs = results
728 .pop()
729 .and_then(|r| r.references)
730 .unwrap_or_default();
731 let mut inputs = None;
732 let mut outputs = None;
733 for r in refs {
734 if r.browse_name.namespace_index != 0 {
735 continue;
736 }
737 match r.browse_name.name.as_ref() {
738 "InputArguments" => inputs = Some(r.node_id.node_id),
739 "OutputArguments" => outputs = Some(r.node_id.node_id),
740 _ => {}
741 }
742 }
743 Ok((inputs, outputs))
744}
745
746async fn read_argument_list(session: &Session, property_node: &NodeId) -> Result<Vec<Argument>> {
747 let to_read = vec![ReadValueId::new(property_node.clone(), AttributeId::Value)];
748 let values = session
749 .read(&to_read, TimestampsToReturn::Neither, 0.0)
750 .await
751 .map_err(|s| anyhow!("read {property_node} failed: {s}"))?;
752 let Some(variant) = values.into_iter().next().and_then(|v| v.value) else {
753 return Ok(Vec::new());
754 };
755 if matches!(variant, Variant::Empty) {
756 return Ok(Vec::new());
757 }
758 <Vec<Argument>>::try_from_variant(variant)
759 .map_err(|e| anyhow!("decode Argument array failed: {e}"))
760}
761
762async fn argument_to_method_argument(session: &Session, a: Argument) -> MethodArgument {
763 let type_label = data_type_label(session, &a.data_type, a.value_rank).await;
764 MethodArgument {
765 name: a.name.to_string(),
766 description: a.description.text.to_string(),
767 data_type: a.data_type,
768 value_rank: a.value_rank,
769 type_label,
770 }
771}
772
773async fn data_type_label(session: &Session, data_type: &NodeId, value_rank: i32) -> String {
774 let base = match builtin_data_type_label(data_type) {
775 Some(s) => s.to_string(),
776 None => read_display_name(session, data_type)
777 .await
778 .unwrap_or_else(|_| data_type.to_string()),
779 };
780 if value_rank >= 1 {
781 format!("{base}[]")
782 } else {
783 base
784 }
785}
786
787fn builtin_data_type_label(id: &NodeId) -> Option<&'static str> {
788 if id.namespace != 0 {
789 return None;
790 }
791 let Identifier::Numeric(n) = id.identifier else {
792 return None;
793 };
794 Some(match n {
795 x if x == DataTypeId::Boolean as u32 => "Boolean",
796 x if x == DataTypeId::SByte as u32 => "SByte",
797 x if x == DataTypeId::Byte as u32 => "Byte",
798 x if x == DataTypeId::Int16 as u32 => "Int16",
799 x if x == DataTypeId::UInt16 as u32 => "UInt16",
800 x if x == DataTypeId::Int32 as u32 => "Int32",
801 x if x == DataTypeId::UInt32 as u32 => "UInt32",
802 x if x == DataTypeId::Int64 as u32 => "Int64",
803 x if x == DataTypeId::UInt64 as u32 => "UInt64",
804 x if x == DataTypeId::Float as u32 => "Float",
805 x if x == DataTypeId::Double as u32 => "Double",
806 x if x == DataTypeId::String as u32 => "String",
807 x if x == DataTypeId::DateTime as u32 => "DateTime",
808 x if x == DataTypeId::Guid as u32 => "Guid",
809 x if x == DataTypeId::ByteString as u32 => "ByteString",
810 x if x == DataTypeId::NodeId as u32 => "NodeId",
811 x if x == DataTypeId::ExpandedNodeId as u32 => "ExpandedNodeId",
812 x if x == DataTypeId::StatusCode as u32 => "StatusCode",
813 x if x == DataTypeId::QualifiedName as u32 => "QualifiedName",
814 x if x == DataTypeId::LocalizedText as u32 => "LocalizedText",
815 _ => return None,
816 })
817}
818
819async fn read_value_write_target(session: &Session, node: &NodeId) -> Result<WriteTarget> {
820 let to_read = vec![
821 ReadValueId::new(node.clone(), AttributeId::DataType),
822 ReadValueId::new(node.clone(), AttributeId::ValueRank),
823 ReadValueId::new(node.clone(), AttributeId::Value),
824 ];
825 let values = session
826 .read(&to_read, TimestampsToReturn::Neither, 0.0)
827 .await
828 .map_err(|s| anyhow!("read failed: {s}"))?;
829 let mut iter = values.into_iter();
830 let data_type_dv = iter.next().ok_or_else(|| anyhow!("missing DataType"))?;
831 let value_rank_dv = iter.next().ok_or_else(|| anyhow!("missing ValueRank"))?;
832 let value_dv = iter.next().ok_or_else(|| anyhow!("missing Value"))?;
833
834 let data_type = match data_type_dv.value {
835 Some(Variant::NodeId(n)) => *n,
836 other => return Err(anyhow!("unexpected DataType variant: {other:?}")),
837 };
838 let value_rank = match value_rank_dv.value {
839 Some(Variant::Int32(i)) => i,
840 Some(Variant::Empty) | None => -1,
841 other => return Err(anyhow!("unexpected ValueRank variant: {other:?}")),
842 };
843 let type_label = data_type_label(session, &data_type, value_rank).await;
844 let current_value = match value_dv.value.as_ref() {
845 Some(v) => variant_to_tree(session, v).format_inline(),
846 None => String::new(),
847 };
848 Ok(WriteTarget {
849 spec: AttrSpec::Value { data_type, value_rank },
850 type_label,
851 current_value,
852 })
853}
854
855fn fixed_attribute_spec(attr_name: &str) -> Option<(AttributeId, AttrSpec)> {
856 Some(match attr_name {
857 "DisplayName" => (AttributeId::DisplayName, AttrSpec::LocalizedText),
858 "Description" => (AttributeId::Description, AttrSpec::LocalizedText),
859 "BrowseName" => (AttributeId::BrowseName, AttrSpec::QualifiedName),
860 "Historizing" => (AttributeId::Historizing, AttrSpec::Boolean),
861 "Executable" => (AttributeId::Executable, AttrSpec::Boolean),
862 "UserExecutable" => (AttributeId::UserExecutable, AttrSpec::Boolean),
863 "IsAbstract" => (AttributeId::IsAbstract, AttrSpec::Boolean),
864 "Symmetric" => (AttributeId::Symmetric, AttrSpec::Boolean),
865 "ContainsNoLoops" => (AttributeId::ContainsNoLoops, AttrSpec::Boolean),
866 "WriteMask" => (AttributeId::WriteMask, AttrSpec::UInt32),
867 "UserWriteMask" => (AttributeId::UserWriteMask, AttrSpec::UInt32),
868 "AccessLevelEx" => (AttributeId::AccessLevelEx, AttrSpec::UInt32),
869 "AccessLevel" => (AttributeId::AccessLevel, AttrSpec::Byte),
870 "UserAccessLevel" => (AttributeId::UserAccessLevel, AttrSpec::Byte),
871 "EventNotifier" => (AttributeId::EventNotifier, AttrSpec::Byte),
872 "MinimumSamplingInterval" => (AttributeId::MinimumSamplingInterval, AttrSpec::Double),
873 "ValueRank" => (AttributeId::ValueRank, AttrSpec::Int32),
874 _ => return None,
875 })
876}
877
878fn fixed_type_label(spec: &AttrSpec) -> &'static str {
879 match spec {
880 AttrSpec::Value { .. } => "Value",
881 AttrSpec::LocalizedText => "LocalizedText",
882 AttrSpec::QualifiedName => "QualifiedName",
883 AttrSpec::Boolean => "Boolean",
884 AttrSpec::UInt32 => "UInt32",
885 AttrSpec::Byte => "Byte",
886 AttrSpec::Double => "Double",
887 AttrSpec::Int32 => "Int32",
888 }
889}
890
891fn format_attribute_current(session: &Session, spec: &AttrSpec, value: Option<&Variant>) -> String {
892 let Some(v) = value else { return String::new() };
893 if matches!(spec, AttrSpec::QualifiedName)
894 && let Variant::QualifiedName(q) = v
895 {
896 return if q.namespace_index == 0 {
897 q.name.to_string()
898 } else {
899 format!("{}:{}", q.namespace_index, q.name)
900 };
901 }
902 variant_to_tree(session, v).format_inline()
903}
904
905pub fn parse_attribute_value(spec: &AttrSpec, input: &str) -> Result<Variant, String> {
906 let s = input.trim();
907 match spec {
908 AttrSpec::Value { data_type, value_rank } => parse_variant(input, data_type, *value_rank),
909 AttrSpec::LocalizedText => Ok(Variant::LocalizedText(Box::new(LocalizedText::from(s)))),
910 AttrSpec::QualifiedName => Ok(Variant::QualifiedName(Box::new(parse_qualified_name(s)))),
911 AttrSpec::Boolean => s
912 .parse::<bool>()
913 .map(Variant::Boolean)
914 .map_err(|e| format!("invalid Boolean: {e}")),
915 AttrSpec::UInt32 => s
916 .parse::<u32>()
917 .map(Variant::UInt32)
918 .map_err(|e| format!("invalid UInt32: {e}")),
919 AttrSpec::Byte => s
920 .parse::<u8>()
921 .map(Variant::Byte)
922 .map_err(|e| format!("invalid Byte: {e}")),
923 AttrSpec::Double => s
924 .parse::<f64>()
925 .map(Variant::Double)
926 .map_err(|e| format!("invalid Double: {e}")),
927 AttrSpec::Int32 => s
928 .parse::<i32>()
929 .map(Variant::Int32)
930 .map_err(|e| format!("invalid Int32: {e}")),
931 }
932}
933
934pub fn parse_variant(input: &str, data_type: &NodeId, value_rank: i32) -> Result<Variant, String> {
937 let is_array = value_rank >= 1;
938 let scalar_type = builtin_scalar_type(data_type)
939 .ok_or_else(|| format!("unsupported data type: {data_type}"))?;
940 if !is_array {
941 return parse_scalar(input.trim(), scalar_type);
942 }
943 let trimmed = input.trim().trim_start_matches('[').trim_end_matches(']');
944 let tokens: Vec<&str> = if trimmed.is_empty() {
945 Vec::new()
946 } else {
947 trimmed.split(',').map(|s| s.trim()).collect()
948 };
949 let mut variants = Vec::with_capacity(tokens.len());
950 for (i, t) in tokens.iter().enumerate() {
951 let v = parse_scalar(t, scalar_type).map_err(|e| format!("item {i}: {e}"))?;
952 variants.push(v);
953 }
954 let variant_type = scalar_type_to_variant_scalar(scalar_type);
955 let array = Array::new(variant_type, variants).map_err(|e| format!("array build: {e}"))?;
956 Ok(Variant::Array(Box::new(array)))
957}
958
959#[derive(Clone, Copy)]
960enum ScalarType {
961 Boolean,
962 SByte,
963 Byte,
964 Int16,
965 UInt16,
966 Int32,
967 UInt32,
968 Int64,
969 UInt64,
970 Float,
971 Double,
972 String,
973 NodeId,
974 Guid,
975}
976
977fn builtin_scalar_type(id: &NodeId) -> Option<ScalarType> {
978 if id.namespace != 0 {
979 return None;
980 }
981 let Identifier::Numeric(n) = id.identifier else {
982 return None;
983 };
984 Some(match n {
985 x if x == DataTypeId::Boolean as u32 => ScalarType::Boolean,
986 x if x == DataTypeId::SByte as u32 => ScalarType::SByte,
987 x if x == DataTypeId::Byte as u32 => ScalarType::Byte,
988 x if x == DataTypeId::Int16 as u32 => ScalarType::Int16,
989 x if x == DataTypeId::UInt16 as u32 => ScalarType::UInt16,
990 x if x == DataTypeId::Int32 as u32 => ScalarType::Int32,
991 x if x == DataTypeId::UInt32 as u32 => ScalarType::UInt32,
992 x if x == DataTypeId::Int64 as u32 => ScalarType::Int64,
993 x if x == DataTypeId::UInt64 as u32 => ScalarType::UInt64,
994 x if x == DataTypeId::Float as u32 => ScalarType::Float,
995 x if x == DataTypeId::Double as u32 => ScalarType::Double,
996 x if x == DataTypeId::String as u32 => ScalarType::String,
997 x if x == DataTypeId::NodeId as u32 => ScalarType::NodeId,
998 x if x == DataTypeId::Guid as u32 => ScalarType::Guid,
999 _ => return None,
1000 })
1001}
1002
1003fn scalar_type_to_variant_scalar(t: ScalarType) -> VariantScalarTypeId {
1004 match t {
1005 ScalarType::Boolean => VariantScalarTypeId::Boolean,
1006 ScalarType::SByte => VariantScalarTypeId::SByte,
1007 ScalarType::Byte => VariantScalarTypeId::Byte,
1008 ScalarType::Int16 => VariantScalarTypeId::Int16,
1009 ScalarType::UInt16 => VariantScalarTypeId::UInt16,
1010 ScalarType::Int32 => VariantScalarTypeId::Int32,
1011 ScalarType::UInt32 => VariantScalarTypeId::UInt32,
1012 ScalarType::Int64 => VariantScalarTypeId::Int64,
1013 ScalarType::UInt64 => VariantScalarTypeId::UInt64,
1014 ScalarType::Float => VariantScalarTypeId::Float,
1015 ScalarType::Double => VariantScalarTypeId::Double,
1016 ScalarType::String => VariantScalarTypeId::String,
1017 ScalarType::NodeId => VariantScalarTypeId::NodeId,
1018 ScalarType::Guid => VariantScalarTypeId::Guid,
1019 }
1020}
1021
1022fn parse_scalar(s: &str, t: ScalarType) -> Result<Variant, String> {
1023 if matches!(t, ScalarType::String) {
1024 return Ok(Variant::String(UAString::from(s)));
1025 }
1026 if s.is_empty() {
1027 return Err("value required".to_string());
1028 }
1029 Ok(match t {
1030 ScalarType::Boolean => Variant::Boolean(
1031 s.parse::<bool>().map_err(|e| format!("invalid Boolean: {e}"))?,
1032 ),
1033 ScalarType::SByte => {
1034 Variant::SByte(s.parse::<i8>().map_err(|e| format!("invalid SByte: {e}"))?)
1035 }
1036 ScalarType::Byte => {
1037 Variant::Byte(s.parse::<u8>().map_err(|e| format!("invalid Byte: {e}"))?)
1038 }
1039 ScalarType::Int16 => {
1040 Variant::Int16(s.parse::<i16>().map_err(|e| format!("invalid Int16: {e}"))?)
1041 }
1042 ScalarType::UInt16 => Variant::UInt16(
1043 s.parse::<u16>().map_err(|e| format!("invalid UInt16: {e}"))?,
1044 ),
1045 ScalarType::Int32 => {
1046 Variant::Int32(s.parse::<i32>().map_err(|e| format!("invalid Int32: {e}"))?)
1047 }
1048 ScalarType::UInt32 => Variant::UInt32(
1049 s.parse::<u32>().map_err(|e| format!("invalid UInt32: {e}"))?,
1050 ),
1051 ScalarType::Int64 => {
1052 Variant::Int64(s.parse::<i64>().map_err(|e| format!("invalid Int64: {e}"))?)
1053 }
1054 ScalarType::UInt64 => Variant::UInt64(
1055 s.parse::<u64>().map_err(|e| format!("invalid UInt64: {e}"))?,
1056 ),
1057 ScalarType::Float => {
1058 Variant::Float(s.parse::<f32>().map_err(|e| format!("invalid Float: {e}"))?)
1059 }
1060 ScalarType::Double => Variant::Double(
1061 s.parse::<f64>().map_err(|e| format!("invalid Double: {e}"))?,
1062 ),
1063 ScalarType::String => unreachable!(),
1064 ScalarType::NodeId => Variant::NodeId(Box::new(
1065 NodeId::from_str(s).map_err(|e| format!("invalid NodeId: {e}"))?,
1066 )),
1067 ScalarType::Guid => Variant::Guid(Box::new(
1068 Guid::from_str(s).map_err(|e| format!("invalid Guid: {e:?}"))?,
1069 )),
1070 })
1071}
1072
1073async fn read_browse_name(session: &Session, node_id: &NodeId) -> Result<String> {
1074 let to_read = vec![ReadValueId::new(node_id.clone(), AttributeId::BrowseName)];
1075 let values = session
1076 .read(&to_read, TimestampsToReturn::Neither, 0.0)
1077 .await
1078 .map_err(|s| anyhow!("read BrowseName failed: {s}"))?;
1079 let q = values
1080 .into_iter()
1081 .next()
1082 .and_then(|v| v.value)
1083 .and_then(|v| match v {
1084 Variant::QualifiedName(q) => Some(*q),
1085 _ => None,
1086 });
1087 Ok(match q {
1088 Some(q) => format_path_segment(q.namespace_index, q.name.as_ref()),
1089 None => node_id.to_string(),
1090 })
1091}
1092
1093async fn read_inverse_parent(session: &Session, node_id: &NodeId) -> Result<Option<NodeId>> {
1094 let desc = BrowseDescription {
1095 node_id: node_id.clone(),
1096 browse_direction: BrowseDirection::Inverse,
1097 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
1098 include_subtypes: true,
1099 node_class_mask: NodeClassMask::all().bits(),
1100 result_mask: BrowseDescriptionResultMask::all().bits(),
1101 };
1102 let mut results = session
1103 .browse(&[desc], 0, None)
1104 .await
1105 .map_err(|s| anyhow!("browse inverse failed: {s}"))?;
1106 let parent = results
1107 .pop()
1108 .and_then(|r| r.references)
1109 .and_then(|refs| {
1110 refs.into_iter()
1111 .find(|rd| !is_excluded_tree_reference(&rd.reference_type_id))
1112 })
1113 .map(|r| r.node_id.node_id);
1114 Ok(parent)
1115}
1116
1117fn format_path_segment(ns: u16, name: &str) -> String {
1118 let escaped = escape_browse_name(name);
1119 if ns == 0 {
1120 escaped
1121 } else {
1122 format!("{ns}:{escaped}")
1123 }
1124}
1125
1126fn escape_browse_name(s: &str) -> String {
1127 let mut out = String::with_capacity(s.len());
1128 for c in s.chars() {
1129 if matches!(c, '&' | '/' | '.' | '<' | '>' | ':' | '#' | '!' | ';') {
1130 out.push('&');
1131 }
1132 out.push(c);
1133 }
1134 out
1135}
1136
1137fn is_excluded_tree_reference(ref_type: &NodeId) -> bool {
1138 if ref_type.namespace != 0 {
1139 return false;
1140 }
1141 let id = match &ref_type.identifier {
1142 opcua::types::Identifier::Numeric(n) => *n,
1143 _ => return false,
1144 };
1145 matches!(
1146 id,
1147 x if x == ReferenceTypeId::HasEventSource as u32
1148 || x == ReferenceTypeId::HasNotifier as u32
1149 )
1150}
1151
1152fn browse_hierarchical(node_id: NodeId) -> BrowseDescription {
1153 BrowseDescription {
1154 node_id,
1155 browse_direction: BrowseDirection::Forward,
1156 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
1157 include_subtypes: true,
1158 node_class_mask: NodeClassMask::all().bits(),
1159 result_mask: BrowseDescriptionResultMask::all().bits(),
1160 }
1161}
1162
1163fn reference_to_tree_child(r: &ReferenceDescription) -> TreeChild {
1164 TreeChild {
1165 node_id: expanded_to_local(&r.node_id),
1166 browse_name: r.browse_name.name.to_string(),
1167 display_name: r.display_name.text.to_string(),
1168 node_class: r.node_class,
1169 has_children: false,
1170 }
1171}
1172
1173async fn reference_to_row(session: &Session, r: ReferenceDescription) -> ReferenceRow {
1174 let reference_type = resolve_reference_type_name(session, &r.reference_type_id).await;
1175 ReferenceRow {
1176 reference_type,
1177 is_forward: r.is_forward,
1178 target_node_id: expanded_to_local(&r.node_id),
1179 target_browse_name: r.browse_name.name.to_string(),
1180 target_display_name: r.display_name.text.to_string(),
1181 target_node_class: r.node_class,
1182 }
1183}
1184
1185async fn resolve_reference_type_name(session: &Session, ref_type: &NodeId) -> String {
1186 let read = vec![ReadValueId::new(ref_type.clone(), AttributeId::DisplayName)];
1187 match session.read(&read, TimestampsToReturn::Neither, 0.0).await {
1188 Ok(vals) => vals
1189 .into_iter()
1190 .next()
1191 .and_then(|v| v.value)
1192 .and_then(|v| match v {
1193 Variant::LocalizedText(t) => Some(t.text.to_string()),
1194 _ => None,
1195 })
1196 .unwrap_or_else(|| ref_type.to_string()),
1197 Err(_) => ref_type.to_string(),
1198 }
1199}
1200
1201async fn has_children_batch(session: &Session, ids: &[NodeId]) -> Vec<bool> {
1202 if ids.is_empty() {
1203 return Vec::new();
1204 }
1205 let descs: Vec<BrowseDescription> = ids
1206 .iter()
1207 .map(|id| BrowseDescription {
1208 node_id: id.clone(),
1209 browse_direction: BrowseDirection::Forward,
1210 reference_type_id: NodeId::new(0, ReferenceTypeId::HierarchicalReferences as u32),
1211 include_subtypes: true,
1212 node_class_mask: NodeClassMask::all().bits(),
1213 result_mask: BrowseDescriptionResultMask::RESULT_MASK_REFERENCE_TYPE.bits(),
1214 })
1215 .collect();
1216 match session.browse(&descs, 0, None).await {
1217 Ok(results) => results
1218 .into_iter()
1219 .map(|r| {
1220 r.references
1221 .map(|refs| {
1222 refs.iter()
1223 .any(|rd| !is_excluded_tree_reference(&rd.reference_type_id))
1224 })
1225 .unwrap_or(false)
1226 })
1227 .collect(),
1228 Err(_) => vec![false; ids.len()],
1229 }
1230}
1231
1232fn expanded_to_local(eid: &ExpandedNodeId) -> NodeId {
1233 eid.node_id.clone()
1234}
1235
1236fn log_client_cert_hint() {
1237 let path = std::env::current_dir()
1238 .unwrap_or_default()
1239 .join("pki/own/cert.der");
1240 tracing::info!(
1241 "encrypted connection as \"{}\" ({}); client certificate at {}",
1242 APPLICATION_NAME,
1243 APPLICATION_URI,
1244 path.display()
1245 );
1246 tracing::info!(
1247 "if the server rejects the connection, copy that file into the server's trusted certs folder"
1248 );
1249}
1250
1251fn looks_like_cert_trust_error(msg: &str) -> bool {
1252 let lower = msg.to_lowercase();
1253 lower.contains("badsecurity")
1254 || lower.contains("badcertificate")
1255 || lower.contains("certificatevalidation")
1256 || lower.contains("untrusted")
1257 || lower.contains("rejected")
1258}
1259
1260fn build_identity_token(auth: &AuthSpec) -> Result<IdentityToken> {
1261 match auth.mode {
1262 AuthMode::Anonymous => Ok(IdentityToken::Anonymous),
1263 AuthMode::UserName => {
1264 if auth.username.is_empty() {
1265 return Err(anyhow!("username required"));
1266 }
1267 Ok(IdentityToken::new_user_name(
1268 auth.username.clone(),
1269 auth.password.clone(),
1270 ))
1271 }
1272 AuthMode::Certificate => {
1273 if auth.cert_path.is_empty() || auth.key_path.is_empty() {
1274 return Err(anyhow!("certificate and private-key paths required"));
1275 }
1276 IdentityToken::new_x509_path(&auth.cert_path, &auth.key_path)
1277 .map_err(|e| anyhow!("failed to load certificate/key: {e}"))
1278 }
1279 }
1280}
1281
1282const APPLICATION_NAME: &str = "Rust OPC UA Client from FreeOpcUa";
1283const APPLICATION_URI: &str = "urn:FreeOpcUa:ua-client";
1284
1285fn warn_insecure_default() {
1286 tracing::warn!(
1287 "INSECURE DEFAULT: server-certificate checks (time, hostname, application-URI) are DISABLED — trusted networks only"
1288 );
1289}
1290
1291fn build_client(verify_cert_metadata: bool) -> Result<opcua::client::Client> {
1292 ClientBuilder::new()
1293 .application_name(APPLICATION_NAME)
1294 .application_uri(APPLICATION_URI)
1295 .product_uri(APPLICATION_URI)
1296 .trust_server_certs(true)
1297 .verify_server_certs(verify_cert_metadata)
1298 .create_sample_keypair(true)
1299 .session_retry_limit(0)
1300 .client()
1301 .map_err(|errs| anyhow!("failed to build OPC UA client: {errs:?}"))
1302}
1303
1304fn security_mode_to_message_mode(m: SecurityMode) -> MessageSecurityMode {
1305 match m {
1306 SecurityMode::None => MessageSecurityMode::None,
1307 SecurityMode::Sign => MessageSecurityMode::Sign,
1308 SecurityMode::SignAndEncrypt => MessageSecurityMode::SignAndEncrypt,
1309 }
1310}
1311
1312fn message_mode_to_security_mode(m: MessageSecurityMode) -> SecurityMode {
1313 match m {
1314 MessageSecurityMode::Sign => SecurityMode::Sign,
1315 MessageSecurityMode::SignAndEncrypt => SecurityMode::SignAndEncrypt,
1316 _ => SecurityMode::None,
1317 }
1318}
1319
1320fn endpoint_description_to_info(ep: EndpointDescription) -> EndpointInfo {
1321 let policy_uri = ep.security_policy_uri.to_string();
1322 let policy_short = SecurityPolicy::from_str(&policy_uri)
1323 .map(|p| p.to_string())
1324 .unwrap_or_else(|_| policy_uri.clone());
1325 let tokens = ep.user_identity_tokens.unwrap_or_default();
1326 let supports_anonymous = tokens
1327 .iter()
1328 .any(|t| matches!(t.token_type, UserTokenType::Anonymous));
1329 let supports_username = tokens
1330 .iter()
1331 .any(|t| matches!(t.token_type, UserTokenType::UserName));
1332 let supports_certificate = tokens
1333 .iter()
1334 .any(|t| matches!(t.token_type, UserTokenType::Certificate));
1335
1336 EndpointInfo {
1337 endpoint_url: ep.endpoint_url.to_string(),
1338 security_policy: policy_short,
1339 security_policy_uri: policy_uri,
1340 security_mode: message_mode_to_security_mode(ep.security_mode),
1341 security_level: ep.security_level,
1342 supports_anonymous,
1343 supports_username,
1344 supports_certificate,
1345 }
1346}
1347
1348const ALL_ATTRIBUTES: &[(AttributeId, &str)] = &[
1349 (AttributeId::AccessLevel, "AccessLevel"),
1350 (AttributeId::AccessLevelEx, "AccessLevelEx"),
1351 (AttributeId::AccessRestrictions, "AccessRestrictions"),
1352 (AttributeId::ArrayDimensions, "ArrayDimensions"),
1353 (AttributeId::BrowseName, "BrowseName"),
1354 (AttributeId::ContainsNoLoops, "ContainsNoLoops"),
1355 (AttributeId::DataType, "DataType"),
1356 (AttributeId::DataTypeDefinition, "DataTypeDefinition"),
1357 (AttributeId::Description, "Description"),
1358 (AttributeId::DisplayName, "DisplayName"),
1359 (AttributeId::EventNotifier, "EventNotifier"),
1360 (AttributeId::Executable, "Executable"),
1361 (AttributeId::Historizing, "Historizing"),
1362 (AttributeId::InverseName, "InverseName"),
1363 (AttributeId::IsAbstract, "IsAbstract"),
1364 (
1365 AttributeId::MinimumSamplingInterval,
1366 "MinimumSamplingInterval",
1367 ),
1368 (AttributeId::NodeClass, "NodeClass"),
1369 (AttributeId::NodeId, "NodeId"),
1370 (AttributeId::RolePermissions, "RolePermissions"),
1371 (AttributeId::Symmetric, "Symmetric"),
1372 (AttributeId::UserAccessLevel, "UserAccessLevel"),
1373 (AttributeId::UserExecutable, "UserExecutable"),
1374 (AttributeId::UserRolePermissions, "UserRolePermissions"),
1375 (AttributeId::UserWriteMask, "UserWriteMask"),
1376 (AttributeId::Value, "Value"),
1377 (AttributeId::ValueRank, "ValueRank"),
1378 (AttributeId::WriteMask, "WriteMask"),
1379];
1380
1381fn attribute_status_ok(dv: &DataValue) -> bool {
1382 match dv.status {
1383 None => dv.value.is_some(),
1384 Some(s) => s.is_good(),
1385 }
1386}
1387
1388fn format_attribute_value(attr: AttributeId, v: &Variant, session: &Session) -> ValueTree {
1389 if matches!(attr, AttributeId::NodeClass)
1390 && let Variant::Int32(i) = v
1391 && let Ok(nc) = NodeClass::try_from(*i)
1392 {
1393 return ValueTree::Leaf(format!("{nc:?}"));
1394 }
1395 variant_to_tree(session, v)
1396}
1397
1398fn format_data_change(session: &Session, dv: &DataValue) -> (String, String, Option<String>) {
1399 let value = match dv.value.as_ref() {
1400 Some(v) => variant_to_tree(session, v).format_inline(),
1401 None => "<null>".to_string(),
1402 };
1403 let status = dv.status.map(|s| s.to_string()).unwrap_or_default();
1404 let timestamp = dv.source_timestamp.as_ref().map(|t| t.to_string());
1405 (value, status, timestamp)
1406}
1407
1408fn variant_to_tree(session: &Session, v: &Variant) -> ValueTree {
1409 match v {
1410 Variant::Empty => ValueTree::Null,
1411 Variant::Boolean(b) => ValueTree::Leaf(b.to_string()),
1412 Variant::SByte(n) => ValueTree::Leaf(n.to_string()),
1413 Variant::Byte(n) => ValueTree::Leaf(n.to_string()),
1414 Variant::Int16(n) => ValueTree::Leaf(n.to_string()),
1415 Variant::UInt16(n) => ValueTree::Leaf(n.to_string()),
1416 Variant::Int32(n) => ValueTree::Leaf(n.to_string()),
1417 Variant::UInt32(n) => ValueTree::Leaf(n.to_string()),
1418 Variant::Int64(n) => ValueTree::Leaf(n.to_string()),
1419 Variant::UInt64(n) => ValueTree::Leaf(n.to_string()),
1420 Variant::Float(n) => ValueTree::Leaf(n.to_string()),
1421 Variant::Double(n) => ValueTree::Leaf(n.to_string()),
1422 Variant::String(s) => ValueTree::Leaf(s.to_string()),
1423 Variant::DateTime(d) => ValueTree::Leaf(d.to_string()),
1424 Variant::Guid(g) => ValueTree::Leaf(format!("{g:?}")),
1425 Variant::StatusCode(s) => ValueTree::Leaf(s.to_string()),
1426 Variant::ByteString(b) => match b.value.as_ref() {
1427 Some(bytes) => ValueTree::Leaf(format!("<{} bytes>", bytes.len())),
1428 None => ValueTree::Null,
1429 },
1430 Variant::XmlElement(_) => ValueTree::Leaf("XmlElement(…)".to_string()),
1431 Variant::QualifiedName(q) => ValueTree::Leaf(q.name.to_string()),
1432 Variant::LocalizedText(t) => ValueTree::Leaf(t.text.to_string()),
1433 Variant::NodeId(n) => ValueTree::Leaf(n.to_string()),
1434 Variant::ExpandedNodeId(n) => ValueTree::Leaf(format!("{n}")),
1435 Variant::ExtensionObject(obj) => extension_object_to_tree(session, obj),
1436 Variant::Variant(inner) => variant_to_tree(session, inner),
1437 Variant::DataValue(_) => ValueTree::Leaf("DataValue(…)".to_string()),
1438 Variant::DiagnosticInfo(_) => ValueTree::Leaf("DiagnosticInfo(…)".to_string()),
1439 Variant::Array(arr) => ValueTree::Array(
1440 arr.values
1441 .iter()
1442 .map(|i| variant_to_tree(session, i))
1443 .collect(),
1444 ),
1445 }
1446}
1447
1448fn extension_object_to_tree(session: &Session, obj: &opcua::types::ExtensionObject) -> ValueTree {
1449 if obj.inner_as::<DynamicStructure>().is_none() {
1450 let label = obj
1451 .type_name()
1452 .map(|n| format!("ExtensionObject ({n})"))
1453 .unwrap_or_else(|| "ExtensionObject".to_string());
1454 return ValueTree::Leaf(label);
1455 }
1456 match dynamic_struct_to_tree(session, obj) {
1457 Some(tree) => tree,
1458 None => ValueTree::Leaf("ExtensionObject (decode failed)".to_string()),
1459 }
1460}
1461
1462fn dynamic_struct_to_tree(
1463 session: &Session,
1464 obj: &opcua::types::ExtensionObject,
1465) -> Option<ValueTree> {
1466 let ds = obj.inner_as::<DynamicStructure>()?;
1467 let ctx_owned = session.context();
1468 let ctx_guard = ctx_owned.read();
1469 let ctx = ctx_guard.context();
1470 let mut buf = Vec::new();
1471 {
1472 let writer_ref: &mut dyn std::io::Write = &mut buf;
1473 let mut writer = JsonStreamWriter::new(writer_ref);
1474 ds.encode(&mut writer, &ctx).ok()?;
1475 writer.finish_document().ok()?;
1476 }
1477 let json: serde_json::Value = serde_json::from_slice(&buf).ok()?;
1478 Some(json_to_tree(&json))
1479}
1480
1481fn json_to_tree(v: &serde_json::Value) -> ValueTree {
1482 match v {
1483 serde_json::Value::Null => ValueTree::Null,
1484 serde_json::Value::Bool(b) => ValueTree::Leaf(b.to_string()),
1485 serde_json::Value::Number(n) => ValueTree::Leaf(n.to_string()),
1486 serde_json::Value::String(s) => ValueTree::Leaf(s.clone()),
1487 serde_json::Value::Array(arr) => ValueTree::Array(arr.iter().map(json_to_tree).collect()),
1488 serde_json::Value::Object(map) => ValueTree::Object(
1489 map.iter()
1490 .map(|(k, v)| (k.clone(), json_to_tree(v)))
1491 .collect(),
1492 ),
1493 }
1494}
1495
1496async fn find_child_by_browse_name(
1497 session: &Session,
1498 parent: &NodeId,
1499 target: &QualifiedName,
1500) -> Result<Option<NodeId>> {
1501 let desc = browse_hierarchical(parent.clone());
1502 let mut results = session
1503 .browse(&[desc], 0, None)
1504 .await
1505 .map_err(|s| anyhow!("browse failed: {s}"))?;
1506 let refs = results.pop().and_then(|r| r.references).unwrap_or_default();
1507 for r in refs {
1508 if is_excluded_tree_reference(&r.reference_type_id) {
1509 continue;
1510 }
1511 if r.browse_name.namespace_index == target.namespace_index
1512 && r.browse_name.name.as_ref() == target.name.as_ref()
1513 {
1514 return Ok(Some(r.node_id.node_id));
1515 }
1516 }
1517 Ok(None)
1518}
1519
1520fn parse_qualified_name(segment: &str) -> QualifiedName {
1525 let body = segment.strip_prefix("ns=").unwrap_or(segment);
1526 if let Some((head, rest)) = body.split_once(':')
1527 && let Ok(ns) = head.parse::<u16>()
1528 {
1529 return QualifiedName::new(ns, rest);
1530 }
1531 QualifiedName::new(0, segment)
1532}
1533
1534async fn register_dynamic_type_loader(session: &Session) -> Result<()> {
1535 let type_tree = DataTypeTreeBuilder::new(|_| true)
1536 .build(session)
1537 .await
1538 .map_err(|e| anyhow!("DataTypeTreeBuilder failed: {e}"))?;
1539 let loader: Arc<dyn TypeLoader> = Arc::new(DynamicTypeLoader::new(Arc::new(type_tree)));
1540 session.add_type_loader(loader);
1541 Ok(())
1542}