Skip to content

Commit

Permalink
add example client to read custom structs and enums from server
Browse files Browse the repository at this point in the history
  • Loading branch information
Olivier committed Jan 1, 2025
1 parent 263146b commit 53da4a6
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions opcua-types/src/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
}
}
}
16 changes: 16 additions & 0 deletions samples/custom-structures-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions samples/custom-structures-client/README.md
Original file line number Diff line number Diff line change
@@ -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.

231 changes: 231 additions & 0 deletions samples/custom-structures-client/src/main.rs
Original file line number Diff line number Diff line change
@@ -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},

Check failure on line 16 in samples/custom-structures-client/src/main.rs

View workflow job for this annotation

GitHub Actions / build-linux (stable)

unused imports: `DynamicStructure` and `ExpandedNodeId`

Check failure on line 16 in samples/custom-structures-client/src/main.rs

View workflow job for this annotation

GitHub Actions / build-linux (beta)

unused imports: `DynamicStructure` and `ExpandedNodeId`
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<Args, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
// 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();

Check failure on line 87 in samples/custom-structures-client/src/main.rs

View workflow job for this annotation

GitHub Actions / build-linux (stable)

unused variable: `handle`

Check failure on line 87 in samples/custom-structures-client/src/main.rs

View workflow job for this annotation

GitHub Actions / build-linux (beta)

unused variable: `handle`
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<Session>, 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<dyn TypeLoader>;

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<Session>,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
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<Session>,
url: &str,
) -> Result<u16, Box<dyn std::error::Error>> {
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,
}
3 changes: 3 additions & 0 deletions samples/demo-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 53da4a6

Please sign in to comment.