Communication

Orientation

Motivation

Modern industrial communication uses Ethernet based protocols. The OPC UA protocol has established itself over the last few years from the large number of available standards. This protocol has been developed from the predecessor OPC. Since the protocol is becoming more and more important and widespread, it is indispensable to gather knowledge in this area. The widespread use also has the advantage that ready-made libraries already exist for many programming languages.

Requirements

  • Knowledge of a programming language is necessary (C/C++, Python, ...).
  • Knowledge of object-oriented programming is recommended, but not necessary.

Goals

After completing the exercise, you will be able to…

  • … deal fundamentally with the industrial communication protocol OPC UA.
  • … read out which information is available on a server.
  • … read out individual information points in a targeted manner.
  • … be informed when selected data changes.
  • … execute functions on the remote server.

  • The basics in the next section
  • OPC Unified Architecture by Mahnke, Leitner & Damm

Guide

The exercise takes between 90 and 120 minutes. The concrete duration depends on the individual learning progress.

The following activities are expected of you:

  • In the module Basics
    • read up on and develop an understanding of the necessary theory.
  • In the module Exercise
    • read the general conditions of the exercise.
    • you will find ready-made source code in Python as sample solutions.
    • you will find recurring challenges when communicating with remote computers.
    • you will find various questions to test your knowledge.
  • In the module Application
    • you will be given various tasks.
  • In the module Considerations
    • you get a short summary of the results.

Basics

This exercise is intended to bring the use of an industrial communication protocol closer. The protocol used is OPC UA. OPC UA offers many possibilities and can be addressed via different programming languages. The protocol offers the following possibilities:

  • Data access on a server
  • Run procedures on the server
  • Create notifications for data changes
Possible programming languages:

After the exercise, you will be able to implement simple data transports using the OPC UA communication protocol. This allows you to interact with some of the online live exercises offered.

Nodes and Node-Classes

OPC UA works with data points, also called nodes. Each node has a number of attributes and based on the node class even more information:
  • Node ID
  • Node Class
  • Browse name
  • display name
  • Description
The Node-ID is unique for each node! Thus a Node is clearly described by the Node-ID. As soon as you have a Node-ID, you can query all information with it. The Node class indicates which information is actually available. The node classes and the information they provide will be discussed later. The browse name is a path element that is used for path-based searching (so-called browsing). The display name and the description are elements that contain a localized text. In addition to the attributes, a node has a list of references. References are covered in the next section.

Node-IDs

A node ID has two elements, a namespace and an identifier. The namespace is a numeric index specified by a URI. The URI designates an institution that is responsible for assigning identifiers. The index is nothing other than the position of the URI in a list stored on the server. The namespace index 0 denotes the OPC UA Foundation. There are four different types of identifiers:
  • Numeric
    A numeric identifier simply denotes an unsigned number. The designation i is used for the sereralization of this identifier.
  • String
    A string is a readable character string. During sereralization, the identifier is marked with s.
  • Byte String (opaque)
    An opaque identifier is an identifier that has been created according to a namespace-specific scheme. This is converted into a Base64 string for serealization. This is effective, since characters that cannot be represented can also occur in the byte string. The letter used for the identification is b.
  • GUID
    A GUID is a 16-byte hexadecimal number represented in the following format: uint32-uint16-uint16-uint16-uint8[2]-uint8[6] An example of a GUID is 9EAA3455-92B1-C9C6-89C8-2CA23723B2EB.
Nodes are generally serealized in the following form: ns=<namespaceIndex> <identifiertype>=<identifier> This syntax is also used to specify all nodes in specifications. Until now, the namespace index and the identifier have already been handled. Thus, the identifier type is still relevant.
Indentifier Identifier type Example
Numeric i ns=0;i=85
String s ns=1;s=Temperature
byte string b ns=1;b=M/RbKBsRVkePCePcx24oRA==
GUID g ns=1;g=9EAA3455-92B1-C9C6-89C8-2CA23723B2EB

Node-Classes

