C++ Based OPC UA Client/Server SDK  1.5.5.355
Lesson 2: Extending the Address Space with Real World Data

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.

Preliminary Note

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

l4gettingstartedlesson02_controller_objecttypeall.png

Figure 2-2 provides an overall view of the classes (and their dependencies), which will be implemented in the follwing steps of this lesson:

Step 1: Creating a New NodeManager
Class NmBuildingAutomation
Step 3: Creating the ControllerObject Class
Class ControllerObject
Step 4: Creating the AirConditionerControllerObject
Class AirConditionerControllerObject
Step 5: Creating the FurnaceControllerObject
Class FurnaceControllerObject

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.

Figure 2-2 Class Overview

l4gettingstartedlesson02_classdiagramm.png

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.

Step 1: Creating a New NodeManager

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.

NmBuildingAutomation Class Definition

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

l4gettingstartedlesson02_nodemanagerclassdiagramm.png

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.

#ifndef __NMBUILDINGAUTOMATION_H__
#define __NMBUILDINGAUTOMATION_H__
#include "nodemanagerbase.h"
class NmBuildingAutomation : public NodeManagerBase
{
UA_DISABLE_COPY(NmBuildingAutomation);
public:
NmBuildingAutomation();
virtual ~NmBuildingAutomation();
UaVariable* getInstanceDeclarationVariable(OpcUa_UInt32 numericIdentifier);
private:
UaStatus createTypeNodes();
};
#endif // __NMBUILDINGAUTOMATION_H__

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.

NmBuildingAutomation Class Implementation

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().

#include "nmbuildingautomation.h"
NmBuildingAutomation::NmBuildingAutomation()
: NodeManagerBase("urn:UnifiedAutomation:CppDemoServer:BuildingAutomation")
{
}

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.

NmBuildingAutomation::~NmBuildingAutomation()
{
}

In the destructor you can add cleanup code later. For now, this is empty.

UaStatus NmBuildingAutomation::afterStartUp()
{
UaStatus ret;
createTypeNodes();
return ret;
}

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.

UaStatus NmBuildingAutomation::beforeShutDown()
{
UaStatus ret;
return ret;
}

In beforeShutDown() you could implement any cleanup code. The created nodes are cleaned up automatically, so this can stay empty here.

UaStatus NmBuildingAutomation::createTypeNodes()
{
UaStatus ret;
UaStatus addStatus;
UaObjectTypeSimple* pControllerType = NULL;
/**************************************************************
* Create the Controller Type
*/
// Add ObjectType "ControllerType"
pControllerType = new UaObjectTypeSimple(
"ControllerType", // Used as string in browse name and display name
UaNodeId(Ba_ControllerType, getNameSpaceIndex()), // Numeric NodeId for types
m_defaultLocaleId, // Defaul LocaleId for UaLocalizedText strings
OpcUa_True); // Abstract object type -> can not be instantiated
return ret;
}

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:

/************************************************************
Controller Type and its instance declaration
*/
// Controller Type
#define Ba_ControllerType 1000
// Instance declaration
#define Ba_ControllerType_State 1001
#define Ba_ControllerType_Temperature 1002
#define Ba_ControllerType_TemperatureSetPoint 1003
#define Ba_ControllerType_PowerConsumption 1004
#define Ba_ControllerType_Start 1006
#define Ba_ControllerType_Stop 1007
#define Ba_ControllerType_Temperature_EURange 1008
#define Ba_ControllerType_Temperature_EngineeringUnits 1009
#define Ba_ControllerType_TemperatureSetPoint_EURange 1010
#define Ba_ControllerType_TemperatureSetPoint_EngineeringUnits 1011
/************************************************************/

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

#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h" // Add this line

and add the following code to createTypeNodes():

