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