The node classes define which additional information is available.
  • Object
    An object is used to create systems or subsystems. It does not matter if it is a physical or virtual system that is mapped. The following attributes are added to the basic attributes:
    • Event Note
      This signals which events can be monitored.
  • Variable
    A variable is used to give content to an object. Since a variable represents a certain value, a variable has many more attributes:
    • DataType
      This is the node ID of the data type.
    • ValueRank
      The ValueRank indicates whether the data is a single value or a list of values. More precisely, the values can be scalar, one-dimensional array, or multidimensional array.
    • ArrayDimensions
      This element indicates how large the individual dimensions are. This attribute is only relevant if the data is not available as a scalar.
    • Value
      This is the value managed by the node. How this element is interpreted depends on the DataType, ValueRank and ArrayDimensions attributes.
    • Historizing
      This element specifies whether a history of the data is stored on the server.
    • AccessLevel
      This element specifies which authorizations are generally granted. Possible permissions are read and write the current value and read/write the history.
    • UserAccessLevel
      This element specifies which authorizations the current user has.
    • MinimumSamplingInterval
      This element specifies how fast the server can capture changes to a value. This is of particular interest if the server must first request the information from a subsystem.
  • Methods
    Methods are functions that can be executed on the server.
    • Executable
      This attribute specifies whether the function is currently executable.
    • UserExecutable
      This attribute specifies whether the function can be executed by the current user.
  • ReferenceType
    ReferenceTypes describe the relation between elements on the server. A detailed explanation of references can be found in the next section.
    • isAbstract
      This attribute specifies whether the type is an organizational element of the hierarchy or whether it can actually be used. If the type is abstract, it cannot be created, but can be used to address existing elements of a subtype. References are discussed in more detail in the later section.
    • symmetric
      This attribute indicates whether the reference is symmetric. If it is symmetric, then the reference in the opposite direction has the same meaning. An example of a non-symmetric reference is the parent-child relationship; for example: A is the parent of B and B is the child of A. An example of a symmetric reference is the sibling relationship.
    • inverseName
      If the reference is not symmetric, this element specifies the name of the inverted reference. In the previous example, the parent of was the regular name of the reference and is the child of is the name of the reverse reference.
  • DataType
    A DataType defines how the data is to be interpreted. The data types specified by the OPC UA Foundation can be found in Namespace 0 and represent basic data types. In addition to the basic data types, it is also possible to define your own data types. However, it makes more sense to define variable types and object types.
    • isAbstract
      This attribute specifies, as with the references, whether it is an organizational element or not.
  • VariableType
    A VariableType is used to define the type of a variable in order to give the data elements a meaning. This is especially interesting if you look at an example. Example: A node of the BaseVariableType, a generic variable type, is available on the server. The node represents a temperature. Now it is unknown whether the temperature is Celsius, Kelvin or Fahrenheit. However, when a Celsius type is defined and used, it is immediately clear how to interpret the data. Alternatively, a temperature data type can be defined, where the interpretation is declared in the description. However, this has the disadvantage that it is not immediately clear from the variable type how the data are to be interpreted.
    • DataType
      This is the node ID of the data type.
    • ValueRank
      The ValueRank indicates whether the data is a single value or a list of values. More precisely, the values can be scalar, one-dimensional array, or multidimensional array.
    • ArrayDimensions
      This element indicates how large the individual dimensions are. This attribute is only relevant if the data is not available as a scalar.
    • Value
      This is the value managed by the node. How this element is interpreted depends on the DataType, ValueRank and ArrayDimensions attributes.
    • isAbstract
      This attribute specifies, as with the references, whether it is an organizational element or not.
  • ObjectType
    An ObjectType is used to ensure that an object has certain attributes when it is created. In object-oriented programming languages, this construct would also be called class.
    • isAbstract
      This attribute specifies, as with the references, whether it is an organizational element or not.
  • View
    A view is a list of nodes that can be interesting for a particular task. For example, you can define a view that is interesting for maintenance and another view that is interesting for regular use. This does not require searching the entire address space of the server.
    • ContainsNoLoop
      This attribute specifies whether the displayed nodes build a circular hierarchy when you follow the references.
    • EventNotifier
      This attribute specifies whether events can be generated via this view (e.g. when data is changed) and whether the event history can be read and changed.

References and Datatypes

References

References are used to relate nodes to each other. A reference always has three components:
  • Source
  • Target
  • Reference type
OPC UA provides abstract and concrete reference types. Abstract reference types cannot be created, but can be used when querying connections. The following figure shows the reference types defined by the standard.
Reference types of opc ua
Abstract references are highlighted by special marking. The practical thing about abstract references is that they serve as organizational elements and can be used to filter results. Selected reference types are discussed below.
  • Organisms
    This reference is used when objects or variables are combined in a folder. A folder is an object that is only used to combine information points. For example, folders are used in the standard information model to group all objects, all type definitions and all views.
  • HasProperty
    This reference is used to map properties of objects and variables. Properties themselves cannot be sources of further references. An example of a property for a file would be the modification date.
  • HasComponent
    This reference is used to define the contents of an object. Elements created using this reference type can still be the source for new references.
  • HasOrderedComponent
    This reference is used to give methods to an object.

Dataypes

OPC UA definiert eine Reihe an Datentypen die verwendet werden können. Zusätzlich zu denen können noch eigene Datentypen definiert werden. Beispiele dafür sind zum Beispiel Aufzählungen; dafür gibt es den Basistyp Enumeration. Ebenso ist es möglich, eigene Strukturen zu definieren, hier muss jedoch im Regelfall auf beiden Seiten, also auf Server und Client, eine Funktion existieren, die die Daten interpretieren kann. Die gesamte Hierarchie der Datentypen ist in der folgenden Abbildung dargestellt.
Data types of opc ua
Im folgenden werden einige Datentypen behandelt:
NodeId
ExpandedNodeId
NodeIDs werden verwendet um Datenelemente eines Servers zu bezeichnen. Darauf wurde im vorhergehenden Abschnitt bereits eingegangen. NodeIDs werden verwendet um lokal Nodes eines Servers zu identifizieren. ExpandedNodeIDs erlauben es, Nodes eines anderen Servers zu referenzieren. So wäre es zum Beispiel möglich alle Variablen und Objektdefinitionen auf einem zentralen Server zu hinterlegen, und diese von anderen Servern referenzieren zu lassen.
DateTime
DateTime wird verwendet, um einen Zeitstempel zu erzeugen.
DataValue
Dies bezeichnet einen Datenwert mit einem Statuscode und einem Zeitstempel.
QualifiedName
Ein QualifiedName bezeichnet einen String, der mit einem Namespace-index versehen ist. Der Browse-Name eines Nodes ist ein Beispiel für einen QualifiedName. Dieser wird in den Angaben dieser Übung wie folgt gekennzeichnet: qn=<namespaceIndex>;<name>
Number
This data type summarizes all numbers. The type of number is irrelevant.
Integer
UInteger
These are signed or unsigned integers. Here, the number indicates how many bits are available.
Float
Double
These are floating point numbers with single and double precision.
DiagnosticInfo
This data type provides detailed information in the event of an error.
Boolean
This data type represents a value that can be True or False.
String
ByteString
Strings and bytestrings are character strings. While strings consist of printable characters, bytestrings consist of all possible characters. Since a byte string very often contains characters that cannot be displayed, they are displayed in Base64 encoded form.
LocalizedText
A LocalizedText is a structure that communicates information in a certain language. A LocalizedText thus has two elements, the language in which the information exists and the information itself.
GUID
A 16 byte value used as a global unique identification number. The value consists of a 32-bit value, two 16-bit values and eight 8-bit values.
XMLElement
An XML element is an element of the Extensible Markup Language.

