Skip to content

Commit

Permalink
split client in two
Browse files Browse the repository at this point in the history
  • Loading branch information
Olivier committed Jan 12, 2025
1 parent 5b2f57e commit 4a1e02f
Show file tree
Hide file tree
Showing 8 changed files with 857 additions and 547 deletions.
842 changes: 491 additions & 351 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion async-opcua-types/src/relative_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ impl From<&[QualifiedName]> for RelativePath {
}
}
}

// Cannot use
//impl<T: AsRef<str>> TryFrom<T> for RelativePath {
// for some strange reasons so implementing all thee manually here
//
impl TryFrom<&str> for RelativePath {
type Error = OpcUaError;

Expand All @@ -110,6 +113,22 @@ impl TryFrom<&str> for RelativePath {
}
}

impl TryFrom<&String> for RelativePath {
type Error = OpcUaError;

fn try_from(value: &String) -> Result<Self, Self::Error> {
RelativePath::from_str(value, &RelativePathElement::default_node_resolver)
}
}

impl TryFrom<String> for RelativePath {
type Error = OpcUaError;

fn try_from(value: String) -> Result<Self, Self::Error> {
RelativePath::from_str(&value, &RelativePathElement::default_node_resolver)
}
}

impl<'a> From<&'a RelativePathElement> for String {
fn from(element: &'a RelativePathElement) -> String {
let mut result = element
Expand Down
13 changes: 9 additions & 4 deletions samples/custom-structures-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opcua-structure-client"
version = "0.13.0" # OPCUARustVersion
version = "0.13.0" # OPCUARustVersion
authors = ["Rust-OpcUa contributors"]
edition = "2021"

Expand All @@ -9,8 +9,13 @@ pico-args = "0.5"
tokio = { version = "1.36.0", features = ["full"] }
log = { workspace = true }

[dependencies.opcua]
path = "../../lib"
version = "0.13.0" # OPCUARustVersion
[dependencies.async-opcua]
path = "../../async-opcua"
version = "0.13.0" # OPCUARustVersion
features = ["client", "console-logging"]
default-features = false

[features]
default = ["json", "xml"]
json = ["async-opcua/json"]
xml = ["async-opcua/xml"]
71 changes: 71 additions & 0 deletions samples/custom-structures-client/src/bin/dynamic_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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, Session},
types::{
custom::{DynamicStructure, DynamicTypeLoader},
errors::OpcUaError,
BrowsePath, ObjectId, TimestampsToReturn, TypeLoader, Variant,
},
};
use opcua_structure_client::client_connect;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (session, handle, ns) = client_connect().await?;
read_structure_var(&session, ns).await?;

session.disconnect().await?;
handle.await.unwrap();
Ok(())
}

async fn read_structure_var(session: &Arc<Session>, ns: u16) -> Result<(), OpcUaError> {
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<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: format!("/{}:ErrorData", ns).try_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(())
}
170 changes: 170 additions & 0 deletions samples/custom-structures-client/src/bin/native_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::sync::Arc;

use opcua::{
client::Session,
types::{errors::OpcUaError, BrowsePath, ExpandedNodeId, NodeId, ObjectId, TimestampsToReturn},
};
use opcua_structure_client::{client_connect, NAMESPACE_URI};

const STRUCT_ENC_TYPE_ID: u32 = 3324;
const STRUCT_DATA_TYPE_ID: u32 = 3325;
//const ENUM_DATA_TYPE_ID: u32 = 3326;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (session, handle, ns) = client_connect().await?;
read_structure_var(&session, ns).await?;

session.disconnect().await?;
handle.await.unwrap();
Ok(())
}

