diff --git a/samples/demo-server/Cargo.toml b/samples/demo-server/Cargo.toml index e9a1ea32..38319db2 100644 --- a/samples/demo-server/Cargo.toml +++ b/samples/demo-server/Cargo.toml @@ -16,3 +16,8 @@ tokio-util = { workspace = true } [dependencies.opcua] path = "../../lib" features = ["all"] + +[features] +default = ["json", "xml"] +json = ["opcua/json"] +xml = ["opcua/xml"] diff --git a/samples/demo-server/README.md b/samples/demo-server/README.md index 1c872a74..f317a602 100644 --- a/samples/demo-server/README.md +++ b/samples/demo-server/README.md @@ -2,15 +2,15 @@ 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 -* Variables of every supported data type including arrays -* Events -* Http access to diagnostics and other info -* More sophisticated logging and data capture -* Be used for testing / verification purposes +- Exposes static and dynamically changing variables +- Variables of every supported data type including arrays +- Events +- Http access to diagnostics and other info +- More sophisticated logging and data capture +- Be used for testing / verification purposes The demo-server enables the `http` feature in `opcua-server` so it can display metrics -from `http://localhost:8585`, however you must start it from the `demo-server` directory so it can find its html +from `http://localhost:8585`, however you must start it from the `demo-server` directory so it can find its html and other resources. ``` @@ -20,24 +20,24 @@ cargo run ## Testing configuration -If you are using the demo server for testing a client you must do a few things depending on what you're testing +If you are using the demo server for testing a client you must do a few things depending on what you're testing against. 1. Copy `sample.server.test.conf` to `../server.test.conf`. The `demo-server` will load this file -if it exists. + if it exists. 2. Edit `../server.test.conf` 3. Set `tcp_config.host` and `discovery_urls` to the IP address of the server host. Do not set it to localhost -5. Set `create_sample_keypair` to false -6. Generate a PKI keypair that is acceptable to your test environment and matches the IP address you set in the config. Copy - this to `pki/own/cert.der` and `pki/private/private.pem`. +4. Set `create_sample_keypair` to false +5. Generate a PKI keypair that is acceptable to your test environment and matches the IP address you set in the config. Copy + this to `pki/own/cert.der` and `pki/private/private.pem`. ### Troubleshooting -* It is best to try opening Project settings in test harness and browsing to server first to ensure trust is possible, and to troubleshoot any basic connection issues. -* Check logs if certs are rejected. -* If you get `BadCertificateTimeInvalid` returned to the test harness, try setting `check_time` +- It is best to try opening Project settings in test harness and browsing to server first to ensure trust is possible, and to troubleshoot any basic connection issues. +- Check logs if certs are rejected. +- If you get `BadCertificateTimeInvalid` returned to the test harness, try setting `check_time` to `false` in the `server.test.conf`. For some reason test harness uses certs which can be out of date. -* If the network is IPv6, use `127.0.0.1` instead of the machine name or `localhost` +- If the network is IPv6, use `127.0.0.1` instead of the machine name or `localhost` ## Run using Docker diff --git a/samples/demo-server/src/customs.rs b/samples/demo-server/src/customs.rs new file mode 100644 index 00000000..2f8b998a --- /dev/null +++ b/samples/demo-server/src/customs.rs @@ -0,0 +1,195 @@ +use std::sync::Arc; + +use opcua::{ + nodes::{DataTypeBuilder, ObjectBuilder, ReferenceDirection, VariableBuilder}, + server::node_manager::memory::SimpleNodeManager, + types::{ + DataTypeDefinition, DataTypeId, EnumDefinition, EnumField, ExpandedNodeId, ExtensionObject, + NodeId, ObjectId, ObjectTypeId, ReferenceTypeId, StructureDefinition, StructureField, + StructureType, + }, +}; + +use crate::NAMESPACE_URI; + +const STRUCT_ENC_TYPE_ID: u32 = 3324; +const STRUCT_DATA_TYPE_ID: u32 = 3325; +const ENUM_DATA_TYPE_ID: u32 = 3326; + +pub fn add_custom_types(nm: Arc, ns: u16) { + let enum_id = add_enum_data_type(&nm, ns); + let struct_id = add_custom_data_type(&nm, ns, &enum_id); + + let addr = nm.address_space(); + let mut addr = addr.write(); + let error_node = NodeId::next_numeric(ns); + let error_data = ErrorData::new("No Error", 98, AxisState::Idle); + VariableBuilder::new(&error_node, "ErrorData", "ErrorData") + .organized_by(ObjectId::ObjectsFolder) + .data_type(struct_id.clone()) + .value(ExtensionObject::new(error_data)) + .insert(&mut *addr); +} + +fn enum_field(name: &str, value: i64) -> EnumField { + EnumField { + name: name.into(), + description: name.into(), + display_name: name.into(), + value, + } +} + +fn add_enum_data_type(nm: &Arc, ns: u16) -> NodeId { + let mut addr = nm.address_space().write(); + + let id = NodeId::new(ns, ENUM_DATA_TYPE_ID); + + let type_def = DataTypeDefinition::Enum(EnumDefinition { + fields: Some(vec![ + enum_field("Disabled", 1), + enum_field("Enabled", 2), + enum_field("Idle", 3), + enum_field("MoveAbs", 4), + enum_field("Error", 5), + ]), + }); + DataTypeBuilder::new(&id, "AxisState", "AxisState") + .subtype_of(DataTypeId::Enumeration) + .data_type_definition(type_def) + .insert(&mut *addr); + + id +} + +fn add_encoding(nm: &SimpleNodeManager, ns: u16, struct_id: &NodeId) -> NodeId { + let mut addr = nm.address_space().write(); + let id = NodeId::new(ns, STRUCT_ENC_TYPE_ID); + ObjectBuilder::new(&id, "Default Binary", "Default Binary") + .reference( + struct_id, + ReferenceTypeId::HasEncoding, + ReferenceDirection::Inverse, + ) + .has_type_definition(ObjectTypeId::DataTypeEncodingType) + .insert(&mut *addr); + id +} + +fn add_custom_data_type(nm: &SimpleNodeManager, ns: u16, e_state_id: &NodeId) -> NodeId { + let struct_id = NodeId::new(ns, STRUCT_DATA_TYPE_ID); + let enc_id = add_encoding(nm, ns, &struct_id); + + let type_def = DataTypeDefinition::Structure(StructureDefinition { + default_encoding_id: NodeId::new(ns, STRUCT_ENC_TYPE_ID), + base_data_type: DataTypeId::Structure.into(), + structure_type: StructureType::Structure, + fields: Some(vec![ + StructureField { + name: "sErrorMessage".into(), + data_type: DataTypeId::String.into(), + value_rank: -1, + ..Default::default() + }, + StructureField { + name: "nErrorID".into(), + data_type: DataTypeId::UInt32.into(), + value_rank: -1, + ..Default::default() + }, + StructureField { + name: "eLastState".into(), + data_type: e_state_id.clone(), + value_rank: -1, + ..Default::default() + }, + ]), + }); + let mut addr = nm.address_space().write(); + DataTypeBuilder::new(&struct_id, "ErrorData", "ErrorData") + .subtype_of(DataTypeId::Structure) + .data_type_definition(type_def) + .reference( + enc_id, + ReferenceTypeId::HasEncoding, + ReferenceDirection::Forward, + ) + .insert(&mut *addr); + + struct_id +} + +#[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, +} + +impl ErrorData { + pub fn new(msg: &str, error_id: u32, last_state: AxisState) -> Self { + Self { + message: msg.into(), + error_id, + last_state, + } + } +} + +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, + } + } +} diff --git a/samples/demo-server/src/main.rs b/samples/demo-server/src/main.rs index 3c00ad7e..09bff542 100644 --- a/samples/demo-server/src/main.rs +++ b/samples/demo-server/src/main.rs @@ -26,10 +26,13 @@ use opcua::server::{ }; mod control; +mod customs; mod machine; mod methods; mod scalar; +const NAMESPACE_URI: &str = "urn:DemoServer"; + struct Args { help: bool, raise_events: bool, @@ -130,10 +133,13 @@ async fn main() { .node_managers() .get_of_type::() .unwrap(); - let ns = handle.get_namespace_index("urn:DemoServer").unwrap(); + let ns = handle.get_namespace_index(NAMESPACE_URI).unwrap(); let token = handle.token(); + // Define some custom types + customs::add_custom_types(node_manager.clone(), ns); + // Add some objects representing machinery machine::add_machinery( ns,