A Minimal OPC-UA Server#

Let’s start exploring the enginir_opcua package by building a minimal runnable server. It is secure from the start: only encrypted and signed connections are offered, and a client has to be trusted before it can connect. The Quick Setup page explains the pieces; this page builds a server you can keep running, and A Minimal OPC-UA Client the client for it. Both in one process are in 01_secure_pair.py.

Preparing the certificates and a user#

A server needs a PKI directory with its certificate, and a client that is allowed to connect needs one too. The command line sets both up for development on one machine:

$ enginir-opcua init server srv --endpoint opc.tcp://127.0.0.1:4840/enginir-opcua/server/
$ enginir-opcua init client cli
$ enginir-opcua pair srv/pki cli/pki
$ enginir-opcua user add srv/users.toml alice --role user

pair makes the two directories trust each other, and it says that it is for one machine only; on a network you move the certificates yourself with enginir-opcua trust. user add asks for a password and stores only a salted hash. A user with the role user may read, write and call methods; anonymous clients may only read.

Server() and Server.quickstart offer only SignAndEncrypt endpoints. To try the server without certificates, use the development profile or Server(allow_no_security=True) and read Security Profiles first.

The server#

server.py#
import asyncio

from enginir_opcua import Server, ua, uamethod


@uamethod
def multiply(parent: ua.NodeId, x: float, y: float) -> float:
    return x * y


async def main() -> None:
    server = Server.quickstart(
        "opc.tcp://127.0.0.1:4840/enginir-opcua/server/", pki="srv/pki", users="srv/users.toml"
    )
    await server.init()
    idx = await server.register_namespace("urn:example:minimal")

    my_object = await server.nodes.objects.add_object(idx, "MyObject")
    my_variable = await my_object.add_variable(idx, "MyVariable", 6.7)
    await my_variable.set_writable()
    await server.nodes.objects.add_method(
        idx, "multiply", multiply, [ua.VariantType.Double, ua.VariantType.Double], [ua.VariantType.Double]
    )

    async with server:
        count = 0.0
        while True:
            await asyncio.sleep(1)
            count += 0.1
            await my_variable.write_value(count)


asyncio.run(main())

Run it with python server.py.

Setup and configuration#

Server.quickstart() returns a configured server with the secure profile: Basic256Sha256, Aes128_Sha256_RsaOaep and Aes256_Sha256_RsaPss, always SignAndEncrypt. init() creates the PKI directory when it is missing and loads the certificate. A custom namespace is registered with register_namespace(). It’s recommended that all custom objects, variables and methods live in a namespace of their own. We store its index as idx and use it to add our objects.

Creating objects, variables and methods#

add_object() and add_variable() create the object “MyObject” and a variable below it. By default all variables are read-only, so we are explicit and make it writable. A method is a Python function decorated with uamethod(), registered with add_method() together with the types of its arguments. An undecorated function works too, but then the conversion from and to UA Variant types is up to you. Methods with named arguments are in 04_methods.py.

Starting the server#

Using the server as a context manager with async with server: ... starts it and shuts it down cleanly. To keep it alive a loop is needed; here the loop also updates the variable once a second.

Looking at it from the command line#

The command line tools (see Command Line Tools) can connect with a secure channel by pointing them at the client’s PKI directory: --pki-dir alone means Basic256Sha256 with SignAndEncrypt. They take the ApplicationUri from the certificate in it.

$ enginir-opcua ls -u opc.tcp://127.0.0.1:4840/enginir-opcua/server/ --pki-dir cli/pki -n i=85
$ enginir-opcua read -u opc.tcp://127.0.0.1:4840/enginir-opcua/server/ --pki-dir cli/pki --user alice \
      -p "0:Objects,2:MyObject,2:MyVariable"

The second command reads the password from the environment variable ENGINIR_OPCUA_PASSWORD or asks for it with --ask-password.

Now that we have a working server, let’s go on and write A Minimal OPC-UA Client!