Discovery and Browsing

OPC UA also defines ways to find servers and read information. Discovery defines how a server can be found and browsing describes how to get information about the available nodes.

Discovery

OPC offers the possibility to work with Discovery Servers. A discovery server is a server where other servers can register. This means that you only need to know one participant in the network to find all available servers.

Browsing

The following functions are offered when browsing:
  • Browse
    When browsing, you specify a node and get all nodes that have the specified node as source of a reference. You can also filter for references and node classes. At this point, the abstract references mentioned above are helpful, as they cover a large number of concrete references. The following information is always returned for each node:
    • Reference
    • Node ID
    • Browsename
    • Displayname
    • Node Class
    Selected predefined elements of the standard information model of OPC-UA are:
    • qn=0;Root
      • qn=0;Objects
        • qn=0;Server
          • qn=0;NamespaceArray
          • qn=0;ServerArray
      • qn=0;Types
        • qn=0;DataTypes
        • qn=0;ObjectTypes
        • qn=0;ReferenceTypes
        • qn=0;VariableTypes
      • qn=0;Views
  • BrowseNext
    This function can be used if not all nodes could be returned. This can be especially the case if more nodes were found than can be sent at once.
  • TranslateBrowsePathToNodeID
    This service can be used to convert a browse path to a node ID. This is especially interesting if you know the name of the data point, but the node ID is unknown. Since you need the Node-ID to be able to address nodes, this service is unavoidable. The Browsepath is a list of browser names for each node. This functionality is especially important if the elements on the server can be described semantically or by an ontology.
  • Register/Unregister Nodes
    With this functionality you can tell a server that the current connection will access more of the specified nodes. Two things happen. First, it reduces the amount of information that needs to be transferred. This is done by giving the client a numeric node ID, which is the easiest to transfer. Second, the server optimizes access to the data point itself so that writing to or executing from the node is more efficient. Unregister tells the server that the node is no longer needed. This functionality should not be used if the node is only read because there are more efficient mechanisms. These mechanisms are described in the Read/Write section.
The following figure shows a recursive browsing result, starting from the object folder and without elements that exist in other namespaces except 0 and 1. Browsing-Result starting from the object folder

Read/Write

Read

As a rule, all attributes of a node can be read. When reading the value of a node, the value attributes, the access rights are taken into account.

Write

As a rule, only the value attribute of a node can be described. The access rights of the current connection are also taken into account.

Subscriptions and Monitored Items

Subscriptions and Monitored Items are a mechanism to monitor nodes and send the new information to the client as needed. First, a client creates a subscription and defines how the information is transported. The client can define the priority of the data, how much information can be transferred at once and the interval at which the data is sent. Then, based on the subscription, Monitored Items can be created. Here you specify which node ID is to be monitored, at which interval the node is to be checked for changes and when a change is to be sent. You can decide whether you want to be informed if the status changes, if status and value change, or if status, value and timestamp change. If certain values are to be monitored, it is highly recommended to choose this mechanism.

Methods

Methods are functions that are stored on the server and can be executed over the network. Methods can have input parameters, output values, both, or none of both. Input parameters are used to provide additional information to the method call. Output values are used to pass information back to the caller. You can easily determine whether a method has arguments by searching the method node. If it has a reference to a node with the browser name qn=0;InputArguments, the method requires input parameters. If it has a reference to a node with the browser name qn=0;OutputArguments, the method returns output values.

Security

OPC UA uses various security mechanisms for communication. When a client contacts a server, a connection is initiated first. A secure channel is created based on the connection. Only after the secure channel has been created is a session created. All communication between client and server is handled in an existing session. The session itself is not bound to a specific secure channel, so a session is retained even if the secure channel is renewed. There are different types of authentication when logging on:
  • Anonymous
  • username/password
  • X509v3 Certificate
  • WS-SecurityToken
In addition to the authentication methods, there are currently three defined types of encryption with which communication is encrypted:
  • None - The communication is not encrypted.
  • Basic128RSA15
  • Basic256

Architecture

OPC UA can be used in various ways. The following are the most common configurations.

Server/Client