...
// Add ObjectType "ControllerType"
pControllerType = new UaObjectTypeSimple(
"ControllerType", // Used as string in browse name and display name
UaNodeId(Ba_ControllerType, getNameSpaceIndex()), // Numeric NodeId for types
m_defaultLocaleId, // Defaul LocaleId for UaLocalizedText strings
OpcUa_True); // Abstract object type -> can not be instantiated
// New code begin
// Add new node to address space by creating a reference from BaseObjectType to this new node
addStatus = addNodeAndReference(OpcUaId_BaseObjectType, pControllerType, OpcUaId_HasSubtype);
UA_ASSERT(addStatus.isGood());
// New code end

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.

UaStatus NmBuildingAutomation::createTypeNodes()
{
UaStatus ret;
UaStatus addStatus;
UaVariant defaultValue; // Add this line
UaObjectTypeSimple* pControllerType = NULL;
OpcUa::BaseDataVariableType* pDataVariable; // Add this line
...
// Add new node to address space by creating a reference from BaseObjectType to this new node
addStatus = addNodeAndReference(OpcUaId_BaseObjectType, pControllerType, OpcUaId_HasSubtype);
UA_ASSERT(addStatus.isGood());
// New code begin
/***************************************************************
* Create the Controller Type Instance declaration
*/
// Add Variable "State" as BaseDataVariable
defaultValue.setUInt32(0);
pDataVariable = new OpcUa::BaseDataVariableType(
UaNodeId(Ba_ControllerType_State, getNameSpaceIndex()), // NodeId of the Variable
"State", // Name of the Variable
getNameSpaceIndex(), // Namespace index of the browse name (same like NodeId)
defaultValue, // Initial value
Ua_AccessLevel_CurrentRead, // Access level
this); // Node manager for this variable
// Set Modelling Rule to Mandatory
pDataVariable->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pDataVariable, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code end

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”.

...
UaVariant defaultValue;
UaObjectTypeSimple* pControllerType = NULL;
OpcUa::DataItemType* pDataItem; // Add this line
OpcUa::AnalogItemType* pAnalogItem; // Add this line
...
// Add Variable "State" as BaseDataVariable
...
UA_ASSERT(addStatus.isGood());
// New code begins
// Add Variable "Temperature" as AnalogItem
defaultValue.setDouble(0);
pAnalogItem = new OpcUa::AnalogItemType(
UaNodeId(Ba_ControllerType_Temperature, getNameSpaceIndex()),
"Temperature",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
pAnalogItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
UaRange tempRange(0, 100);
pAnalogItem->setEURange(tempRange);
UaEUInformation tempEUInformation("", -1, UaLocalizedText("en", "\xc2\xb0\x46") /* °F */, UaLocalizedText("en", "Degrees Fahrenheit"));
pAnalogItem->setEngineeringUnits(tempEUInformation);
// Add Variable "TemperatureSetPoint" as AnalogItem
defaultValue.setDouble(0);
pAnalogItem = new OpcUa::AnalogItemType(
UaNodeId(Ba_ControllerType_TemperatureSetPoint, getNameSpaceIndex()),
"TemperatureSetPoint",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead | Ua_AccessLevel_CurrentWrite,
this);
pAnalogItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
pAnalogItem->setEURange(tempRange);
pAnalogItem->setEngineeringUnits(tempEUInformation);
// Add Variable "PowerConsumption"
defaultValue.setDouble(0);
pDataItem = new OpcUa::DataItemType(
UaNodeId(Ba_ControllerType_PowerConsumption, getNameSpaceIndex()),
"PowerConsumption",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
pDataItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
//New code ends

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().

UaVariable* NmBuildingAutomation::getInstanceDeclarationVariable(OpcUa_UInt32 numericIdentifier)
{
// Try to find the instance declaration node with the numeric identifier
// and the namespace index of this node manager
UaNode* pNode = findNode(UaNodeId(numericIdentifier, getNameSpaceIndex()));
if ( (pNode != NULL) && (pNode->nodeClass() == OpcUa_NodeClass_Variable) )
{
// Return the node if valid and a variable
return (UaVariable*)pNode;
}
else
{
return NULL;
}
}

Step 2: Integrating the New NodeManger into the UA Server

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.

#include "opcserver.h"
#include "uaplatformlayer.h"
#include "uathread.h"
#include "xmldocument.h"
#include "shutdown.h"
#include "nmbuildingautomation.h" // <-- New NodeManager; add this line

The next step is the instantiation of the NodeManager.

int OpcServerMain(const char* szAppPath)
{
...
if ( ret == 0 )
{
...
// Create and initialize server object
OpcServer* pServer = new OpcServer;
pServer->setServerConfig(sConfigFileName, szAppPath);
// New code begins
// Add NodeManager for the server specific nodes
NmBuildingAutomation *pMyNodeManager = new NmBuildingAutomation();
pServer->addNodeManager(pMyNodeManager);
// New code ends
// Start server object
ret = pServer->start();

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.

Step 3: Creating the ControllerObject Class

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.

ControllerObject Class Definition

Add a new file named “controllerobject.h” to your project and add the following code:

#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
class NmBuildingAutomation;
class ControllerObject :
public UaObjectBase
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(const UaString& name, const UaNodeId& newNodeId, const UaString& defaultLocaleId, NmBuildingAutomation* pNodeManager);
virtual ~ControllerObject(void);
OpcUa_Byte eventNotifier() const;
protected:
UaMutexRefCounted* m_pSharedMutex;
};
#endif

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.

ControllerObject Class Implementation

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:

#include "controllerobject.h"
#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "opcua_analogitemtype.h"
ControllerObject::ControllerObject(const UaString& name, const UaNodeId& newNodeId, const UaString& defaultLocaleId, NmBuildingAutomation* pNodeManager)
: UaObjectBase(name, newNodeId, defaultLocaleId),
m_pSharedMutex(NULL)
{
// Use a mutex shared across all variables of this object
m_pSharedMutex = new UaMutexRefCounted;
UaVariable* pInstanceDeclaration;
OpcUa::DataItemType* pDataItem;
OpcUa::AnalogItemType* pAnalogItem;
UaStatus addStatus;
/**************************************************************
* Create the Controller components
*/
// Add Variable "State"
// Get the instance declaration node used as base for this variable instance
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_State);
UA_ASSERT(pInstanceDeclaration!=NULL);
pDataVariable = new OpcUa::BaseDataVariableType(
this, // Parent node
pInstanceDeclaration, // Instance declaration variable this variable instance is based on
pNodeManager, // Node manager responsible for this variable
m_pSharedMutex); // Shared mutex used across all variables of this object
addStatus = pNodeManager->addNodeAndReference(this, pDataVariable, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Add Variable "Temperature"
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_Temperature);
UA_ASSERT(pInstanceDeclaration!=NULL);
// Create new variable and add it as component to this object
pAnalogItem = new OpcUa::AnalogItemType(this, pInstanceDeclaration, pNodeManager, m_pSharedMutex);
addStatus = pNodeManager->addNodeAndReference(this, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
UaRange tempRange(0, 100);
pAnalogItem->setEURange(tempRange);
UaEUInformation tempEUInformation("", -1, UaLocalizedText("en", "\xc2\xb0\x46") /* °F */, UaLocalizedText("en", "Degrees Fahrenheit"));
pAnalogItem->setEngineeringUnits(tempEUInformation);
// Add Variable "TemperatureSetPoint"
// Get the instance declaration node used as base for this variable instance
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_TemperatureSetPoint);
UA_ASSERT(pInstanceDeclaration!=NULL);
// Create new variable and add it as component to this object
pAnalogItem = new OpcUa::AnalogItemType(this, pInstanceDeclaration, pNodeManager, m_pSharedMutex);
addStatus = pNodeManager->addNodeAndReference(this, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
pAnalogItem->setEURange(tempRange);
pAnalogItem->setEngineeringUnits(tempEUInformation);
// Add Variable "PowerConsumption"
// Get the instance declaration node used as base for this variable instance
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_PowerConsumption);
UA_ASSERT(pInstanceDeclaration!=NULL);
// Create new variable and add it as component to this object
pDataItem = new OpcUa::DataItemType(this, pInstanceDeclaration, pNodeManager, m_pSharedMutex);
addStatus = pNodeManager->addNodeAndReference(this, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
}

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.

ControllerObject::~ControllerObject(void)
{
if ( m_pSharedMutex )
{
// Release our local reference
m_pSharedMutex->releaseReference();
m_pSharedMutex = NULL;
}
}

The destructor is releasing the shared mutex reference of this class.

OpcUa_Byte ControllerObject::eventNotifier() const
{
return Ua_EventNotifier_None;
}

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.

Step 4: Creating the AirConditionerControllerObject

AirConditionerControllerObject Class Definition

Add a new header file named “airconditionercontrollerobject.h” to your project containing the following code:

#ifndef __AIRCONDITIONERCONTROLLEROBJECT_H__
#define __AIRCONDITIONERCONTROLLEROBJECT_H__
#include "controllerobject.h"
class AirConditionerControllerObject :
public ControllerObject
{
public:
AirConditionerControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager);
virtual ~AirConditionerControllerObject(void);
virtual UaNodeId typeDefinitionId() const;
};
#endif

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.

AirConditionerControllerObject Class Implementation

Add a new source file named “airconditionercontrollerobject.cpp” to your project containing the following code:

#include "airconditionercontrollerobject.h"
#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "opcua_analogitemtype.h"
AirConditionerControllerObject::AirConditionerControllerObject(
const UaString& name,
const UaNodeId& newNodeId,
const UaString& defaultLocaleId,
NmBuildingAutomation* pNodeManager)
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager)
{
UaVariable* pInstanceDeclaration;
OpcUa::AnalogItemType* pAnalogItem;
UaStatus addStatus;
/**************************************************************
* Create the AirConditionerController components
*/
// Add Variable "Humidity"
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_AirConditionerControllerType_Humidity);
UA_ASSERT(pInstanceDeclaration!=NULL);
// Create new variable and add it as component to this object
pAnalogItem = new OpcUa::AnalogItemType(
this, // Parent node
pInstanceDeclaration, // Instance declaration variable this variable instance is based on
pNodeManager, // Node manager responsible for this variable
m_pSharedMutex); // Shared mutex used across all variables of this object
addStatus = pNodeManager->addNodeAndReference(this, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
UaRange tempRange(0, 100);
pAnalogItem->setEURange(tempRange);
UaEUInformation tempEUInformation("", -1, UaLocalizedText("en", "%"), UaLocalizedText("en", "Percent"));
pAnalogItem->setEngineeringUnits(tempEUInformation);
// Add Variable "HumiditySetpoint"
// Get the instance declaration node used as base for this variable instance
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_AirConditionerControllerType_HumiditySetpoint);
UA_ASSERT(pInstanceDeclaration!=NULL);
// Create new variable and add it as component to this object
pAnalogItem = new OpcUa::AnalogItemType(this, pInstanceDeclaration, pNodeManager, m_pSharedMutex);
addStatus = pNodeManager->addNodeAndReference(this, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
pAnalogItem->setEURange(tempRange);
pAnalogItem->setEngineeringUnits(tempEUInformation);
}

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.

AirConditionerControllerObject::~AirConditionerControllerObject(void)
{
}

For now, the destructor is empty.

UaNodeId AirConditionerControllerObject::typeDefinitionId() const
{
UaNodeId ret(Ba_AirConditionerControllerType, browseName().namespaceIndex());
return ret;
}

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.

/************************************************************
AirConditioner Controller Type and its instance declaration
*/
// AirConditioner Controller Type
#define Ba_AirConditionerControllerType 2000
// Instance declaration
#define Ba_AirConditionerControllerType_State 2001
#define Ba_AirConditionerControllerType_Humidity 2002
#define Ba_AirConditionerControllerType_HumiditySetpoint 2003
#define Ba_AirConditionerControllerType_StartWithSetpoint 2004
#define Ba_AirConditionerControllerType_StartWithSetpoint_In 2005
/************************************************************/

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.

UaStatus NmBuildingAutomation::createTypeNodes()
{
UaStatus ret;
UaStatus addStatus;
UaVariant defaultValue;
UaObjectTypeSimple* pControllerType = NULL;
UaObjectTypeSimple* pAirConditionerControllerType = NULL; // Add this line
OpcUa::DataItemType* pDataItem;
OpcUa::AnalogItemType* pAnalogItem;
...
// Add Variable "PowerConsumption"
defaultValue.setDouble(0);
pDataItem = new OpcUa::DataItemType(
UaNodeId(Ba_ControllerType_PowerConsumption, getNameSpaceIndex()),
"PowerConsumption",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
pDataItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pControllerType, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code begins
/**************************************************************
* Create the AirConditionerController Type
*/
pAirConditionerControllerType = new UaObjectTypeSimple(
"AirConditionerControllerType", // Used as string in browse name and display name
UaNodeId(Ba_AirConditionerControllerType, getNameSpaceIndex()), // Numeric NodeId for types
m_defaultLocaleId,
OpcUa_False);
// Add Object Type node to address space and create reference to Controller type
addStatus = addNodeAndReference(pControllerType, pAirConditionerControllerType, OpcUaId_HasSubtype);
UA_ASSERT(addStatus.isGood());
/***************************************************************
* Create the AirConditionerController Type Instance declaration
*/
// Add Variable "Humidity"
defaultValue.setDouble(0);
pAnalogItem = new OpcUa::AnalogItemType(
UaNodeId(Ba_AirConditionerControllerType_Humidity, getNameSpaceIndex()),
"Humidity",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
pAnalogItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pAirConditionerControllerType, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
pAnalogItem->setEURange(tempRange);
UaEUInformation tempEUPercent("", -1, UaLocalizedText("en", "%"), UaLocalizedText("en", "Percent"));
pAnalogItem->setEngineeringUnits(tempEUPercent);
// Add Variable "HumiditySetpoint"
defaultValue.setDouble(0);
pAnalogItem = new OpcUa::AnalogItemType(
UaNodeId(Ba_AirConditionerControllerType_HumiditySetpoint, getNameSpaceIndex()),
"HumiditySetpoint",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead | Ua_AccessLevel_CurrentWrite,
this);
pAnalogItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pAirConditionerControllerType, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
pAnalogItem->setEURange(tempRange);
pAnalogItem->setEngineeringUnits(tempEUPercent);
// New code ends

Step 5: Creating the FurnaceControllerObject

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:

#ifndef __FURNACECONTROLLEROBJECT_H__
#define __FURNACECONTROLLEROBJECT_H__
#include "controllerobject.h"
class FurnaceControllerObject :
public ControllerObject
{
public:
FurnaceControllerObject(const UaString& name, const UaNodeId& newNodeId, const UaString& defaultLocaleId, NmBuildingAutomation* pNodeManager);
virtual ~FurnaceControllerObject(void);
virtual UaNodeId typeDefinitionId() const;
};
#endif

Add a new source file named “furnacecontrollerobject.cpp” to your project containing the following code:

#include "furnacecontrollerobject.h"
#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "opcua_analogitemtype.h"
FurnaceControllerObject::FurnaceControllerObject(const UaString& name, const UaNodeId& newNodeId, const UaString& defaultLocaleId, NmBuildingAutomation* pNodeManager)
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager)
{
UaVariable* pInstanceDeclaration;
OpcUa::DataItemType* pDataItem;
UaStatus addStatus;
/**************************************************************
* Create the FurnaceController components
*/
// Add Variable "GasFlow"
// Get the instance declaration node used as base for this variable instance
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_FurnaceControllerType_GasFlow);
UA_ASSERT(pInstanceDeclaration!=NULL);
pDataItem = new OpcUa::DataItemType(
this, // Parent node
pInstanceDeclaration, // Instance declaration variable this variable instance is based on
pNodeManager, // Node manager responsible for this variable
m_pSharedMutex); // Shared mutex used across all variables of this object
addStatus = pNodeManager->addNodeAndReference(this, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
}
FurnaceControllerObject::~FurnaceControllerObject(void)
{
}
UaNodeId FurnaceControllerObject::typeDefinitionId() const
{
UaNodeId ret(Ba_FurnaceControllerType, browseName().namespaceIndex());
return ret;
}

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.

/************************************************************
Furnace Controller Type and its instance declaration
*/
// Furnace Controller Type
#define Ba_FurnaceControllerType 3000
// Instance declaration
#define Ba_FurnaceControllerType_State 3001
#define Ba_FurnaceControllerType_GasFlow 3002
#define Ba_FurnaceControllerType_StartWithSetpoint 3003
#define Ba_FurnaceControllerType_StartWithSetpoint_In 3004
/************************************************************/

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.

UaStatus NmBuildingAutomation::createTypeNodes()
{
UaStatus ret;
UaStatus addStatus;
UaVariant defaultValue;
UaObjectTypeSimple* pControllerType = NULL;
UaObjectTypeSimple* pAirConditionerControllerType = NULL;
UaObjectTypeSimple* pFurnaceControllerType = NULL; // Add this line
OpcUa::DataItemType* pDataItem;
OpcUa::AnalogItemType* pAnalogItem;
...
// Add Variable "HumiditySetpoint"
defaultValue.setDouble(0);
pAnalogItem = new OpcUa::AnalogItemType(
UaNodeId(Ba_AirConditionerControllerType_HumiditySetpoint, getNameSpaceIndex()),
"HumiditySetpoint",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead | Ua_AccessLevel_CurrentWrite,
this);
pAnalogItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pAirConditionerControllerType, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// Set property values - the property nodes are already created
pAnalogItem->setEURange(tempRange);
pAnalogItem->setEngineeringUnits(tempEUPercent);
// New code begins
/**************************************************************
* Create the FurnaceController Type
*/
pFurnaceControllerType = new UaObjectTypeSimple(
"FurnaceControllerType", // Used as string in browse name and display name
UaNodeId(Ba_FurnaceControllerType, getNameSpaceIndex()), // Numeric NodeId for types
m_defaultLocaleId,
OpcUa_False);
// Add Object Type node to address space and create reference to Controller type
addStatus = addNodeAndReference(pControllerType, pFurnaceControllerType, OpcUaId_HasSubtype);
UA_ASSERT(addStatus.isGood());
/**************************************************************
* Create the FurnaceController Type Instance declaration
*/
// Add Variable "GasFlow"
defaultValue.setDouble(0);
pDataItem = new OpcUa::DataItemType(
UaNodeId(Ba_FurnaceControllerType_GasFlow, getNameSpaceIndex()),
"GasFlow",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead,
this);
pAnalogItem->setModellingRuleId(OpcUaId_ModellingRule_Mandatory);
addStatus = addNodeAndReference(pFurnaceControllerType, pDataItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
// New code ends

Step 6: Creating Controller Objects

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.

#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
// New code begin
#include "airconditionercontrollerobject.h"
#include "furnacecontrollerobject.h"
#include "opcua_analogitemtype.h"
// New code end
...
UaStatus NmBuildingAutomation::afterStartUp()
{
UaStatus ret;
// New code begin
AirConditionerControllerObject *pAirConditioner;
FurnaceControllerObject *pFurnace;
UaString sName;
// New code end
createTypeNodes();
// New code begin
sName = "AirConditioner1";
pAirConditioner = new AirConditionerControllerObject(
sName,
UaNodeId(sName, getNameSpaceIndex()),
m_defaultLocaleId,
this
);
ret = addNodeAndReference(OpcUaId_ObjectsFolder, pAirConditioner, OpcUaId_Organizes);
UA_ASSERT(ret.isGood());
sName = "Furnace1";
pFurnace = new FurnaceControllerObject(
sName,
UaNodeId(sName, getNameSpaceIndex()),
m_defaultLocaleId,
this
);
ret = addNodeAndReference(OpcUaId_ObjectsFolder, pFurnace, OpcUaId_Organizes);
UA_ASSERT(ret.isGood());
// New code end
return ret;
}

Step 7: Run Application

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

serverlesson02_expert.png