async fn read_structure_var(session: &Arc<Session>, ns: u16) -> Result<(), OpcUaError> {
session.add_type_loader(Arc::new(CustomTypeLoader));

let res = session
.translate_browse_paths_to_node_ids(&[BrowsePath {
starting_node: ObjectId::ObjectsFolder.into(),
relative_path: format!("/{}:ErrorData", ns).try_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);
Ok(())
}

// The struct and enum code after this line could/should be shared with demo server,
// but having it here makes the example self-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,
}

static TYPES: std::sync::LazyLock<opcua::types::TypeLoaderInstance> =
std::sync::LazyLock::new(|| {
let mut inst = opcua::types::TypeLoaderInstance::new();
{
inst.add_binary_type(
STRUCT_DATA_TYPE_ID,
STRUCT_ENC_TYPE_ID,
opcua::types::binary_decode_to_enc::<ErrorData>,
);

inst
}
});

#[derive(Debug, Clone, Copy)]
pub struct CustomTypeLoader;
impl opcua::types::TypeLoader for CustomTypeLoader {
fn load_from_binary(
&self,
node_id: &opcua::types::NodeId,
stream: &mut dyn std::io::Read,
ctx: &opcua::types::Context<'_>,
) -> Option<opcua::types::EncodingResult<Box<dyn opcua::types::DynEncodable>>> {
let idx = ctx.namespaces().get_index(NAMESPACE_URI)?;
if idx != node_id.namespace {
return None;
}
let Some(num_id) = node_id.as_u32() else {
return Some(Err(opcua::types::Error::decoding(
"Unsupported encoding ID. Only numeric encoding IDs are currently supported",
)));
};
TYPES.decode_binary(num_id, stream, ctx)
}
#[cfg(feature = "xml")]
fn load_from_xml(
&self,
_node_id: &opcua::types::NodeId,
_stream: &opcua::types::xml::XmlElement,
_ctx: &opcua::types::xml::XmlContext<'_>,
) -> Option<Result<Box<dyn opcua::types::DynEncodable>, opcua::types::xml::FromXmlError>> {
todo!()
}
#[cfg(feature = "json")]
fn load_from_json(
&self,
_node_id: &opcua::types::NodeId,
_stream: &mut opcua::types::json::JsonStreamReader<&mut dyn std::io::Read>,
_ctx: &opcua::types::Context<'_>,
) -> Option<opcua::types::EncodingResult<Box<dyn opcua::types::DynEncodable>>> {
todo!()
}
fn priority(&self) -> opcua::types::TypeLoaderPriority {
opcua::types::TypeLoaderPriority::Generated
}
}

impl opcua::types::ExpandedMessageInfo for ErrorData {
fn full_type_id(&self) -> opcua::types::ExpandedNodeId {
ExpandedNodeId {
node_id: NodeId::new(0, STRUCT_ENC_TYPE_ID),
namespace_uri: NAMESPACE_URI.into(),
server_index: 0,
}
}

fn full_json_type_id(&self) -> opcua::types::ExpandedNodeId {
todo!()
}

fn full_xml_type_id(&self) -> opcua::types::ExpandedNodeId {
todo!()
}

fn full_data_type_id(&self) -> opcua::types::ExpandedNodeId {
ExpandedNodeId {
node_id: NodeId::new(0, STRUCT_DATA_TYPE_ID),
namespace_uri: NAMESPACE_URI.into(),
server_index: 0,
}
}
}
85 changes: 85 additions & 0 deletions samples/custom-structures-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use std::sync::Arc;

use opcua::{
client::{ClientBuilder, IdentityToken, Session},
crypto::SecurityPolicy,
types::{MessageSecurityMode, StatusCode, UserTokenPolicy},
};
use tokio::task::JoinHandle;

pub 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
);
}
}

pub const DEFAULT_URL: &str = "opc.tcp://localhost:4855";

pub async fn client_connect(
) -> Result<(Arc<Session>, JoinHandle<StatusCode>, u16), Box<dyn std::error::Error>> {
// Read command line arguments
let args = Args::parse_args()?;
if args.help {
Args::usage();
return Err("Help requested, exiting".into());
}
// 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))?;

Ok((session, handle, ns))
}
Loading

0 comments on commit 4a1e02f

Please sign in to comment.