A computer, the server, provides information and methods. Other computers, the clients, connect to the server to get the information. This is the most common way in which the protocol is used.
Server/Client

Chained Server

With the Chained-Server architecture, communication takes place via an intermediate server. The intermediate server has an embedded client that communicates with the actual server. This server can be used, for example, for protocol conversion. Another application would be if the intermediate server is accessible from outside the production network, but the actual server is not. The server that is interacted with in the offered online exercises is a chained server. In this case, each connected server has its own namespace. The namespace of the server contains only organizational elements, a folder for each connected server and an indicator if the server is online.
Chained Server

Server to Server

Server to server communication is used when data has to be available on several servers. In this scenario, each server has an embedded client to notify the second server of changes. This is particularly advantageous when server redundancies are required.
Server to Server

Aggregating Server

An aggregating server connects several servers and processes the information obtained before passing it on. While the Chained Server only passes on the data of the connected servers, the Aggregating Server concentrates the information that can be obtained from the data.
Aggregating Server

Exercise

Different example exercises with Python solutions are presented here. Therefore, the Python codes and the functions of the Python OPC-UA library are explained step by step. The installation of the library is explained HERE, while the functions of the library are documented HERE.

Connection without User Credentials

This example demonstrates how to connect with an OPC-UA server by using the given library functions. A connection with the server from the University of Applied Sciences Vienna shall be established. The following basic data are necessary for connecting:
  • Server: engine.ie.technikum-wien.at
  • Port: 4840
First, the required Python libraries have to be imported. The library time will be used later to avoid the termination of the Python program. The class Client from opcua is imported only, since you only want to establish a connection. The server name and the port are assigned to the variables HOST and PORT. Now, an instance of the class Client can be created. The URL of the server as string is therefore required.
#!/usr/bin/env python3
"""
This example shows how to connect to the server without credentials
"""
import time
from opcua import Client

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    # This syntax does not provide credentials and we are logging in anonymously
    # at the specified server. The Server has been configured to allow anonymous
    # users to read every datapoint. All other actions are prohibited.
    CLIENT = Client("opc.tcp://{}:{}/".format(HOST, PORT))
In the next step you try to connect with the method connect(). In case this process is successful an infinite loop is executed, which is interrupted by a KeyboardInterrupt. Hence, the program can be stopped by the user inputs Control-C or Delete. In order to demonstrate a successful connection with the server, a corresponding information is exported every loop pass.
#!/usr/bin/env python3
"""
This example shows how to connect to the server without credentials
"""
import time
from opcua import Client

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    # This syntax does not provide credentials and we are logging in anonymously
    # at the specified server. The Server has been configured to allow anonymous
    # users to read every datapoint. All other actions are prohibited.
    CLIENT = Client("opc.tcp://{}:{}/".format(HOST, PORT))
    try:
        # Connect to the Server
        # This function connects to the server. If the connection has already
        # been implemented, nothing is done. If the connection terminated the
        # function reconnects
        CLIENT.connect()
        try:
            # Loop
            while True:
                print("connected with server")
                # Sleep
                time.sleep(1)
        except KeyboardInterrupt:
            pass
    finally:
        # Disconnect
        CLIENT.disconnect()
The output of the example solution is shown in the following image.
solution output connection without user credentials

Connection with User Credentials

The only difference to the previous example is that the connection is established with user credentials. The following basic data are necessary for connecting:
  • Server: engine.ie.technikum-wien.at
  • Port: 4840
  • Username: student
  • Password: student
The program code does already contain the entire program, since the user credentials for the server connection must be added only. These data are assigned to the variables USER and PASS. The correct URL requires the login information separated by :, which is assigned to the variable CREDENTIALS. Again, the instance Client is created, while the login information must be written in front of the server name in contrast to the first example. As in email addresses the user information are separated by the @ symbol from the server name. The continuing program is identical to the previous one.
#!/usr/bin/env python3
"""
This example shows how to connect to the server with credentials
"""
import time
from opcua import Client

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    USER = "student"
    PASS = "student"
    CREDENTIALS = "{}:{}".format(USER, PASS)
    # This syntax does provide credentials.
    CLIENT = Client("opc.tcp://{}@{}:{}/".format(CREDENTIALS, HOST, PORT))
    try:
        # Connect to the Server
        CLIENT.connect()
        try:
            # Loop
            while True:
                print("connected with server")
                # Sleep
                time.sleep(1)
        except KeyboardInterrupt:
            pass
    finally:
        # Disconnect
        CLIENT.disconnect()
The output of the example solution is shown in the following image.
solution output connection with user credentials

Browsing

The following task is to determine the nodes of the main server and the sample server. This requires two namespaces, the namespace of the main server itself and the namespace of the sample server. The following task displays the available namespaces, the elements of the main server namespace and the elements of the sample namespace. In addition to the data from the previous task, the following information is required:
  • Namespace of the main server: opc.tcp://engine.ie.technikum-wien.at
  • Namespace of the example server: opc.tcp://engine.ie.technikum-wien.at/OPCExamples
  • NodeID of the example server: ns=1;s=OPC Examples
