defmodule OpcUA.Server do use OpcUA.Common alias OpcUA.{NodeId} @moduledoc """ OPC UA Server API module. This module provides functions for configuration, add/delete/read/write nodes and discovery a OPC UA Server. `OpcUA.Server` is implemented as a `__using__` macro so that you can put it in any module, you can initialize your Server manually (see `test/server_tests/write_event_test.exs`) or by overwriting `configuration/1` and `address_space/1` to autoset the configuration and information model. It also helps you to handle Server's "write value" events by overwriting `handle_write/2` callback. The following example shows a module that takes its configuration from the environment (see `test/server_tests/terraform_test.exs`): ```elixir defmodule MyServer do use OpcUA.Server alias OpcUA.Server # Use the `init` function to configure your server. def init({parent_pid, 103} = _user_init_state, opc_ua_server_pid) do Server.start(opc_ua_server_pid) %{parent_pid: parent_pid} 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 ``` Because it is small a GenServer, it accepts the same [options](https://hexdocs.pm/elixir/GenServer.html#module-how-to-supervise) for supervision to configure the child spec and passes them along to `GenServer`: ```elixir defmodule MyModule do use OpcUA.Server, restart: :transient, shutdown: 10_000 end ``` """ @doc """ Optional callback that handles node values updates from a Client to a Server. It's first argument will a tuple, in which its first element is the `node_id` of the updated node and the second element is the updated value. the second argument it's the GenServer state (Parent process). """ @callback handle_write(key :: {%NodeId{}, any}, term()) :: term() @type config_params :: {:hostname, binary()} | {:port, non_neg_integer()} | {:users, keyword()} @type config_options :: {:config, config_params} | {:discovery, {binary(), non_neg_integer()}} @doc """ Optional callback that gets the Server configuration and discovery connection parameters. """ @callback configuration(term()) :: config_options @type address_space_list :: {:namespace, binary()} | {:variable_node, %OpcUA.VariableNode{}} | {:variable_type_node, %OpcUA.VariableTypeNode{}} | {:method_node, %OpcUA.MethodNode{}} #WIP | {:object_node, %OpcUA.ObjectNode{}} | {:object_type_node, %OpcUA.ObjectTypeNode{}} | {:reference_type_node, %OpcUA.ReferenceTypeNode{}} | {:data_type_node, %OpcUA.DataTypeNode{}} | {:view_node, %OpcUA.ViewNode{}} | {:reference_node, %OpcUA.ReferenceNode{}} | {:monitored_item, %OpcUA.MonitoredItem{}} @doc """ Optional callback that gets a list of nodes (with their attributes) to be automatically set. """ @callback address_space(term()) :: address_space_list defmacro __using__(opts) do quote location: :keep, bind_quoted: [opts: opts] do use GenServer, Keyword.drop(opts, [:configuration]) @behaviour OpcUA.Server @mix_env Mix.env() alias __MODULE__ def start_link(user_initial_params \\ []) do GenServer.start_link(__MODULE__, user_initial_params, unquote(opts)) end @impl true def init(user_initial_params) do send self(), :init {:ok, user_initial_params} end @impl true def handle_info(:init, user_initial_params) do # Server Terraform {:ok, s_pid} = OpcUA.Server.start_link() configuration = apply(__MODULE__, :configuration, [user_initial_params]) address_space = apply(__MODULE__, :address_space, [user_initial_params]) OpcUA.Server.set_default_config(s_pid) # configutation = [config: list(), discovery: {term(), term()}] set_server_config(s_pid, configuration, :config) set_server_config(s_pid, configuration, :discovery) # address_space = [namespace: "", namespace: "", variable: %VariableNode{}, ...] set_server_address_space(s_pid, address_space) # User initialization. user_state = apply(__MODULE__, :init, [user_initial_params, s_pid]) {:noreply, user_state} end def handle_info({%NodeId{} = node_id, value}, state) do state = apply(__MODULE__, :handle_write, [{node_id, value}, state]) {:noreply, state} end @impl true def handle_write(write_event, state) do require Logger Logger.warn("No handle_write/2 clause in #{__MODULE__} provided for #{inspect(write_event)}") state end @impl true def address_space(_user_init_state), do: [] @impl true def configuration(_user_init_state), do: [] defp set_server_config(s_pid, configuration, type) do config_params = Keyword.get(configuration, type, []) Enum.each(config_params, fn(config_param) -> GenServer.call(s_pid, {type, config_param}) end) end defp set_server_address_space(s_pid, address_space) do for {node_type, node_params} <- address_space, reduce: %{} do acc -> add_node(s_pid, node_type, node_params, acc) end end defp add_node(s_pid, :namespace, node_param, namespaces) do ns_index = GenServer.call(s_pid, {:add, {:namespace, node_param}}) Map.put(namespaces, node_param, ns_index) end defp add_node(s_pid, node_type, node, namespaces) do # separate the node params (creation arguments & node attributes) {node_args, node_attrs} = node |> Map.from_struct() |> Map.pop(:args) # Create node #node_args = replace_namespace(node_args, namespaces) GenServer.call(s_pid, {:add, {node_type, node_args}}) # add nodes attribures node_id = Keyword.get(node_args, :requested_new_node_id, nil) #node_attrs = replace_namespace(node_attrs, namespaces) add_node_attrs(s_pid, node_id, node_attrs) namespaces end defp add_node_attrs(s_pid, node_id, node_attrs) do for {attr, attr_value} <- node_attrs do set_node_attr(s_pid, node_id, attr, attr_value) end end defp set_node_attr(_s_pid, _node_id, _attr, nil), do: nil defp set_node_attr(s_pid, node_id, attr, attr_value) do GenServer.call(s_pid, {:write, {attr, node_id, attr_value}}) end # TODO: complete the function. # defp replace_namespace(params, namespaces) do # for {param, param_value} <- params, reduce: %{} do # acc -> # param_value = # if is_struct(params_value) do # end # Map.put # end # end defoverridable start_link: 0, start_link: 1, configuration: 1, address_space: 1, handle_write: 2 end end @doc """ Starts up a OPC UA Server GenServer. """ @spec start_link(term(), list()) :: {:ok, pid} | {:error, term} | {:error, :einval} def start_link(args \\ [], opts \\ []) do GenServer.start_link(__MODULE__, {args, self()}, opts) end @doc """ Stops a OPC UA Server GenServer. """ @spec stop(GenServer.server()) :: :ok def stop(pid) do GenServer.stop(pid) end # Configuration & lifecycle functions @doc """ Reads an internal Server Config. """ @spec get_config(GenServer.server()) :: {:ok, map()} | {:error, binary()} | {:error, :einval} def get_config(pid) do GenServer.call(pid, {:config, {:get_server_config, nil}}) end @doc """ Sets a default Server Config. """ @spec set_default_config(GenServer.server()) :: :ok | {:error, binary()} | {:error, :einval} def set_default_config(pid) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:set_default_server_config, nil}}) else # Valgrind GenServer.call(pid, {:config, {:set_default_server_config, nil}}, :infinity) end end @doc """ Sets a default Server Config with no network layer and no endpoints. """ @spec set_basics(GenServer.server()) :: :ok | {:error, binary()} | {:error, :einval} def set_basics(pid) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:set_basics, nil}}) else # Valgrind GenServer.call(pid, {:config, {:set_basics, nil}}, :infinity) end end @doc """ Sets a port number for the Server. """ @spec set_network_tcp_layer(GenServer.server(), integer()) :: :ok | {:error, binary()} | {:error, :einval} def set_network_tcp_layer(pid, port) when is_integer(port) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:network_tcp_layer, port}}) else # Valgrind GenServer.call(pid, {:config, {:network_tcp_layer, port}}, :infinity) end end @doc """ Sets the host name for the Server. """ @spec set_hostname(GenServer.server(), binary()) :: :ok | {:error, binary()} | {:error, :einval} def set_hostname(pid, hostname) when is_binary(hostname) do GenServer.call(pid, {:config, {:hostname, hostname}}) end @doc """ Sets a port number for the Server. """ @spec set_port(GenServer.server(), integer()) :: :ok | {:error, binary()} | {:error, :einval} def set_port(pid, port) when is_integer(port) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:port, port}}) else # Valgrind GenServer.call(pid, {:config, {:port, port}}, :infinity) end end @doc """ Adds users (and passwords) the Server. Users must be a tuple list ([{user, password}]). """ @spec set_users(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def set_users(pid, users) when is_list(users) do GenServer.call(pid, {:config, {:users, users}}) end @doc """ Adds endpoints for all configured security policies in each mode. """ @spec add_all_endpoints(GenServer.server()) :: :ok | {:error, binary()} | {:error, :einval} def add_all_endpoints(pid) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:add_all_endpoints, nil}}) else # Valgrind GenServer.call(pid, {:config, {:add_all_endpoints, nil}}, :infinity) end end @doc """ Start OPC UA Server. """ @spec start(GenServer.server()) :: :ok | {:error, binary()} | {:error, :einval} def start(pid) do GenServer.call(pid, {:start_server, nil}) end @doc """ Stop OPC UA Server. """ @spec stop_server(GenServer.server()) :: :ok | {:error, binary()} | {:error, :einval} def stop_server(pid) do GenServer.call(pid, {:stop_server, nil}) end # Encryption @doc """ Creates a server configuration with all security policies for the given certificates. The following must be filled: * `:private_key` -> binary() or function(). * `:certificate` -> binary() or function(). * `:port` -> interger(). """ @spec set_default_config_with_certs(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def set_default_config_with_certs(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:all_policies, args}}) else # Valgrind GenServer.call(pid, {:config, {:all_policies, args}}, :infinity) end end @doc """ Adds the security policy ``SecurityPolicy#None`` to the server with certicate (no endpoint). The following must be filled: * `:certificate` -> binary() or function(). """ @spec add_none_policy(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_none_policy(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:none_policy, args}}) else # Valgrind GenServer.call(pid, {:config, {:none_policy, args}}, :infinity) end end @doc """ Adds the security policy ``SecurityPolicy#Basic128Rsa15`` to the server with certicate (no endpoint). The following must be filled: * `:private_key` -> binary() or function(). * `:certificate` -> binary() or function(). """ @spec add_basic128rsa15_policy(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_basic128rsa15_policy(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:basic128rsa15_policy, args}}) else # Valgrind GenServer.call(pid, {:config, {:basic128rsa15_policy, args}}, :infinity) end end @doc """ Adds the security policy ``SecurityPolicy#Basic256`` to the server with certicate (no endpoint). The following must be filled: * `:private_key` -> binary() or function(). * `:certificate` -> binary() or function(). """ @spec add_basic256_policy(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_basic256_policy(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:basic256_policy, args}}) else # Valgrind GenServer.call(pid, {:config, {:basic256_policy, args}}, :infinity) end end @doc """ Adds the security policy ``SecurityPolicy#Basic256Sha256`` to the server with certicate (no endpoint). The following must be filled: * `:private_key` -> binary() or function(). * `:certificate` -> binary() or function(). """ @spec add_basic256sha256_policy(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_basic256sha256_policy(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:basic256sha256_policy, args}}) else # Valgrind GenServer.call(pid, {:config, {:basic256sha256_policy, args}}, :infinity) end end @doc """ Adds all supported security policies and sets up certificate validation procedures (no endpoint). The following must be filled: * `:private_key` -> binary() or function(). * `:certificate` -> binary() or function(). """ @spec add_all_policies(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_all_policies(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:config, {:all_policies_no_endpoint, args}}) else # Valgrind GenServer.call(pid, {:config, {:all_policies_no_endpoint, args}}, :infinity) end end # Discovery functions @doc """ Sets the configuration for the a Server representing a local discovery server as a central instance. Any other server can register with this server using "discovery_register" function NOTE: before calling this function, this server should have the default configuration. LDS Servers only supports the Discovery Services. Cannot be used in combination with any other capability. The following args must be filled: * `:application_uri` -> binary(). * `:timeout` -> boolean(). """ @spec set_lds_config(GenServer.server(), binary(), integer() | nil) :: :ok | {:error, binary()} | {:error, :einval} def set_lds_config(pid, application_uri, timeout \\ nil) when is_binary(application_uri) and (is_integer(timeout) or is_nil(timeout)) do GenServer.call(pid, {:discovery, {application_uri, timeout}}) end @doc """ Registers a server in a discovery server. NOTE: The Server sends the request once started. Use port = 0 to dynamically port allocation. The following must be filled: * `:application_uri` -> binary(). * `:server_name` -> binary(). * `:endpoint` -> binary(). * `:timeout` -> boolean(). """ @spec discovery_register(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def discovery_register(pid, args) when is_list(args) do if(@mix_env != :test) do GenServer.call(pid, {:discovery, {:discovery_register, args}}) else # Valgrinnd GenServer.call(pid, {:discovery, {:discovery_register, args}}, :infinity) end end @doc """ Unregister the server from the discovery server. NOTE: Server must be started. """ @spec discovery_unregister(GenServer.server()) :: :ok | {:error, binary()} | {:error, :einval} def discovery_unregister(pid) do GenServer.call(pid, {:discovery, {:discovery_unregister, nil}}) end # Add & Delete nodes functions @doc """ Add a new namespace. """ @spec add_namespace(GenServer.server(), binary()) :: {:ok, integer()} | {:error, binary()} | {:error, :einval} def add_namespace(pid, namespace) when is_binary(namespace) do GenServer.call(pid, {:add, {:namespace, namespace}}) end @doc """ Add a new variable node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. * `:type_definition` -> %NodeID{}. """ @spec add_variable_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_variable_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:variable_node, args}}) end @doc """ Add a new variable type node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. * `:type_definition` -> %NodeID{}. """ @spec add_variable_type_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_variable_type_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:variable_type_node, args}}) end @doc """ Add a new object node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. * `:type_definition` -> %NodeID{}. """ @spec add_object_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_object_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:object_node, args}}) end @doc """ Add a new object type node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. """ @spec add_object_type_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_object_type_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:object_type_node, args}}) end @doc """ Add a new view node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. """ @spec add_view_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_view_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:view_node, args}}) end @doc """ Add a new reference type node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. """ @spec add_reference_type_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_reference_type_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:reference_type_node, args}}) end @doc """ Add a new data type node to the server. The following must be filled: * `:requested_new_node_id` -> %NodeID{}. * `:parent_node_id` -> %NodeID{}. * `:reference_type_node_id` -> %NodeID{}. * `:browse_name` -> %QualifiedName{}. """ @spec add_data_type_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_data_type_node(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:data_type_node, args}}) end @doc """ Add a new reference in the server. The following must be filled: * `:source_id` -> %NodeID{}. * `:reference_type_id` -> %NodeID{}. * `:target_id` -> %NodeID{}. * `:is_forward` -> boolean(). """ @spec add_reference(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def add_reference(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:reference, args}}) end @doc """ Deletes a reference in the server. The following must be filled: * `:source_id` -> %NodeID{}. * `:reference_type_id` -> %NodeID{}. * `:target_id` -> %NodeID{}. * `:is_forward` -> boolean(). * `:delete_bidirectional` -> boolean(). """ @spec delete_reference(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def delete_reference(pid, args) when is_list(args) do GenServer.call(pid, {:delete_reference, args}) end @doc """ Deletes a node in the server. The following must be filled: * `:node_id` -> %NodeID{}. * `:delete_references` -> boolean(). """ @spec delete_node(GenServer.server(), list()) :: :ok | {:error, binary()} | {:error, :einval} def delete_node(pid, args) when is_list(args) do GenServer.call(pid, {:delete_node, args}) end # Add Monitored Items function @doc """ Create a local MonitoredItem with a sampling interval that detects data changes. The following must be filled: * `:monitored_item` -> %NodeID{}. * `:sampling_time` -> double(). """ @spec add_monitored_item(GenServer.server(), list()) :: {:ok, integer()} | {:error, binary()} | {:error, :einval} def add_monitored_item(pid, args) when is_list(args) do GenServer.call(pid, {:add, {:monitored_item, args}}) end @doc """ Deletes a local MonitoredItem. """ @spec delete_monitored_item(GenServer.server(), integer()) :: :ok | {:error, binary()} | {:error, :einval} def delete_monitored_item(pid, monitored_item_id) when is_integer(monitored_item_id) do GenServer.call(pid, {:delete_monitored_item, monitored_item_id}) end @doc false def test(pid) do GenServer.call(pid, {:test, nil}, :infinity) end # Handlers def init({_args, controlling_process}) do lib_dir = :opex62541 |> :code.priv_dir() |> to_string() |> set_ld_library_path() executable = lib_dir <> "/opc_ua_server" port = open_port(executable, use_valgrind?()) state = %State{port: port, controlling_process: controlling_process} {:ok, state} end # Handlers Lifecyle & Configuration Functions. def handle_call({:config, {:get_server_config, nil}}, caller_info, state) do call_port(state, :get_server_config, caller_info, nil) {:noreply, state} end def handle_call({:config, {:set_default_server_config, nil}}, caller_info, state) do call_port(state, :set_default_server_config, caller_info, nil) {:noreply, state} end def handle_call({:config, {:set_basics, nil}}, caller_info, state) do call_port(state, :set_basics, caller_info, nil) {:noreply, state} end def handle_call({:config, {:network_tcp_layer, port}}, caller_info, state) do call_port(state, :set_network_tcp_layer, caller_info, port) {:noreply, state} end def handle_call({:config, {:hostname, hostname}}, caller_info, state) do call_port(state, :set_hostname, caller_info, hostname) {:noreply, state} end def handle_call({:config, {:port, port}}, caller_info, state) do call_port(state, :set_port, caller_info, port) {:noreply, state} end def handle_call({:config, {:users, users}}, caller_info, state) do call_port(state, :set_users, caller_info, users) {:noreply, state} end def handle_call({:config, {:add_all_endpoints, nil}}, caller_info, state) do call_port(state, :add_all_endpoints, caller_info, nil) {:noreply, state} end def handle_call({:start_server, nil}, caller_info, state) do call_port(state, :start_server, caller_info, nil) {:noreply, state} end def handle_call({:stop_server, nil}, caller_info, state) do call_port(state, :stop_server, caller_info, nil) {:noreply, state} end # Encryption. def handle_call({:config, {:all_policies, args}}, caller_info, state) do with cert <- Keyword.fetch!(args, :certificate), pkey <- Keyword.fetch!(args, :private_key), port <- Keyword.get(args, :port, nil), certificate <- get_binary_data(cert), private_key <- get_binary_data(pkey), true <- is_binary(certificate), true <- is_binary(private_key), true <- is_integer(port) || is_nil(port) do c_args = {port, certificate, private_key} call_port(state, :set_config_with_security_policies, caller_info, c_args) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end def handle_call({:config, {:none_policy, args}}, caller_info, state) do with cert <- Keyword.fetch!(args, :certificate), certificate <- get_binary_data(cert), true <- is_binary(certificate) do call_port(state, :add_security_policy_none, caller_info, certificate) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end def handle_call({:config, {:basic128rsa15_policy, args}}, caller_info, state) do with cert <- Keyword.fetch!(args, :certificate), pkey <- Keyword.fetch!(args, :private_key), certificate <- get_binary_data(cert), private_key <- get_binary_data(pkey), true <- is_binary(certificate), true <- is_binary(private_key) do c_args = {certificate, private_key} call_port(state, :add_security_policy_basic128rsa15, caller_info, c_args) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end def handle_call({:config, {:basic256_policy, args}}, caller_info, state) do with cert <- Keyword.fetch!(args, :certificate), pkey <- Keyword.fetch!(args, :private_key), certificate <- get_binary_data(cert), private_key <- get_binary_data(pkey), true <- is_binary(certificate), true <- is_binary(private_key) do c_args = {certificate, private_key} call_port(state, :add_security_policy_basic256, caller_info, c_args) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end def handle_call({:config, {:basic256sha256_policy, args}}, caller_info, state) do with cert <- Keyword.fetch!(args, :certificate), pkey <- Keyword.fetch!(args, :private_key), certificate <- get_binary_data(cert), private_key <- get_binary_data(pkey), true <- is_binary(certificate), true <- is_binary(private_key) do c_args = {certificate, private_key} call_port(state, :add_security_policy_basic256sha256, caller_info, c_args) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end def handle_call({:config, {:all_policies_no_endpoint, args}}, caller_info, state) do with cert <- Keyword.fetch!(args, :certificate), pkey <- Keyword.fetch!(args, :private_key), certificate <- get_binary_data(cert), private_key <- get_binary_data(pkey), true <- is_binary(certificate), true <- is_binary(private_key) do c_args = {certificate, private_key} call_port(state, :add_all_security_policies, caller_info, c_args) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end # Discovery Functions. def handle_call({:discovery, {:discovery_register, args}}, caller_info, state) do application_uri = Keyword.fetch!(args, :application_uri) server_name = Keyword.fetch!(args, :server_name) endpoint = Keyword.fetch!(args, :endpoint) timeout = Keyword.get(args, :timeout, nil) c_args = {application_uri, server_name, endpoint, timeout} call_port(state, :discovery_register, caller_info, c_args) {:noreply, state} end def handle_call({:discovery, {:discovery_unregister, nil}}, caller_info, state) do call_port(state, :discovery_unregister, caller_info, nil) {:noreply, state} end def handle_call({:discovery, {application_uri, timeout}}, caller_info, state) do c_args = {application_uri, timeout} call_port(state, :set_lds_config, caller_info, c_args) {:noreply, state} end # Handlers Add & Delete Functions. def handle_call({:add, {:namespace, namespace}}, caller_info, state) do call_port(state, :add_namespace, caller_info, namespace) {:noreply, state} end def handle_call({:add, {:variable_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() type_definition = Keyword.fetch!(args, :type_definition) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name, type_definition} call_port(state, :add_variable_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:variable_type_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() type_definition = Keyword.fetch!(args, :type_definition) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name, type_definition} call_port(state, :add_variable_type_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:object_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() type_definition = Keyword.fetch!(args, :type_definition) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name, type_definition} call_port(state, :add_object_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:object_type_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name} call_port(state, :add_object_type_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:view_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name} call_port(state, :add_view_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:reference_type_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name} call_port(state, :add_reference_type_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:data_type_node, args}}, caller_info, state) do requested_new_node_id = Keyword.fetch!(args, :requested_new_node_id) |> to_c() parent_node_id = Keyword.fetch!(args, :parent_node_id) |> to_c() reference_type_node_id = Keyword.fetch!(args, :reference_type_node_id) |> to_c() browse_name = Keyword.fetch!(args, :browse_name) |> to_c() c_args = {requested_new_node_id, parent_node_id, reference_type_node_id, browse_name} call_port(state, :add_data_type_node, caller_info, c_args) {:noreply, state} end def handle_call({:add, {:reference, args}}, caller_info, state) do source_id = Keyword.fetch!(args, :source_id) |> to_c() reference_type_id = Keyword.fetch!(args, :reference_type_id) |> to_c() target_id = Keyword.fetch!(args, :target_id) |> to_c() is_forward = Keyword.fetch!(args, :is_forward) c_args = {source_id, reference_type_id, target_id, is_forward} call_port(state, :add_reference, caller_info, c_args) {:noreply, state} end def handle_call({:delete_reference, args}, caller_info, state) do source_id = Keyword.fetch!(args, :source_id) |> to_c() reference_type_id = Keyword.fetch!(args, :reference_type_id) |> to_c() target_id = Keyword.fetch!(args, :target_id) |> to_c() is_forward = Keyword.fetch!(args, :is_forward) delete_bidirectional = Keyword.fetch!(args, :delete_bidirectional) c_args = {source_id, reference_type_id, target_id, is_forward, delete_bidirectional} call_port(state, :delete_reference, caller_info, c_args) {:noreply, state} end def handle_call({:delete_node, args}, caller_info, state) do node_id = Keyword.fetch!(args, :node_id) |> to_c() delete_reference = Keyword.fetch!(args, :delete_reference) c_args = {node_id, delete_reference} call_port(state, :delete_node, caller_info, c_args) {:noreply, state} end # Add/delete Monitored Items function def handle_call({:add, {:monitored_item, args}}, caller_info, state) do with monitored_item <- Keyword.fetch!(args, :monitored_item) |> to_c(), sampling_time <- Keyword.fetch!(args, :sampling_time), true <- is_float(sampling_time) do c_args = {monitored_item, sampling_time} call_port(state, :add_monitored_item, caller_info, c_args) {:noreply, state} else _ -> {:reply, {:error, :einval} ,state} end end def handle_call({:delete_monitored_item, monitored_item_id}, caller_info, state) do call_port(state, :delete_monitored_item, caller_info, monitored_item_id) {:noreply, state} end # Catch all def handle_call({:test, nil}, caller_info, state) do call_port(state, :test, caller_info, nil) {:noreply, state} end def handle_call(invalid_call, _caller_info, state) do Logger.error("#{__MODULE__} Invalid call: #{inspect(invalid_call)}") {:reply, {:error, :einval}, state} end def handle_info({_port, {:exit_status, code}}, state) do Logger.warn("(#{__MODULE__}) Error code: #{inspect(code)}.") # retrying delay Process.sleep(@c_timeout) {:stop, :restart, state} end def handle_info({:EXIT, _port, reason}, state) do Logger.debug("(#{__MODULE__}) Exit reason: #{inspect(reason)}") # retrying delay Process.sleep(@c_timeout) {:stop, :restart, state} end def handle_info(msg, state) do Logger.warn("(#{__MODULE__}) Unhandled message: #{inspect(msg)}.") {:noreply, state} end defp handle_c_response( {:write, {ns_index, type, name}, c_value}, %{controlling_process: c_pid} = state ) do variable_node = NodeId.new(ns_index: ns_index, identifier_type: type, identifier: name) value = parse_c_value(c_value) send(c_pid, {variable_node, value}) state end defp handle_c_response({:test, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end # C Handlers Lifecyle & Configuration Functions. defp handle_c_response({:get_server_config, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:set_default_server_config, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:set_basics, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:set_network_tcp_layer, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:set_hostname, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:set_port, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:set_users, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_all_endpoints, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:start_server, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:stop_server, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end # C Encryption function. defp handle_c_response({:set_config_with_security_policies, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_security_policy_none, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_security_policy_basic128rsa15, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_security_policy_basic256, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_security_policy_basic256sha256, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_all_security_policies, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end # C Handlers Add & Delete Functions. defp handle_c_response({:add_namespace, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_variable_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_variable_type_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_object_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_object_type_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_view_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_reference_type_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_data_type_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:add_reference, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:delete_reference, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:delete_node, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end # C Handlers "Discovery". defp handle_c_response({:set_lds_config, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:discovery_register, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:discovery_unregister, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end # Add Monitored Items function defp handle_c_response({:add_monitored_item, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end defp handle_c_response({:delete_monitored_item, caller_metadata, data}, state) do GenServer.reply(caller_metadata, data) state end end