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.
Literature
- 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
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
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.
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.
-
Event Note
-
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.
-
DataType
-
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.
-
Executable
-
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.
-
isAbstract
-
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.
-
isAbstract
-
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.
-
DataType
-
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.
-
isAbstract
-
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.
-
ContainsNoLoop
References and Datatypes
References
References are used to relate nodes to each other.
A reference always has three components:
- Source
- Target
- Reference type
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.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
-
qn=0;Root
-
qn=0;Objects
-
qn=0;Server
- qn=0;NamespaceArray
- qn=0;ServerArray
-
qn=0;Server
-
qn=0;Types
- qn=0;DataTypes
- qn=0;ObjectTypes
- qn=0;ReferenceTypes
- qn=0;VariableTypes
- qn=0;Views
-
qn=0;Objects
-
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.
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
- 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.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.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.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. 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.
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:
Connection without User Credentials
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))
#!/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 only difference to the previous example is that the connection is established with user credentials.
The following basic data are necessary for connecting:
Connection with User Credentials
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 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:
Browsing
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))
#!/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
#!/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 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:
Reading
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 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.
Subscribing
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:
After creating an instance of the class, a counter is initialized.
The counter saves the number of value changes.
If an instance of the class is deleted, a notification with the number of value changes will be exported.
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
#!/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()
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
#!/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
#!/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
- Calls -- Number of calls to the function
- Message -- The number of calls as a printable string message
- ns=3;i=320
-
- Input
- input -- Input string
- Output
- output -- Output string with flipped case
#!/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
Links and Literaure
- explanation OPC-UA
- explanation OPC
- OPC-UA library C/C++
- OPC-UA library Python
- OPC-UA library Node.js
- OPC-UA library Matlab
- Base64 coding
- Extensible Markup Language (XML)
- installation guide OPC-UA library in Python
- Python OPC-UA documentation
Further Topics
- Client-Side Node Management
- Server implementation
- Data Modeling