Since the program should be stopped automatically after the required information has been determined, the library time is not required. The connection to the server is established as usual, while an anonymous connection is sufficient. In order to demonstrate which namespaces are available on the main server, they are exported in the next step. The namespaces of the server are assigned as a python list to the variable NAMESPACES by calling the method get_namespace_array(). By looping over the obtained list, the individual namespaces of the server are exported.
#!/usr/bin/env python3
"""
This example shows how to connect to the server without credentials
"""
from opcua import Client
from opcua import ua

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    # This syntax does not provide credentials and we are logging in anonymously
    # at the specified server. The Server has been configured to allow anonymous
    # users to read every datapoint. All other actions are prohibited.
    CLIENT = Client("opc.tcp://{}:{}/".format(HOST, PORT))
    try:
        # Connect to the Server
        # This function connects to the server. If the connection has already
        # been implemented, nothing is done. If the connection terminated the
        # function reconnects
        CLIENT.connect()
        # There are multiple Namespaces on the Server and we want to query them
        NAMESPACES = CLIENT.get_namespace_array()
        print(">Namespaces")
        for namespace in NAMESPACES:
            print(" {}".format(namespace))
Now, the nodes of the main server are determined. The node of the main server is of data type Object, which is assigned to the variable STARTNODE by applying the method get_objects_node(). In order to retrieve the reference nodes of this Object, the method get_children() is called. This method returns a python list containing the nodes, which is assigned to the variable NODE. Thus, all nodes of the main server are determined. The node information are exported by a for loop over the list object NODES. The output information contains the NodeID, the browse name as well as the name of the NodeClass. A ua.NodeClass object must be created, in order to obtain the name of the NodeClass, which is why the library ua has been imported at the beginning of the program. The ua.NodeClass object requires the NodeClass of the node, which is determined by the method get_node_class() in the last row of the program code.
#!/usr/bin/env python3
"""
This example shows how to connect to the server without credentials
"""
from opcua import Client
from opcua import ua

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    # This syntax does not provide credentials and we are logging in anonymously
    # at the specified server. The Server has been configured to allow anonymous
    # users to read every datapoint. All other actions are prohibited.
    CLIENT = Client("opc.tcp://{}:{}/".format(HOST, PORT))
    try:
        # Connect to the Server
        # This function connects to the server. If the connection has already
        # been implemented, nothing is done. If the connection terminated the
        # function reconnects
        CLIENT.connect()
        # There are multiple Namespaces on the Server and we want to query them
        NAMESPACES = CLIENT.get_namespace_array()
        print(">Namespaces")
        for namespace in NAMESPACES:
            print(" {}".format(namespace))
        # In order to browse, we need some node as a starting point.
        STARTNODE = CLIENT.get_objects_node()
        # The method get_children returns all children of the Node in question
        NODES = STARTNODE.get_children()
        print(">Browsing")
        print(" Children of: {}".format(STARTNODE.nodeid.to_string()))
        for node in NODES:
            print(" {} -- {} -- {}".format(
                node.nodeid.to_string(),                  # Short form of NodeID
                node.get_browse_name().to_string(),       # Short form
                ua.NodeClass(node.get_node_class()).name))# Name of the nodeclass
Now, you only have to determine the nodes of the example server. Since the NodeID is given in this example, the node is simply assigned to the variable STARTNODE by the method get_node(NodeID). Note that the node of the main server is no longer accessible due to the overwriting of the variable STARTNODE. The further procedure is identical to that of the previous part, while the connection with the server is terminated by the method disconnect() at the end of the program. For completion, the entire program code to solve this example is given by:
#!/usr/bin/env python3
"""
This example shows how to connect to the server without credentials
"""
from opcua import Client
from opcua import ua

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    # This syntax does not provide credentials and we are logging in anonymously
    # at the specified server. The Server has been configured to allow anonymous
    # users to read every datapoint. All other actions are prohibited.
    CLIENT = Client("opc.tcp://{}:{}/".format(HOST, PORT))
    try:
        # Connect to the Server
        # This function connects to the server. If the connection has already
        # been implemented, nothing is done. If the connection terminated the
        # function reconnects
        CLIENT.connect()
        # There are multiple Namespaces on the Server and we want to query them
        NAMESPACES = CLIENT.get_namespace_array()
        print(">Namespaces")
        for namespace in NAMESPACES:
            print(" {}".format(namespace))
        # In order to browse, we need some node as a starting point.
        STARTNODE = CLIENT.get_objects_node()
        # The method get_children returns all children of the Node in question
        NODES = STARTNODE.get_children()
        print(">Browsing")
        print(" Children of: {}".format(STARTNODE.nodeid.to_string()))
        for node in NODES:
            print(" {} -- {} -- {}".format(
                node.nodeid.to_string(),                  # Short form of NodeID
                node.get_browse_name().to_string(),       # Short form
                ua.NodeClass(node.get_node_class()).name))# Name of the nodeclass
        # As we know the Node-ID, we can access the node directly
        STARTNODE = CLIENT.get_node("ns=1;s=OPC Examples")
        NODES = STARTNODE.get_children()
        print(" Children of: {}".format(STARTNODE.nodeid.to_string()))
        for node in NODES:
            print(" {} -- {} -- {}".format(
                node.nodeid.to_string(),                  # Short form of NodeID
                node.get_browse_name().to_string(),       # Short form
                ua.NodeClass(node.get_node_class()).name))# Name of the nodeclass
    finally:
        # Disconnect
        CLIENT.disconnect()
