From d29c249b70f42f227d6867d6feb6813d9bd66bec Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 8 Jan 2025 09:15:30 +0100 Subject: [PATCH] add example client to read custom structs and enums from server --- Cargo.lock | 10 ++ samples/custom-structures-client/Cargo.toml | 16 ++ samples/custom-structures-client/README.md | 6 + samples/custom-structures-client/src/main.rs | 178 +++++++++++++++++++ samples/demo-server/README.md | 1 + 5 files changed, 211 insertions(+) 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 34b1afb2..2af0cf29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1365,6 +1365,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/samples/custom-structures-client/Cargo.toml b/samples/custom-structures-client/Cargo.toml new file mode 100644 index 00000000..f3e08784 --- /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 00000000..c92390a1 --- /dev/null +++ b/samples/custom-structures-client/README.md @@ -0,0 +1,6 @@ +To run this sample: + +1. Launch `samples/demo-server`. That servers exposes custom enums and variables +2. Run as `cargo run` + +The client connects to the server, read a variable and disconnects diff --git a/samples/custom-structures-client/src/main.rs b/samples/custom-structures-client/src/main.rs new file mode 100644 index 00000000..0397b2f7 --- /dev/null +++ b/samples/custom-structures-client/src/main.rs @@ -0,0 +1,178 @@ +// 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. Read a variable on server with data type being a custom structure +use std::sync::Arc; + +use opcua::{ + client::{custom_types::DataTypeTreeBuilder, ClientBuilder, IdentityToken, Session}, + crypto::SecurityPolicy, + types::{ + custom::{DynamicStructure, DynamicTypeLoader}, + BrowsePath, MessageSecurityMode, ObjectId, StatusCode, TimestampsToReturn, TypeLoader, + UserTokenPolicy, 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 = session + .get_namespace_index(NAMESPACE_URI) + .await + .map_err(|e| format!("Error getting namespace index {:?}", e))?; + read_structure_var(&session, ns).await?; + + session.disconnect().await?; + 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 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?; + 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 dv = session + .read(&[node_id.into()], TimestampsToReturn::Neither, 0.0) + .await? + .into_iter() + .next() + .unwrap(); + dbg!(&dv); + let Some(Variant::ExtensionObject(val)) = dv.value else { + panic!("Unexpected variant type"); + }; + let val: DynamicStructure = *val.into_inner_as().unwrap(); + + dbg!(&val.get_field(0)); + dbg!(&val.get_field(1)); + dbg!(&val.get_field(2)); + Ok(()) +} + +// 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 f317a602..2f413a32 100644 --- a/samples/demo-server/README.md +++ b/samples/demo-server/README.md @@ -3,6 +3,7 @@ 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