-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Olivier
committed
Jan 12, 2025
1 parent
5b2f57e
commit 4a1e02f
Showing
8 changed files
with
857 additions
and
547 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
samples/custom-structures-client/src/bin/dynamic_client.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
170
samples/custom-structures-client/src/bin/native_client.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
Oops, something went wrong.