The output of the example solution is shown in the following image.
solution output browse example

Reading

The following example demonstrates how to receive information from a node. The given variable ExampleVariable must be read in this example. In addition to the data from the previous task, the following information is required:
  • Namespace of the node: opc.tcp://engine.ie.technikum-wien.at/OPCExamples
  • Node ID part of the node to be read: s=ExampleVariable
Again, an anonymous connection with the server of the Univeristy of Applied Sciences Vienna is established. With the namespace the namespace index is determined by the method get_namespace_index() and assigned to the variable IDX. Therefore the URL of the namespace must be used as parameter for the method. A node instance (NODE) is created by the already explained method get_node(NodeID). The NodeID must be utilized as the method's parameter, while the previously determined namespace index is required. In the final step the value of the node is simply assigned to the variable VALUE by the method get_value() and exported in the console.
#!/usr/bin/env python3
"""
This example shows how to read a variable from the server
"""
from opcua import Client

if __name__ == "__main__":
    # Create a OPC-UA Client with the following specification:
    # Server: engine.ie.technikum-wien.at
    # Port: 4840
    HOST = "engine.ie.technikum-wien.at"
    PORT = 4840
    # This syntax does not provide credentials and we are logging in anonymously
    # at the specified server. The Server has been configured to allow anonymous
    # users to read every datapoint. All other actions are prohibited.
    CLIENT = Client("opc.tcp://{}:{}/".format(HOST, PORT))
    try:
        # Connect to the Server
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExamples")
        # Get Node from the client based on the NodeID
        NODE = CLIENT.get_node("ns={};s=ExampleVariable".format(IDX))
        # Read the current value
        VALUE = NODE.get_value()
        # Print the Information
        print("{}: {}".format(NODE, VALUE))
    finally:
        # Disconnect
        CLIENT.disconnect()
The output of the example solution is shown in the following image.
solution output read example

Subscribing

The exercise is to create a subscription with a monitored item for the corresponding node. This is the preferred variant for receiving data periodically. At each event of value change of the variable ExampleVariable a notification of the event shall be exported.
  • Namespace of the node: opc.tcp://engine.ie.technikum-wien.at/OPCExamples
  • Node ID part of the node to be read: s=ExampleVariable
In the first step the class SubscriptionHandler is created. The task of the SubscriptionHandler is to monitor the node. The functions of the class are responsible for following:
  • __init__(self)
    After creating an instance of the class, a counter is initialized. The counter saves the number of value changes.
  • __del__(self)
    If an instance of the class is deleted, a notification with the number of value changes will be exported.
  • datachange_notification(self, node, value, _)
    This function is called, when a value change of the assigned node is registered. The parameter value contains the current value of the node. The parameter _ contains the row data of the notification, which does not have to be considered any further. The current value shall be exported in this exercise. Additionally, the counter is incremented at each value change. The function name datachange_notification is a default notation given by the OPC-UA library and is called automatically at value change when required in the main program. The correct implementation of the value change notification is explained in the next part.
#!/usr/bin/env python3
"""
This example shows how to create a subscription to a variable
"""
import time
from opcua import Client

class SubscriptionHandler(object): # pylint: disable=too-few-public-methods
    """
    Subsrciption handler.
    """
    def __init__(self):
        self.counter = 0

    def __del__(self):
        print("Observed {} datachanges".format(self.counter))

    def datachange_notification(self, node, value, _):
        """
        Function that's called when data is changed

        Args:
            node: The node id that had a datachange
            value: The current value
            _: The raw data of the notification

        Returns:
            None
        """
        print("{}: {}".format(node, value))
        self.counter = self.counter + 1
The next step is the implementation of the main program. As usual, an anonymous connection with the server is established and the node to be monitored is assigned to the variable NODE. Thereafter, the instance HANDLER of our created class SubscriptionHandler is implemented. In order to create a subscription, the method create_subscription(500, HANDLER) is applied on the client variable. The first parameter (500) denotes the update time in milliseconds. Hence, in this example the examination regarding value changes occurs two times each second. Additionally, the method requires the desired handler. Thus, the variable SUB obtains the previously defined class SubscriptionHandler. The subscription also needs information about what to be monitored. This is accomplished by the method subscribe_data_change(NODE), which obtains the node to be monitored. In order to avoid the termination of the program at this point, an infinite loop is executed, which can be interrupted by the user input Control-C or Delete. In case the program is interrupted, the number of registered value changes shall be exported at the end of the program. This is done by deleting the instance using the method delete().
#!/usr/bin/env python3
"""
This example shows how to create a subscription to a variable
"""
import time
from opcua import Client

class SubscriptionHandler(object): # pylint: disable=too-few-public-methods
    """
    Subsrciption handler.
    """
    def __init__(self):
        self.counter = 0

    def __del__(self):
        print("Observed {} datachanges".format(self.counter))

    def datachange_notification(self, node, value, _):
        """
        Function that's called when data is changed

        Args:
            node: The node id that had a datachange
            value: The current value
            _: The raw data of the notification

        Returns:
            None
        """
        print("{}: {}".format(node, value))
        self.counter = self.counter + 1

