Work in progress
This is a small tutorial for using the OPC UA server library. It will assume you are familiar with OPC UA,
Rust and tools such as cargo
.
- A small overview of OPC UA is here.
- Rust OPC UA's compatibility with the standard is described here.
The Rust OPC UA server API supports all of the OPC UA embedded profile services and a few of the standard profile services.
These are implemented for you so generally once you create a server configuration, set up an address space and register some callbacks you are ready to run a server.
- Create or load a configuration that defines the TCP address / port server runs on, the endpoints it supports, user identities etc.
- Create a server from the configuration
- Populate additional nodes into the address space and callbacks / timers that change state.
- Run the server
- Server runs forever, listening for connections.
We're going to start with a blank project.
cargo init --bin test-server
TODO
TODO
use opcua_server::prelude::*;
The server can be configured in a number of ways:
- A
ServerBuilder
is the easiest way to build a server programatically. - A configuration file described in yaml or some other
serde
supported format that you read from.
A ServerBuilder
allows you to programatically construct a Server
.
let server = ServerBuilder::new()
.application_name("Server Name")
.application_uri("urn:server_uri")
.discovery_urls(vec![endpoint_url(port_offset)])
.create_sample_keypair(true)
.pki_dir("./pki-server")
.discovery_server_url(None)
.host_and_port(hostname(), 1234)
.user_token(sample_user_id, ServerUserToken::new_user_pass("sample", "sample1"))
.endpoints(
[
("none", endpoint_path, SecurityPolicy::None, MessageSecurityMode::None, &user_token_ids),
("basic128rsa15_sign", endpoint_path, SecurityPolicy::Basic128Rsa15, MessageSecurityMode::Sign, &user_token_ids),
("basic128rsa15_sign_encrypt", endpoint_path, SecurityPolicy::Basic128Rsa15, MessageSecurityMode::SignAndEncrypt, &user_token_ids),
("basic256_sign", endpoint_path, SecurityPolicy::Basic256, MessageSecurityMode::Sign, &user_token_ids),
("basic256_sign_encrypt", endpoint_path, SecurityPolicy::Basic256, MessageSecurityMode::SignAndEncrypt, &user_token_ids),
("basic256sha256_sign", endpoint_path, SecurityPolicy::Basic256Sha256, MessageSecurityMode::Sign, &user_token_ids),
("basic256sha256_sign_encrypt", endpoint_path, SecurityPolicy::Basic256Sha256, MessageSecurityMode::SignAndEncrypt, &user_token_ids),
].iter().map(|v| {
(v.0.to_string(), ServerEndpoint::from((v.1, v.2, v.3, &v.4[..])))
}).collect())
.server().unwrap();
Reading from file can be done like so.
let mut server = Server::new(ServerConfig::load(&PathBuf::from("../server.conf")).unwrap());
Alternatively, let's say you use a configuration file, but how do you create it when one isn't there? Well your code
logic could test if the file can load, and if it doesn't, could create the default one with a ServerBuilder
.
let server_config_path = "./myserver.conf";
let server_config = if let Ok(server_config) = ServerConfig::load(&PathBuf::from(server_config_path))) {
server_config
}
else {
let server_config = ServerBuilder::new()
.application_name("Server Name")
.application_uri("urn:server_uri")
//... Lines deleted
.config();
server_config.save(server_config_path);
server_config
}
let mut server = Server::new(server_config);
The default TCP config uses an address / port of 127.0.0.1
and 4855
. If you intend for your server
to be remotely accessible then explicitly set the address to the assigned IP address or resolvable hostname for the network
adapter your server will listen on.
Also ensure that your machine has a firewall rule to allow through the port number you use.
The server configuration determines what encryption it uses on its endpoints, and also what user identity tokens it accepts.
The client and server can communicate over an insecure or a secure channel.
- An insecure channel is plaintext and is not encrypted in any way. This might be fine where trust is implicit and controlled between the client and the server, e.g. when they reside on a private network, or even the same device.
- A secure channel. The client presents a certificate to the server, the server presents a certificate to the client. Each must trust the other, at which point the session proceeds over an encrypted channel.
Once the client establishes a session with the server, the next thing it will do is present its identity for activating the session. The identity is the user's credentials which can be anonymous, user / password or X509 identity token.
Your server has an address space that contains the default OPC UA node set. The default node set describes all the standard types, server diagnostics variables and more besides.
To this you may wish to add your own objects and variables. To make this easy, you can create new nodes with a builder, e.g:
let address_space = server.address_space().write().unwrap();
// This is a convenience helper
let folder_id = address_space
.add_folder("Variables", "Variables", &NodeId::objects_folder_id())
.unwrap();
// Build a variable
let node_id = NodeId::new(2, , "MyVar");
VariableBuilder::new(&node_id, "MyVar", "MyVar")
.organized_by(&folder_id)
.value(0u8)
.insert(&mut address_space);
The builder pattern allows you to set each property of your node and common relationships to other nodes before inserting it into the address space.
TODO Clients of servers will typically read values of variables, and may do so from a subscription. A variable can reflect a value from a physical device that your server will update either as it changes, or on a timer, or when a client requests it.
- Add hoc. Your code sets the variable when you deem the value to have changed.
- Getter. The server invokes a getter in your code whenever the value is requested.
- Timer. The server invokes a call back on a timed interval allowing you the chance to update the value. OPC UA for Rust provides a timer mechanism as a convenience
In addition you may also register a setter callback which is called whenever a client attempts to write a value to the variable. Your callback could ignore the change, clamp it to some range or call the physical device with the change.
Running a server is a synchronous action:
// Run the server. This does not ordinarily exit so you must Ctrl+C to terminate
server.run();
If you prefer to make it asynchronous, run it on a separate thread, or use Server::run_server
.
OPC UA for Rust provides an extensive amount of logging at error, warn, info, debug and trace levels. All this is via the standard log facade so choose which logging implementation you want to capture information. See the link for implementations that you can use.
For convenience OPC UA for Rust provides a simple opcua-console-logging
crate that wraps env_logger
and writes out logging information to stdout. To use it, set an RUST_OPCUA_LOG
environment variable (not RUST_LOG
),
otherwise following the documentation in env_logger
. e.g.
export RUST_OPCUA_LOG=debug
In your Cargo.toml
:
[dependencies]
opcua-console-logging = "0.7.0" # Where version == version of OPC UA for Rust
In your main()
:
fn main() {
opcua_console_logging::init();
//...
}
The demo-server
sample demonstrates more sophisticated logging using the log4rs crate.