View Source Elixir Integration
Content
Integration
Both Server and Client modules are implemented as a __using__
macro so that you can put it in any module;
you only need to add the defined callbacks to integrate this library into your project.
Either way, it is always possible to directly use Server and Client modules, as shown in previous tutorials.
Server
For convenience, Server
is a GenServer
wrapper for automating configuration and adding the address space (information model); it also accepts the same options for supervision to configure the child spec and passes them along to GenServer
, for example:
use OpcUA.Server, restart: :transient, shutdown: 10_000
With these instructions, the Server backend will be integrated into your application module. Now you only have to add the callbacks you require.
The basic callback is the init/2
that let you manipulate the OPC UA server by given it's PID (opc_ua_server_pid) with some user data (user_init_state):
# Use the `init` function to configure your server.
def init(user_init_state, opc_ua_server_pid) do
# Do some initial process and start the server at your convenience.
Server.start(opc_ua_server_pid)
# You can set a new state for your app (if required).
user_init_state
end
Note: No callback automatically starts the OPC UA server, so it is recommended to use init\2
because it is the last callback to be executed.
Server Configuration
The first executed optional callback is configuration/1
. It gets and executes the Server configuration and discovery connection parameters as follows:
def configuration(_user_init_state) do
[
config: [
port: 4041,
users: [{"alde103", "secret"}]
]
]
end
In this example, the server will use the port 4041
with a predefined user. The user_init_state
is propagated to this callback too.
Note: The output of these callbacks must be a list (type config_options) as shown in the Server module.
Address Space
The next executed optional callback is address_space/1
. It gets and adds all nodes (namespaces, object nodes, variable nodes, monitored items) to the Server as follows:
def address_space(_user_init_state) do
[
namespace: "Sensor",
object_node: OpcUA.ObjectNode.new(
[
requested_new_node_id: NodeId.new(ns_index: 2, identifier_type: "string", identifier: "R1_TS1_Sensor"),
parent_node_id: NodeId.new(ns_index: 0, identifier_type: "integer", identifier: 85),
reference_type_node_id: NodeId.new(ns_index: 0, identifier_type: "integer", identifier: 35),
browse_name: QualifiedName.new(ns_index: 2, name: "Temperature sensor"),
type_definition: NodeId.new(ns_index: 0, identifier_type: "integer", identifier: 58)
]
),
variable_node: OpcUA.VariableNode.new(
[
requested_new_node_id: NodeId.new(ns_index: 2, identifier_type: "string", identifier: "R1_TS1_Temperature"),
parent_node_id: NodeId.new(ns_index: 2, identifier_type: "string", identifier: "R1_TS1_Sensor"),
reference_type_node_id: NodeId.new(ns_index: 0, identifier_type: "integer", identifier: 47),
browse_name: QualifiedName.new(ns_index: 2, name: "Temperature"),
type_definition: NodeId.new(ns_index: 0, identifier_type: "integer", identifier: 63)
],
write_mask: 0x3FFFFF,
value: {10, 103.0},
access_level: 3
),
monitored_item: OpcUA.MonitoredItem.new(
[
monitored_item: NodeId.new(ns_index: 2, identifier_type: "string", identifier: "R1_TS1_Temperature"),
sampling_time: 1000.0,
subscription_id: 1
]
)
]
end
This example emulates a temperature sensor based on an object node (R1_TS1_Sensor
) with a variable node (R1_TS1_Temperature
) to display the temperature, which is registered as a monitored item. The user_init_state
is propagated to this callback too.
Note: The output of these callbacks must be a list (type address_space_list) as shown in the Server module. The order matters according to the node dependency. In this example, the parent node of the variable node is an object node. If the object node is not defined at the variable node definition time, the server will crash.
Write Events
A runtime callback is handle_write/2
. It handles every Client writing events to any Server node, as follows:
def handle_write(write_event, state) do
# Do something with the write event ({node_id, value} = write_event)
# and your module state (state)
state
end
Examples
The following example shows a module that takes its configuration from the environment:
defmodule MyServer do
use OpcUA.Server
alias OpcUA.{NodeId, Server, QualifiedName}
# Use the `init` function to configure your server.
def init(user_init_state, opc_ua_server_pid) do
# Do some initial process and start the server at your convenience.
Server.start(opc_ua_server_pid)
# You can set a new state for your app.
user_init_state
end
def configuration(_user_init_state), do: Application.get_env(:opex62541, :configuration, [])
def address_space(_user_init_state), do: Application.get_env(:opex62541, :address_space, [])
def handle_write(write_event, %{parent_pid: parent_pid} = state) do
send(parent_pid, write_event)
state
end
end
This code can be executed as
{:ok, my_pid} = MyServer.start_link({self(), 103} = _user_init_state)
More examples can be found in the source code tests.
Client
The Client
module can be initialized manually (as shown in previous tutorials) or by overwriting configuration/1
and monitored_items/1
callbacks to auto-set the configuration and subscription items. It also helps you handle the Client's "subscription" events (monitorItems) by overwriting handle_subscription/2
callback.
Like the Server
module, the Client
module is also based on a GenServer
behavior. Therefore it accepts the same options for supervision to configure the child spec and passes them along to GenServer
; to add the Client
behavior to your application, use the following code:
use OpcUA.Client, restart: :transient, shutdown: 10_000
The basic callback is the init/2
that let you maniplulate the OPC UA client by given its PID (opc_ua_server_pid) with some user data (user_init_state):
# Use the `init` function to configure your client.
def init({parent_pid, 103} = _user_init_state, opc_ua_client_pid) do
# this will be your app state
%{parent_pid: parent_pid, opc_ua_client_pid: opc_ua_client_pid}
end
Client Configuration
The first executed optional callback is configuration/1
, it gets and execute the Client configuration and handles server connection parameters as follows:
def configuration(_user_init_state) do
{:ok, localhost} = :inet.gethostname()
[
config: [
set_config: %{
"requestedSessionTimeout" => 1200000,
"secureChannelLifeTime" => 600000,
"timeout" => 50000
}
],
conn: [
by_username: [
url: "opc.tcp://#{localhost}:4041/",
user: "alde103",
password: "secret"
]
]
]
end
In this example, the client will use a predefined timeout and connection (url, user, etc.) parameters. The user_init_state
is propagated to this callback too.
Note: The output of these callbacks must be a list (type config_options) as shown in the Client module.
Monitored Item
The next optional callback to be executed is monitored_items/1
. It gets and adds all nodes (namespaces, object nodes, variable nodes, monitored items) to the Server, for example:
def monitored_items(_user_init_state) do
[
subscription: 200.0,
monitored_item: OpcUA.MonitoredItem.new(
[
monitored_item: NodeId.new(ns_index: 2, identifier_type: "string", identifier: "R1_TS1_Temperature"),
sampling_time: 100.0,
subscription_id: 1
]
)
]
end
In this example, the client automatically sends a subscription request (with a publishing interval of 200.0 ms) and a monitored item request (R1_TS1_Temperature
), the user_init_state
is propagated to this callback too.
Note: The output of these callbacks must be a list (type address_space_list) as shown in the Client module.
An runtime callback is handle_monitored_data/2
, it handles every Server events triggered by a monitored item data, as follows:
def handle_monitored_data(monitored_item_event, state) do
# Do something with the event ({subscription_id, monitored_id, value} = monitored_item_event)
# and your module state (state)
state
end
Another runtime callback is handle_deleted_monitored_item/2
, it handles the withdrawal events of monitored items from the server, for example:
def handle_deleted_monitored_item(subscription_id, monitored_id, state) do
# Do something with this event ({subscription_id, monitored_id, value} = monitored_item_event)
# and your module state (state)
state
end
Subscription
The library automatically creates subscriptions. However, there are some callbacks to handle unexpected events, as shown below:
def handle_subscription_timeout(subscription_id, state) do
# Do something with this event (subscription_id is an integer)
# and your module state (state)
state
end
The handle_deleted_subscription/2
handles the withdrawal events of subscriptions from the server, for example:
def handle_deleted_subscription(subscription_id, state) do
# Do something with this event (subscription_id is an integer)
# and your module state (state)
state
end
Example
The following example shows a module that takes its configuration from the environment and notifies any client event to its parent process:
defmodule MyClient do
use OpcUA.Client
alias OpcUA.Client
# Use the `init` function to configure your Client.
def init({parent_pid, 103} = _user_init_state, opc_ua_client_pid) do
%{parent_pid: parent_pid, opc_ua_client_pid: opc_ua_client_pid}
end
def configuration(_user_init_state), do: Application.get_env(:my_client, :configuration, [])
def monitored_items(_user_init_state), do: Application.get_env(:my_client, :monitored_items, [])
def handle_subscription_timeout(subscription_id, state) do
send(state.parent_pid, {:subscription_timeout, subscription_id})
state
end
def handle_deleted_subscription(subscription_id, state) do
send(state.parent_pid, {:subscription_delete, subscription_id})
state
end
def handle_monitored_data(changed_data_event, state) do
send(state.parent_pid, {:value_changed, changed_data_event})
state
end
def handle_deleted_monitored_item(subscription_id, monitored_id, state) do
send(state.parent_pid, {:item_deleted, {subscription_id, monitored_id}})
state
end
def read_node_value(pid, node), do: GenServer.call(pid, {:read, node}, :infinity)
def get_client(pid), do: GenServer.call(pid, {:get_client, nil})
def handle_call({:read, node}, _from, state) do
resp = Client.read_node_value(state.opc_ua_client_pid, node)
{:reply, resp, state}
end
def handle_call({:get_client, nil}, _from, state) do
{:reply, state.opc_ua_client_pid, state}
end
end
This code can be executed as
{:ok, c_pid} = MyClient.start_link({self(), 103})
More examples can be found in the source code tests.