if __name__ == "__main__":
    CLIENT = Client("opc.tcp://engine.ie.technikum-wien.at:4840/")
    try:
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExamples")
        NODE = CLIENT.get_node("ns={};s=ExampleVariable".format(IDX))
        # Create a handler to work with the updated data
        HANDLER = SubscriptionHandler()
        # Create a subscription
        SUB = CLIENT.create_subscription(500, HANDLER)
        # Connect NodeIDs to the Subscription
        SUB.subscribe_data_change(NODE)
        try:
            # Loop -- only needed so the program doesn't stop right away
            while True:
                # Sleep
                time.sleep(1)
        except KeyboardInterrupt:
            pass
        SUB.delete()
    finally:
        CLIENT.disconnect()
The output of the example solution is shown in the following image.
solution output subscription example

Application

Login-Data

You need the following data to access the exercise:

  • Host: engine.ie.technikum-wien.at
  • Port: 4840
  • Username: student
  • Password: student
  • Namespace: opc.tcp://engine.ie.technikum-wien.at/OPCExercises

Browsing

Find out which data points can be reached via the node ns=1;s=OPC Exercises. Determine the node IDs in the specified namespace, the display name and the class.

Solution The following nodes are available on the server:
  • ns=3;i=119 -- Sensor 1 -- Variable
  • ns=3;i=316 -- Sensor 2 -- Variable
  • ns=3;i=317 -- Sensor 3 -- Variable
  • ns=3;i=318 -- Test-Method -- Method
  • ns=3;i=320 -- Convert String -- Method
This can be determined with the following example solution:
#!/usr/bin/env python3
"""
This is an example-solution
"""
from opcua import Client

if __name__ == "__main__":
    CLIENT = Client("opc.tcp://student:student@engine.ie.technikum-wien.at:4840/")
    try:
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExercises")
        print("Namespace Index of Exercises: {}".format(IDX))
        print("Nodes in ns={}:".format(IDX))
        NODE = CLIENT.get_node("ns=1;s=OPC Exercises")
        NODES = NODE.get_children()
        for node in NODES:
            if node.nodeid.NamespaceIndex != IDX: # Required as we get all children of the node
                continue
            print("{} -- {} -- {}".format(
                node.nodeid.to_string(),             # Get a short printable NodeId
                node.get_display_name().to_string(), # Get a short printable Display Name
                node.get_node_class().name))         # Get a short readbale nodeclass
    finally:
        CLIENT.disconnect()

Reading variables

Output the description of the nodes determined in the previous task. For the variables, read the values of the individual nodes over 10 seconds in 1-second intervals and output the values determined.

Solution The following descriptions and variable values exist for the individual nodes. Note, that the variable values represent random values and, hence, vary.
ns=3;i=119
The sensor reading of an analogue sensor with a range of 4 to 20 mA.
Values: [4.251206457216534, 4.289866899644205, 4.895675476539514, 4.9098246865436375, 5.420688888428982, 5.88977037896077, 6.270517873818549, 7.583171043162588, 8.316529417004658, 8.831909074195444]
ns=3;i=316
The sensor reading of an analogue sensor with a range of -10 to 10 V.
Values: [-2.9375731073518523, -1.7124065024717305, -0.7790857976530966, 0.14439170175511734, 1.2071837310511713, 2.121364889782571, 3.1357310629583113, 4.92733921542168, 5.738893638260663, 6.580431443068198]
ns=3;i=317
The sensor reading of a digital sensor with either true or false
Values: [True, True, True, False, False, True, True, True, False, False]
ns=3;i=318
Method that returns how many times it has been called so far
ns=3;i=320
Method that converts a string. Specifically it flips upper and lowercase
This can be determined with the following example solution:
#!/usr/bin/env python3
"""
This is an example-solution
"""
import time
from opcua import Client
from opcua import ua

if __name__ == "__main__":
    CLIENT = Client("opc.tcp://student:student@engine.ie.technikum-wien.at:4840/")
    try:
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExercises")
        print("Namespace Index of Exercises: {}".format(IDX))
        NODE = CLIENT.get_node("ns=1;s=OPC Exercises")
        NODES = NODE.get_children()
        for node in NODES:
            if node.nodeid.NamespaceIndex != IDX:
                continue
            print("{}: {}".format(node.nodeid.to_string(), node.get_description().to_string()))
            if node.get_node_class() == ua.NodeClass.Variable:
                values = []
                for i in range(10):
                    values.append(node.get_value())
                    time.sleep(1)
                print("Values: {}".format(values))
    finally:
        CLIENT.disconnect()

Analyzing methods

Determine which parameters the methods you found require and which results the methods deliver.

Solution The methods have the following parameters:
ns=3;i=318
  • Output
    1. Calls -- Number of calls to the function
    2. Message -- The number of calls as a printable string message
ns=3;i=320
  • Input
    1. input -- Input string
  • Output
    1. output -- Output string with flipped case
This can be determined with the following example solution:
#!/usr/bin/env python3
"""
This is an example solution
"""
from opcua import Client
from opcua import ua

