From 53da4a6f04962e097d57fa0f320d4b5c59310d22 Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 1 Jan 2025 00:26:26 +0100 Subject: [PATCH] add example client to read custom structs and enums from server --- Cargo.lock | 10 + opcua-types/src/impls.rs | 21 +- samples/custom-structures-client/Cargo.toml | 16 ++ samples/custom-structures-client/README.md | 7 + samples/custom-structures-client/src/main.rs | 231 +++++++++++++++++++ samples/demo-server/README.md | 3 + 6 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 samples/custom-structures-client/Cargo.toml create mode 100644 samples/custom-structures-client/README.md create mode 100644 samples/custom-structures-client/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 0556d0633..9c9c5299e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1364,6 +1364,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "opcua-structure-client" +version = "0.13.0" +dependencies = [ + "log", + "opcua", + "pico-args", + "tokio", +] + [[package]] name = "opcua-types" version = "0.13.0" diff --git a/opcua-types/src/impls.rs b/opcua-types/src/impls.rs index 6f330178a..d40bf3fec 100644 --- a/opcua-types/src/impls.rs +++ b/opcua-types/src/impls.rs @@ -18,8 +18,8 @@ use crate::{ AnonymousIdentityToken, ApplicationDescription, CallMethodRequest, DataTypeId, EndpointDescription, Error, ExpandedNodeId, HistoryUpdateType, IdentityCriteriaType, MessageSecurityMode, MonitoredItemCreateRequest, MonitoringMode, MonitoringParameters, - NumericRange, ObjectId, ReadValueId, ServiceCounterDataType, ServiceFault, SignatureData, - UserNameIdentityToken, UserTokenPolicy, UserTokenType, + NumericRange, ObjectId, ReadValueId, ReferenceTypeId, RelativePath, ServiceCounterDataType, + ServiceFault, SignatureData, UserNameIdentityToken, UserTokenPolicy, UserTokenType, }; use super::PerformUpdateType; @@ -414,3 +414,20 @@ impl Default for IdentityCriteriaType { Self::Anonymous } } + +impl From<&[QualifiedName]> for RelativePath { + fn from(value: &[QualifiedName]) -> Self { + let elements = value + .iter() + .map(|qn| super::relative_path_element::RelativePathElement { + reference_type_id: ReferenceTypeId::HierarchicalReferences.into(), + is_inverse: false, + include_subtypes: true, + target_name: qn.clone(), + }) + .collect(); + Self { + elements: Some(elements), + } + } +} diff --git a/samples/custom-structures-client/Cargo.toml b/samples/custom-structures-client/Cargo.toml new file mode 100644 index 000000000..f3e087840 --- /dev/null +++ b/samples/custom-structures-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "opcua-structure-client" +version = "0.13.0" # OPCUARustVersion +authors = ["Rust-OpcUa contributors"] +edition = "2021" + +[dependencies] +pico-args = "0.5" +tokio = { version = "1.36.0", features = ["full"] } +log = { workspace = true } + +[dependencies.opcua] +path = "../../lib" +version = "0.13.0" # OPCUARustVersion +features = ["client", "console-logging"] +default-features = false diff --git a/samples/custom-structures-client/README.md b/samples/custom-structures-client/README.md new file mode 100644 index 000000000..45e56bbf9 --- /dev/null +++ b/samples/custom-structures-client/README.md @@ -0,0 +1,7 @@ +To run this sample: + +1. Launch either the `samples/demo-server`. That servers exposes custom enums and variables +2. Run as `cargo run` + +The client connects to the server and read a variable. + diff --git a/samples/custom-structures-client/src/main.rs b/samples/custom-structures-client/src/main.rs new file mode 100644 index 000000000..c1f24d981 --- /dev/null +++ b/samples/custom-structures-client/src/main.rs @@ -0,0 +1,231 @@ +// OPCUA for Rust +// SPDX-License-Identifier: MPL-2.0 +// Copyright (C) 2017-2024 Adam Lock + +//! This simple OPC UA client will do the following: +//! +//! 1. Create a client configuration +//! 2. Connect to an endpoint specified by the url with security None +//! 3. Subscribe to values and loop forever printing out their values +use std::sync::Arc; + +use opcua::{ + client::{custom_types::DataTypeTreeBuilder, ClientBuilder, IdentityToken, Session}, + crypto::SecurityPolicy, + types::{ + custom::{DynamicStructure, DynamicTypeLoader}, + BrowsePath, ExpandedNodeId, MessageSecurityMode, NodeId, ObjectId, ReadValueId, StatusCode, + TimestampsToReturn, TypeLoader, UserTokenPolicy, VariableId, Variant, + }, +}; + +const NAMESPACE_URI: &str = "urn:DemoServer"; + +struct Args { + help: bool, + url: String, +} + +impl Args { + pub fn parse_args() -> Result> { + let mut args = pico_args::Arguments::from_env(); + Ok(Args { + help: args.contains(["-h", "--help"]), + url: args + .opt_value_from_str("--url")? + .unwrap_or_else(|| String::from(DEFAULT_URL)), + }) + } + + pub fn usage() { + println!( + r#"Simple Client +Usage: + -h, --help Show help + --url [url] Url to connect to (default: {})"#, + DEFAULT_URL + ); + } +} + +const DEFAULT_URL: &str = "opc.tcp://localhost:4855"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Read command line arguments + let args = Args::parse_args()?; + if args.help { + Args::usage(); + return Ok(()); + } + // Optional - enable OPC UA logging + opcua::console_logging::init(); + + // Make the client configuration + let mut client = ClientBuilder::new() + .application_name("Simple Client") + .application_uri("urn:SimpleClient") + .product_uri("urn:SimpleClient") + .trust_server_certs(true) + .create_sample_keypair(true) + .session_retry_limit(3) + .client() + .unwrap(); + + let (session, event_loop) = client + .connect_to_matching_endpoint( + ( + args.url.as_ref(), + SecurityPolicy::None.to_str(), + MessageSecurityMode::None, + UserTokenPolicy::anonymous(), + ), + IdentityToken::Anonymous, + ) + .await + .unwrap(); + let handle = event_loop.spawn(); + session.wait_for_connection().await; + + let ns = get_namespace_idx(&session, NAMESPACE_URI).await?; + read_structure_var(session, ns).await?; + + //TODO close session + //handle.await.unwrap(); + Ok(()) +} + +async fn read_structure_var(session: Arc, ns: u16) -> Result<(), StatusCode> { + let type_tree = DataTypeTreeBuilder::new(|f| f.namespace <= ns) + .build(&session) + .await + .unwrap(); + + let typ = type_tree + .get_struct_type(&NodeId::new(ns, 3325)) + .unwrap() + .clone(); + dbg!(&typ); + let type_tree = Arc::new(type_tree); + + let loader = Arc::new(DynamicTypeLoader::new(type_tree.clone())) as Arc; + + session.add_type_loader(loader.clone()); + + let res = session + .translate_browse_paths_to_node_ids(&[BrowsePath { + starting_node: ObjectId::ObjectsFolder.into(), + relative_path: (&["ErrorData".into()][..]).into(), + }]) + .await?; + dbg!(&res); + let Some(target) = &res[0].targets else { + panic!("translate browse path did not return a NodeId") + }; + let node_id = &target[0].target_id.node_id; + let res = session + .read(&[node_id.into()], TimestampsToReturn::Neither, 0.0) + .await? + .into_iter() + .next() + .unwrap(); + //dbg!(&res); + let Some(Variant::ExtensionObject(val)) = res.value else { + panic!("Unexpected variant type"); + }; + //dbg!(&val); + //let val: DynamicStructure = *val.into_inner_as().unwrap(); + //dbg!(val.values()); + let val: ErrorData = *val.into_inner_as().unwrap(); + dbg!(val); + Ok(()) +} + +async fn get_namespace_array( + session: &Arc, +) -> Result, Box> { + let nodeid: NodeId = VariableId::Server_NamespaceArray.into(); + let result = session + .read( + &[ReadValueId::from(nodeid)], + TimestampsToReturn::Source, + 0.0, + ) + .await?; + if let Some(Variant::Array(array)) = &result[0].value { + let arr = array + .values + .iter() + .map(|v| { + //TODO iterator can handle result itself!!! + if let Variant::String(s) = v { + s.value().clone().unwrap_or(String::new()) + } else { + String::new() + } + }) + .collect(); + return Ok(arr); + } + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Path did not lead to a node {:?}", result), + ))) +} + +async fn get_namespace_idx( + session: &Arc, + url: &str, +) -> Result> { + let array = get_namespace_array(session).await?; + let idx = array.iter().position(|s| s == url).ok_or_else(|| { + Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Namespace {} not found in {:?}", url, array), + )) + })?; + + Ok(idx.try_into().unwrap()) +} + +// the struct and enum code after that line could/should be shared with demo server +// but having it here make the example selv contained + +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + opcua::types::UaEnum, + opcua::types::BinaryEncodable, + opcua::types::BinaryDecodable, +)] +//#[cfg_attr( +//feature = "json", +//derive(opcua::types::JsonEncodable, opcua::types::JsonDecodable) +//)] +//#[cfg_attr(feature = "xml", derive(opcua::types::FromXml))] +#[derive(Default)] +#[repr(i32)] +pub enum AxisState { + #[default] + Disabled = 1i32, + Enabled = 2i32, + Idle = 3i32, + MoveAbs = 4i32, + Error = 5i32, +} + +#[derive(Debug, Clone, PartialEq, opcua::types::BinaryEncodable, opcua::types::BinaryDecodable)] +//#[cfg_attr( +//feature = "json", +//derive(opcua::types::JsonEncodable, opcua::types::JsonDecodable) +//)] +//#[cfg_attr(feature = "xml", derive(opcua::types::FromXml))] +#[derive(Default)] +pub struct ErrorData { + message: opcua::types::UAString, + error_id: u32, + last_state: AxisState, +} diff --git a/samples/demo-server/README.md b/samples/demo-server/README.md index f317a6023..53ca3627b 100644 --- a/samples/demo-server/README.md +++ b/samples/demo-server/README.md @@ -3,6 +3,9 @@ Use `simple-server` as reference for a very simple OPC UA server. Use `demo-server` (this project) for a more full-featured server that demonstrates the following. - Exposes static and dynamically changing variables + +* Expose custom structure and enumeration + - Variables of every supported data type including arrays - Events - Http access to diagnostics and other info