Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom structures client #15

Merged
merged 4 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 223 additions & 73 deletions Cargo.lock

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions async-opcua-server/src/address_space/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::node_manager::{ParsedReadValueId, ParsedWriteValue, RequestContext, S
use log::debug;
use opcua_nodes::TypeTree;
use opcua_types::{
AttributeId, DataEncoding, DataTypeId, DataValue, NumericRange, StatusCode, TimestampsToReturn,
Variant, WriteMask,
AttributeId, DataEncoding, DataTypeId, DataValue, DateTime, NumericRange, StatusCode,
TimestampsToReturn, Variant, WriteMask,
};

use super::{AccessLevel, AddressSpace, HasNodeId, NodeType, Variable};
Expand Down Expand Up @@ -272,6 +272,29 @@ pub fn read_node_value(
result_value
}

/// Invoke `Write` for the given `node_to_write` on `node`.
pub fn write_node_value(
node: &mut NodeType,
node_to_write: &ParsedWriteValue,
) -> Result<(), StatusCode> {
let now = DateTime::now();
if node_to_write.attribute_id == AttributeId::Value {
if let NodeType::Variable(variable) = node {
return variable.set_value_range(
node_to_write.value.value.clone().unwrap_or_default(),
&node_to_write.index_range,
node_to_write.value.status.unwrap_or_default(),
&now,
&node_to_write.value.source_timestamp.unwrap_or(now),
);
}
}
node.as_mut_node().set_attribute(
node_to_write.attribute_id,
node_to_write.value.value.clone().unwrap_or_default(),
)
}

/// Add the given list of namespaces to the type tree in `context` and
/// `address_space`.
pub fn add_namespaces(
Expand Down
46 changes: 33 additions & 13 deletions async-opcua-server/src/node_manager/memory/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ use std::{collections::HashMap, sync::Arc, time::Duration};

use async_trait::async_trait;
use opcua_core::{trace_read_lock, trace_write_lock};
use opcua_nodes::NodeSetImport;
use opcua_nodes::{HasNodeId, NodeSetImport};

use crate::{
address_space::{read_node_value, AddressSpace, NodeBase, NodeType},
address_space::{read_node_value, write_node_value, AddressSpace},
node_manager::{
DefaultTypeTree, MethodCall, MonitoredItemRef, MonitoredItemUpdateRef, NodeManagerBuilder,
NodeManagersRef, ParsedReadValueId, RequestContext, ServerContext, SyncSampler, WriteNode,
Expand All @@ -14,8 +14,8 @@ use crate::{
};
use opcua_core::sync::RwLock;
use opcua_types::{
AttributeId, DataValue, MonitoringMode, NodeId, NumericRange, StatusCode, TimestampsToReturn,
Variant,
AttributeId, DataValue, MonitoringMode, NodeClass, NodeId, NumericRange, StatusCode,
TimestampsToReturn, Variant,
};

use super::{
Expand Down Expand Up @@ -364,18 +364,38 @@ impl SimpleNodeManagerImpl {
}
};

let (NodeType::Variable(var), AttributeId::Value) = (node, write.value().attribute_id)
else {
write.set_status(StatusCode::BadNotWritable);
return;
};

let Some(cb) = cbs.get(var.node_id()) else {
if node.node_class() != NodeClass::Variable
|| write.value().attribute_id != AttributeId::Value
{
write.set_status(StatusCode::BadNotWritable);
return;
};
}

write.set_status(cb(write.value().value.clone(), &write.value().index_range));
if let Some(cb) = cbs.get(node.as_node().node_id()) {
// If there is a callback registered, call that.
write.set_status(cb(write.value().value.clone(), &write.value().index_range));
} else if write.value().value.value.is_some() {
// If not, write the value to the node hierarchy.
match write_node_value(node, write.value()) {
Ok(_) => write.set_status(StatusCode::Good),
Err(e) => write.set_status(e),
}
} else {
// If no value is passed return an error.
write.set_status(StatusCode::BadNothingToDo);
}
if write.status().is_good() {
if let Some(val) = node.as_mut_node().get_attribute(
TimestampsToReturn::Both,
write.value().attribute_id,
&NumericRange::None,
&opcua_types::DataEncoding::Binary,
) {
context.subscriptions.notify_data_change(
[(val, node.node_id(), write.value().attribute_id)].into_iter(),
);
}
}
}

/// Add a callback called on `Write` for the node given by `id`.
Expand Down
21 changes: 21 additions & 0 deletions samples/custom-structures-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[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.async-opcua]
path = "../../async-opcua"
version = "0.14.0" # OPCUARustVersion
features = ["client", "console-logging"]
default-features = false

[features]
default = ["json", "xml"]
json = ["async-opcua/json"]
xml = ["async-opcua/xml"]
6 changes: 6 additions & 0 deletions samples/custom-structures-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
To run this sample:

1. Launch `samples/demo-server` as that server exposes custom enums and variables
2. Run as `cargo run`

The client connects to the server, reads a variable and disconnects.
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!("/{0}:ErrorFolder/{0}: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(())
}
147 changes: 147 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,147 @@
use std::sync::Arc;

use opcua::{
client::Session,
types::{
errors::OpcUaError, ua_encodable, BrowsePath, ExpandedNodeId, ExtensionObject, NodeId,
ObjectId, StaticTypeLoader, TimestampsToReturn, Variant, WriteValue,
},
};
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> {
// Register our loader that will parse UA struct into our Rust struc
session.add_type_loader(Arc::new(CustomTypeLoader));

//get node_id using browsepath
let res = session
.translate_browse_paths_to_node_ids(&[BrowsePath {
starting_node: ObjectId::ObjectsFolder.into(),
relative_path: format!("/{0}:ErrorFolder/{0}: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;

// value of node variable
let dv = session
.read(&[node_id.into()], TimestampsToReturn::Neither, 0.0)
.await?
.into_iter()
.next()
.unwrap();

if let Some(Variant::ExtensionObject(obj)) = dv.value {
dbg!("Native rust object: ", &obj.body.unwrap());
}

// Now show how to write a value from client
let new = ErrorData {
message: "New message".into(),
error_id: 100,
last_state: AxisState::Error,
};

let res = session
.write(&[WriteValue::value_attr(
node_id.clone(),
Variant::ExtensionObject(ExtensionObject {
body: Some(Box::new(new)),
}),
)])
.await?;
dbg!(res);
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)]
#[ua_encodable]
#[repr(i32)]
pub enum AxisState {
#[opcua(default)]
Disabled = 1i32,
Enabled = 2i32,
Idle = 3i32,
MoveAbs = 4i32,
Error = 5i32,
}

#[derive(Debug, Clone, PartialEq, Default)]
#[ua_encodable]
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 StaticTypeLoader for CustomTypeLoader {
fn instance() -> &'static opcua::types::TypeLoaderInstance {
&TYPES
}

fn namespace() -> &'static str {
NAMESPACE_URI
}
}

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,
}
}
}
Loading