if __name__ == "__main__":
    CLIENT = Client("opc.tcp://student:student@engine.ie.technikum-wien.at:4840/")
    try:
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExercises")
        print("Namespace Index of Exercises: {}".format(IDX))
        PARENT = CLIENT.get_node("ns=1;s=OPC Exercises")
        for child in PARENT.get_children():
            if child.nodeid.NamespaceIndex != IDX:
                continue
            if child.get_node_class() != ua.NodeClass.Method:
                continue
            print("{}: {}".format(child.nodeid.to_string(), child.get_display_name().to_string()))
            for arg in child.get_children():
                print(arg.get_display_name().to_string())
                for param in arg.get_value():
                    print("{}: {}".format(param.Name, param.Description.to_string()))
    finally:
        CLIENT.disconnect()

Executing methods

Execute all found methods. If you need to provide input, you may choose any input you like.

Solution This can be determined with the following example solution:
#!/usr/bin/env python3
"""
This is an example solution
"""
from opcua import Client

if __name__ == "__main__":
    CLIENT = Client("opc.tcp://student:student@engine.ie.technikum-wien.at:4840/")
    try:
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExercises")
        print("Namespace Index of Exercises: {}".format(IDX))
        METHOD1 = CLIENT.get_node("ns={};i=318".format(IDX))
        METHOD2 = CLIENT.get_node("ns={};i=320".format(IDX))
        PARENT = CLIENT.get_node("ns=1;s=OPC Exercises")
        for idx, res in enumerate(PARENT.call_method(METHOD1)):
            print("{}: {}".format(idx, res))
        RES = PARENT.call_method(METHOD2, "Test String")
        print(RES)
    finally:
        CLIENT.disconnect()

Subscription

Subscribe to the variables for 10 seconds and display the received values afterwards.

Solution This can be determined with the following example solution:
#!/usr/bin/env python3
"""
This is an example-solution
"""
import time
from opcua import Client
from opcua import ua

class SubscriptionHandler(object): # pylint: disable=too-few-public-methods
    """
    Subsrciption handler.
    """
    def __init__(self):
        self.counter = 0
        self.items = {}

    def __del__(self):
        print("Observed {} datachanges".format(self.counter))
        for key, value in self.items.items():
            print("{} ({} values): {}".format(key.nodeid.to_string(), len(value), value))

    def datachange_notification(self, node, value, _):
        """
        Function that's called when data is changed

        Args:
            node: The node id that had a datachange
            value: The current value
            _: The raw data of the notification

        Returns:
            None
        """
        if node not in self.items.keys():
            self.items[node] = []
        self.items[node].append(value)
        self.counter = self.counter + 1

if __name__ == "__main__":
    CLIENT = Client("opc.tcp://student:student@engine.ie.technikum-wien.at:4840/")
    try:
        CLIENT.connect()
        IDX = CLIENT.get_namespace_index("opc.tcp://engine.ie.technikum-wien.at/OPCExercises")
        print("Namespace Index of Exercises: {}".format(IDX))
        NODE = CLIENT.get_node("ns=1;s=OPC Exercises")
        NODES = NODE.get_children()
        HANDLER = SubscriptionHandler()
        SUB = CLIENT.create_subscription(500, HANDLER)
        for child in NODES:
            if child.nodeid.NamespaceIndex != IDX:
                continue
            if child.get_node_class() == ua.NodeClass(2):
                print("{}: {}".format(
                    child.nodeid.to_string(),
                    child.get_description().to_string()))
                SUB.subscribe_data_change(child)
        print("Setup done; sleep 10 Seconds")
        time.sleep(10)
        SUB.delete()
    finally:
        CLIENT.disconnect()

Considerations

After completing the online exercise, you are able to interact with the laboratories. You have learned the basics of the communication protocol and have already tried some of the most important functions. You are now able to…
  • … deal fundamentally with the industrial communication protocol OPC UA.
  • … read out which information is available on a server.
  • … read out individual information points in a targeted manner.
  • … be informed when selected data changes.
  • … execute functions on the remote server.

Self-Evaluation

Which attribute is unique for each node?

The node ID is unique for each node!

Which elements does the node ID contain?

The node ID contains two elements: namespace and identifier

Which information does the node ID example ns=2;b=aGVsbG8gd29ybGQ= contain?

On the one hand, the namespace index 2 can be retrieved. On the other hand, the identifier based on the Base64 format is given. The Base64 format is denoted by the identifier type b. Note: In this case, the content of the identifier can be converted into a human-readable text by online tools!

What's the usage of the node class VariableType?

VariableType is used to give a data element a meaning. Thereby, the data element can be interpreted.

Which components does a reference contain?

A reference always contains three components: source, target and reference type

What's the usage of subscriptions?

In combination with monitored items, subscriptions are used in order to monitor nodes. Thereby, data changes of certain nodes can be sent periodically and under pre-defined circumstances.

Which encryption types do exist in order to encrypt the communication between the server and the client?

  • None - The communication is not encrypted.
  • Basic128RSA15
  • Basic256

Take-Home-Messages

  • Each node is characterized by its NodeID explicitly
  • Additionally available information of a node are defined by its node class
  • The node class Object is used for the creation of further (sub) systems
  • The node class Variable is used in order to define the node's content
  • The node class methods describes functions executable on the server
  • References are used in order to relate nodes to each other
  • Subscriptions and Monitored Items allow to monitor nodes

Further Topics

  • Client-Side Node Management
  • Server implementation
  • Data Modeling

This site uses cookies

Cookies help us to improve your browsing experience and analyze site traffic. Find out more on how we use cookies.
I accept cookies
I refuse cookies