C++ Based OPC UA Client/Server/PubSub SDK
1.7.0.449
|
This lesson explains how to create an OPC UA Server Address Space in order to describe a real world system with UA. The lesson is based on a simple building automation scenario including air conditioning and furnace control.
The lesson requires basic knowledge of OPC UA. See OPC UA Fundamentals—especially Address Space Concepts and OPC UA Node Classes—in order to become familiar with OPC UA concepts and terms.
Real world example
The real world example used for this getting started is a temperature controller with two different specialized types, an air conditioner controller and a furnace controller. Both controllers are providing state, temperature and temperature setpoint variables. Since both controllers share some variables, the object type representation defines a base ControllerType and the two derived object types AirConditionerControllerType and FurnaceControllerType. Figure 2-1 shows the object types and their variables in the OPC UA notation.
This lesson creates only the object types and object instances with data variables and properties. Additional features like methods and events are added in the following lessons.
Figure 2-1 gives you an overview of the OPC UA object types ControllerType, AirConditionerControllerType and FurnaceControllerType. The ControllerType is directly derived from the BaseObjectType. Instances of the AirConditionerControllerType and the FurnaceControllerType contain also the variables defined on the ControllerType. The variables Temperature, TemperatureSetPoint, Humidity and HumiditySetPoint use the AnalogItemType which defines the properties EURange and EngineeringUnits. The variable types of the different variable components are indicated in the figure.
Figure 2-1 The Controller Object Type and the derived types
Figure 2-2 provides an overall view of the classes (and their dependencies), which will be implemented in the follwing steps of this lesson:
The classes implemented in this lesson are white. The yellow and green classes are provided by the SDK. The green ones are interfaces for the different node classes defined by OPC UA. The yellow classes are implementations of these interfaces and the interfaces NodeManager and IOManager are provided by the SDK. The interface NodeManager provides access to the nodes of the address space by UA services like Browse. The IOManager interface defines methods for Read, Write and Monitoring access to the attribute values of the nodes.
The class NmBuildingAutomation implements the interfaces NodeManager and IOManager by deriving from the SDK class NodeManagerBase. This covers already all services necessary for OPC Data Access functionality. The objects and variables providing the information about the controllers are created in this class and are then managed by the SDK class NodeManagerUaNode.
The classes ControllerObject, AirConditionerControllerObject and FurnaceControllerObject are representing the different controller types of the real world example by implementing the interface UaObject and by aggregating variables based on the UaVariable interface. This implementation classes are using classes provided by the SDK for the interface implementation.
NodeManagers are responsible for managing a set of nodes for an OPC UA Namespace. They provide methods to add and remove Nodes and References, they implement the OPC UA Browse Services and they resolve NodeIds to the IO information that IOManagers need for accessing the underlying data source.
For our example, we create a new class NmBuildingAutomation which inherits from NodeManagerBase, which is derived from NodeManagerUaNode and IOManagerUaNode. These two classes are two specialized classes which implement the SDK interfaces NodeManager, NodeManagerConfig and IOManager. These classes give you a default implementation for these SDK interfaces which work on an in-memory UA Node Model.
NodeManagerUaNode provides you with an implementation of NodeManagerConfig used to add and remove Nodes and References.
Figure 2-3 shows the class hierarchy and class members in detail.
Figure 2-3 NodeManager Class Hierarchy
IOManagerUaNode implements IOManager. For details about IOManager and IOManagerUaNode see Lesson 3: Connecting the Nodes to Real Time Data. By inheriting from both—NodeManagerUaNode and IOManagerUaNode —we get a class which is a NodeManager and an IOManager.
Add a new header file named “nmbuildingautomation.h” containing the following code to your project.
The class definition defines constructor and destructor and the two pure virtual functions afterStartUp and beforeShutDown of NodeManagerBase that we are going to implement. Additionally we define the method getInstanceDeclarationVariable that returns instance declaration nodes and a private createTypeNodes() method that will create our Type Model in the Address Space.
The macro UA_DISABLE_COPY disables copying of the constructor and the assignment operator of this class to avoid misuses of this class.
Add a new source file named “nmbuildingautomation.cpp” to your project.
Implementing this class is straight forward. First we have to implement the two pure virtual functions from NodeManager, i.e. afterStartup() and beforeShutdown().
The construtor takes three parameters which initialize its base class. The first parameter is the NamespaceURI, for which this NodeManager is responsible for. For a description of the optional parameters see the documentation of NodeManagerBase::NodeManagerBase.
In the destructor you can add cleanup code later. For now, this is empty.
The afterStartUp() method is called right after the NodeManager has been created and intialized. Here we can create our UaNodes. For now, we only call our private createTypeNodes() method that will create our Type Model for this NodeManager.
In beforeShutDown() you could implement any cleanup code. The created nodes are cleaned up automatically, so this can stay empty here.
createTypeNodes() creates a server-specific type model. This is done by defining and adding Type Nodes to the Address Space. Type Nodes represent more or less complex ObjectTypes. The latter are defined once by the server and can be used in several places. Several instances of the ObjectType can be instantiated, using the AddNodes Service. A server exposing complex ObjectTypes (and instances of that type) gives clients the possibility to program their application with knowledge of the type information and use it on all instances.
As a start, createTypeNodes() adds the ObjectType “ControllerType”. In our example, BrowseName and DisplayName have the same string and only one language is supported for the Type system. Thus we can use the class UaObjectTypeSimple.
The NodeId for this Type can be numeric, because our Type system is static. We use a define which is created in a new header file named buildingautomationtypeids.h. Add the following code to this file:
This header also contains defines of the InstanceDeclarations according to ControllerType. InstanceDeclarations will be introduced in the following.
Include the new header file in nmbuildingautomation.cpp
and add the following code to createTypeNodes():
addNodeAndReference() adds the ObjectType Node to the Address Space and creates a HasSubtype Reference to the ObjectType BaseObjectType.
In the following code snippets, Variables are added to the Address Space.
This code section creates and adds the Variable “State” to the Address Space. This Variable is one of the entities, so-called InstanceDeclarations, used to define an ObjectType. InstanceDeclarations are defined as Variables, Objects, and Methods exposed beneath the ObjectType. Note that InstanceDeclarations are typically added with HasComponent or HasProperty.
This code section creates and adds the remaining Variables “Temperature”, “TemperatureSetPoint”, and “PowerConsumption”.
The two analog items Temperature and TemperatureSetPoint provide additional properties providing engineering information like range and units. The property nodes are created automatically. The values are set by using the methods setEURange and setEngineeringUnits.
Finally we are implementing getInstanceDeclarationVariable(). This method returns the instance declaration node for the numeric NodeId which is passed in. These nodes have been created earlier in NmBuildingAutomation::createTypeNodes().
To integrate the new NodeManager into the server it must be instantiated from OpcServerMain. First open servermain.cpp and add the include to your new NodeManager to the existing list of includes.
The next step is the instantiation of the NodeManager.
After the NodeManager has been created you have to pass it to the addNodeManager() method. Thus the NodeManager will be integrated into the server’s Address Space. In this context a new entry in the server’s NameSpaceTable will be created automatically.
We have already created ControllerType Nodes in NmBuildingAutomation::createTypeNodes() which define how an OPC UA ControllerType looks like.
We also need a C++ class which implements the functionality of the OPC UA ControllerType instances. We call this class ControllerObject. To be precise, this class will be an abstract class, because the OPC UA ControllerType is declared as abstract, too. This means that this class will never be instantiated, but will serve as base class for derived OPC UA ControllerType subclasses.
Add a new file named “controllerobject.h” to your project and add the following code:
The class definition defines constructor, destructor, and eventNotifier(), a pure virtual function of UaObject that we are going to implement. The macro UA_DISABLE_COPY disables copying of the constructor and the assignment operator of this class to avoid misuses of this class.
We implement the pure virtual function eventNotifier() of UaObject in the abstract base class since it returns the same value for both concrete types, whereas the pure virtual function typeDefinition() of UaNode remains being unimplemented, which results in the class ControllerObject being abstract, too. The typeDefinition() returns different NodeIds for the two derived classes.
Add a new file named “controllerobject.cpp” to your project containing the following code:
The first three parameters to be passed to the constructor initialize its base class. The first parameter is the name of the ControllerObject used in BrowseName and DisplayName. The remaining ones are the NodeId and the default localeId of the object, respectively. The last parameter permits access to NodeManagerUaNode functionality, in particular addNodeAndReference().
In addition, the constructor creates the components of ControllerObject similarly to the InstanceDeclarations of the “ControllerType” ObjectType.
The destructor is releasing the shared mutex reference of this class.
eventnotifier() returns the value of the EventNotifier attribute. Events are introduced in Lesson 5: Adding Support for Events.
The next two steps of this tutorial demonstrate the implementation of classes which inherit from ControllerObject and can be instantiated.
Add a new header file named “airconditionercontrollerobject.h” to your project containing the following code:
The class definition defines constructor, destructor, and typeDefinitionId(). The macro UA_DISABLE_COPY disables copying of the constructor and the assignment operator of this class.
Add a new source file named “airconditionercontrollerobject.cpp” to your project containing the following code:
The constructor demands four parameters being passed to its base class. It also creates the components of ControllerObject similarly to the InstanceDeclarations of “ControllerType” ObjectType.
For now, the destructor is empty.
typeDefinitionId() returns the TypeDefinition NodeId using the Ba_AirConditionerControllerType define used for the numeric NodeId of the object type.
The defines for the AirConditioner object type and its instance declaration nodes are added with the following code to buildingautomationtypeids.h. The type nodes are static and therefore use the efficient numeric NodeIds. The defines are created to improve the readability of the code.
This code also adds defines of AirConditionerControllerType Instance declarations.
Add AirConditionerControllerType and Variables to the Address Space by adding the following code to NmBuildingAutomation::createTypeNodes(). The code creates the object type node AirConditionerControllerType and the two variable nodes Humidity and HumiditySetpoint defined by this object type.
We are going to define and implement the class FurnaceControllerObject as we did for AirConditionerControllerObject. Add a new header file named “furnacecontrollerobject.h” to your project containing the following code:
Add a new source file named “furnacecontrollerobject.cpp” to your project containing the following code:
The defines for the Furnace controller object type and its instance declaration nodes are added with the following code to buildingautomationtypeids.h. The type nodes are static and therefore use the efficient numeric NodeIds. The defines are created to make the code better readable.
Add FurnaceControllerType and Variables to the Address Space by adding the following code to NmBuildingAutomation::createTypeNodes(). The code creates the object type node FurnaceControllerType and the variable node GasFlow defined by this object type.
Finally, we have to include the necessary header files in nmbuildingautomation.cpp and complete NmBuildingAutomation::afterStartUp() in order to create controller instances as Objects beneath the Objects Folder.
Compile and run the server application.
When connecting to the server with UaExpert, the Objects and ObjectTypes created in this lesson appear in the Address Space (see screenshot below).
Figure 2-4 Objects and Object Types in UaExpert