In the previous Lesson 2: Extending the Address Space with Real World Data we created a nice object oriented address space, but the data provided by this address space are only initial values. There is no connection to real time data implemented yet.
If the source of the real time data delivers data changes through an event based mechanism, the connection to the data source is very simple. The only thing that needs to be implemented is the call to UaVariable::setValue on the variable nodes if a data change arrives for this variable. All the read and data monitoring is already handled by the SDK. An example for this update mechanism can be found in the Hello World server example → Create a variable in the server’s address space → Simulate Data.
The application specific node manager implementation gets informed about write action to the variables by overwriting the IOManagerUaNode methods IOManagerUaNode::beforeSetAttributeValue() or IOManagerUaNode::afterSetAttributeValue() in the node manager implementation.
If the source of the real time data requires data polling, this lesson explains the steps necessary to implement read, monitoring and write access to device data.
Step 1: Introducing BaUserData
The read and write access to the node attributes is implemented in the SDK class IOManagerUaNode by accesssing the UaNode interfaces like UaVariable or UaObject. All necessary information is already provided during the creation of the node in NodeManagerUaNode::afterStartUp() or during runtime.
The variable specific value attribute handling is defined by the bit mask returned from UaVariable::valueHandling. There are three options:
- UaVariable_Value_Cache | UaVariable_Value_CacheIsSource (Default setting)
- This default setting tells the SDK that the value is always up-to-date and all read, write and monitoring actions can be executed on the UaVariable node. This option is used if the variable represents internal configuration data or the data source is event based and delivers data changes automatically.
- UaVariable_Value_Cache (Used in this lesson)
- This setting is used if the read and write access for this variable will be implemented by overwriting the methods IOManagerUaNode::readValues() and IOManagerUaNode::writeValues() in the node manager implementation.
- UaVariable_Value_None
- This setting is used if the IOManager interface is implemented directly for this variable. This requires to overwrite NodeManagerUaNode::getIOManager() and to return the own IOManager responsible for the value attribute of this variable.
We need to change all controller variables that provide data from the devices to the option UaVariable_Value_Cache to get the read and write calls for these variables. To execute these read and write actions, we need to store the information to access the device in the variable. This is done by using the UaNode functionality UaNode::setUserData() and UaNode::getUserData(). For this storage capability we need to derive our data class from UserDataBase to allow the node to delete the data object when the node is deleted. The following figure shows the new BaUserData class and how it is related to the other classes.
Figure 3-1 Introducing BaUserData
UserDataBase Class Definition
Add the class definition of BaUserData to the file controllerobject.h:
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "userdatabase.h"
class NmBuildingAutomation;
...
{
UA_DISABLE_COPY(BaUserData);
public:
BaUserData(
OpcUa_Boolean isState,
OpcUa_UInt32 deviceAddress,
OpcUa_UInt32 variableOffset)
: m_isState(isState),
m_deviceAddress(deviceAddress),
m_variableOffset(variableOffset)
{}
virtual ~BaUserData(){}
inline OpcUa_UInt32 isState() const { return m_isState; }
inline OpcUa_UInt32 deviceAddress() const { return m_deviceAddress; }
inline OpcUa_UInt32 variableOffset() const { return m_variableOffset; }
private:
OpcUa_Boolean m_isState;
OpcUa_UInt32 m_deviceAddress;
OpcUa_UInt32 m_variableOffset;
};
#endif
The class stores the following information:
- m_isState
- Flag indicating if the variable represents the state of the controller
- m_deviceAddress
- The device address used to access the device
- m_variableOffset
- The offset of the variable in the device data area
Step 2: Use of BaUserData in Controller Classes
Two settings are necessary to change the behaviour of a UaVariable to the device read and write actions.
To pass the necessary device address and the communication interface to the controller classes, we need to extend the constructors. The changes are shown in the following code snippets.
Add the marked code to controllerobject.h,
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
airconditionercontrollerobject.h,
class AirConditionerControllerObject :
public ControllerObject
{
public:
AirConditionerControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
and furnacecontrollerobject.h.
class FurnaceControllerObject :
public ControllerObject
{
public:
FurnaceControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
The use of the new parameters and the necessary settings to the variable objects is shown in the following code snippet from controllerobject.cpp:
ControllerObject::ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
m_pSharedMutex(NULL),
m_deviceAddress(deviceAddress),
m_pCommIf(pCommIf)
{
BaUserData* pUserData = NULL;
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_State);
UA_ASSERT(pInstanceDeclaration!=NULL);
this,
pInstanceDeclaration,
pNodeManager,
m_pSharedMutex);
addStatus = pNodeManager->addNodeAndReference(this, pDataVariable, OpcUaId_HasComponent);
UA_ASSERT(addStatus.
isGood());
pUserData = new BaUserData(OpcUa_True, deviceAddress, 0);
...
pUserData = new BaUserData(OpcUa_False, deviceAddress, 0);
...
pUserData = new BaUserData(OpcUa_False, deviceAddress, 1);
...
UA_ASSERT(addStatus.
isGood());
pUserData = new BaUserData(OpcUa_False, deviceAddress, 2);
The two setting calls on the variables are added to each variable instance of the controller base class in the constructor of the class ControllerObject.
Please note the different device offsets used in the examples above. The offset is based on the knowledge about the controller device.
Add the marked code changes to airconditionercontrollerobject.cpp
AirConditionerControllerObject::AirConditionerControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager, deviceAddress, pCommIf)
{
BaUserData* pUserData = NULL;
...
pUserData = new BaUserData(OpcUa_False, deviceAddress, 3);
...
pUserData = new BaUserData(OpcUa_False, deviceAddress, 4);
The two setting calls on the variables are also added to each variable instance of the air conditioner controller class in the constructor of the class AirConditionerControllerObject.
Add the marked code changes to furnacecontrollerobject.cpp
FurnaceControllerObject::FurnaceControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf)
: ControllerObject(name, newNodeId, defaultLocaleId, pNodeManager, deviceAddress, pCommIf)
{
BaUserData* pUserData = NULL;
....
UA_ASSERT(addStatus.
isGood());
pUserData = new BaUserData(OpcUa_False, deviceAddress, 3);
The two setting calls on the variables are also added to each variable instance of the furnace controller class in the constructor of the class FurnaceControllerObject.
Step 3: Implementing IOManagerUaNode Functionality
After changing the UaVariable settings, the method IOManagerUaNode::readValues is called for UA Read service invocations and data monitoring and the method IOManagerUaNode::writeValues is called for UA Write service invocations. We need to overwrite these methods in the class NmBuildingAutomation since the implementation in IOManagerUaNode is empty.
Figure 3-2 Overwriting IOManagerUaNode methods for read and write
NmBuildingAutomation Class Definition
NmBuildingAutomation has to overwrite readValues() and writeValues() shown in the following class definition. We also need to add the member variable for the communication interface pointer.
#ifndef __NMBUILDINGAUTOMATION_H__
#define __NMBUILDINGAUTOMATION_H__
#include "nodemanagerbase.h"
class BaCommunicationInterface;
{
UA_DISABLE_COPY(NmBuildingAutomation);
public:
NmBuildingAutomation();
virtual ~NmBuildingAutomation();
UaVariable* getInstanceDeclarationVariable(OpcUa_UInt32 numericIdentifier);
private:
BaCommunicationInterface *m_pCommIf;
};
#endif // __NMBUILDINGAUTOMATION_H__
NmBuildingAutomation Class Implementation
The following code additions to nmbuildingautomation.cpp provides the method prototypes and the creation of the communication interface, BaCommunicationInterface, which is introduced in the next step.
#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "airconditionercontrollerobject.h"
#include "furnacecontrollerobject.h"
#include "opcua_analogitemtype.h"
#include "bacommunicationinterface.h"
NmBuildingAutomation::NmBuildingAutomation()
:
NodeManagerBase(
"urn:UnifiedAutomation:CppDemoServer:BuildingAutomation")
{
m_pCommIf = new BaCommunicationInterface;
}
NmBuildingAutomation::~NmBuildingAutomation()
{
delete m_pCommIf;
}
...
{
return ret;
}
{
return ret;
}
UaStatus NmBuildingAutomation::createTypeNodes()
{
...
For now, we leave the function bodies almost empty due to the lack of a communication interface for getting the information to access devices. The communication interface is introduced in the next step.
Step 4: Introducing BaCommunicationInterface
The remaining part of this lesson aims on linking IOManagerUaNode functionality with device data. Therefore, we introduce the class BaCommunicationInterface, a class being prepared for providing simulation data for our scenario through the interface shown in Figure 3-3.
Figure 3-3 BaCommunicationInterface
Whereas
- getCountControllers()
- returns the number of available controllers
- getControllerConfig()
- provides configuration information of a controller, e.g. address information and type
- getControllerState()
- provides the current state of a controller
- getControllerData()
- allows read access to a certain data point in a controller with controller index and data offset in controller
- setControllerData()
- allows write access to a certain data point in a controller with controller index and data offset in controller
To use the class BaCommunicationInterface and the simulation data, add the following files to your project:
- [SDK Installation Directory]/examples/simulation_buildingautomation/bacommunicationinterface.cpp
- [SDK Installation Directory]/examples/simulation_buildingautomation/bacommunicationinterface.h
- [SDK Installation Directory]/examples/simulation_buildingautomation/bacontrollersimulation.cpp
- [SDK Installation Directory]/examples/simulation_buildingautomation/bacontrollersimulation.h
Add the marked code to controllerobject.h
#ifndef __CONTROLLEROBJECT_H__
#define __CONTROLLEROBJECT_H__
#include "uaobjecttypes.h"
#include "userdatabase.h"
class NmBuildingAutomation;
class BaCommunicationInterface;
class ControllerObject :
{
UA_DISABLE_COPY(ControllerObject);
public:
ControllerObject(
NmBuildingAutomation* pNodeManager,
OpcUa_UInt32 deviceAddress,
BaCommunicationInterface *pCommIf);
virtual ~ControllerObject(void);
protected:
OpcUa_UInt32 m_deviceAddress;
BaCommunicationInterface* m_pCommIf;
};
...
Step 5: Implementing NmBuildingAutomation::readValues() for Non-State Variables
In the next step, we are implementing NmBuildingAutomation::readValues() for Non-State Variables.
UaStatus NmBuildingAutomation::readValues(
{
OpcUa_UInt32 i;
OpcUa_UInt32 count = arrUaVariables.length();
arrDataValues.create(count);
for (i=0; i<count; i++)
{
arrDataValues[i].setSourceTimestamp(timeStamp);
arrDataValues[i].setServerTimestamp(timeStamp);
if (pVariable)
{
BaUserData* pUserData = (BaUserData*)pVariable->
getUserData();
if ( pUserData )
{
if ( pUserData->isState() == OpcUa_False )
{
OpcUa_Double value;
status = m_pCommIf->getControllerData(
pUserData->deviceAddress(),
pUserData->variableOffset(),
value);
{
arrDataValues[i].setValue(vTemp, OpcUa_True, OpcUa_False);
}
else
{
arrDataValues[i].setStatusCode(status.
statusCode());
}
}
else
{
arrDataValues[i].setStatusCode(OpcUa_BadNotImplemented);
}
}
else
{
arrDataValues[i].setStatusCode(OpcUa_BadInternalError);
}
}
else
{
arrDataValues[i].setStatusCode(OpcUa_BadInternalError);
}
}
return ret;
}
readValues() provides an array of UaVariable interface pointers used to indicate which variables should be read. The second parameter is an array of UaDataValue classes used to return the values read.
After creating the output array for the read values, the requested variables are processed in a loop.
For every variable, the user data is requested by using the method UaNode::getUserData(). The returned user data object is casted to BaUserData. The user data is then used to detect if the state or one of the data variables is requested.
If a data value is requested, the method getControllerData of the communication interface is called. The device address and the offset stored in the user data are used to get the current value. The returned value or, in the case of an error, the status are used to set the data value in the out parameter array.
Step 6: Creating Devices Based on BaCommunicationInterface Information
We can also use BaCommunicationInterface in order to create devices in NmBuildingAutomation::afterStartUp().
Therefore, we need the number of available controllers provided by getControllerCount(), and additional configuration information, especially the type of each controller and its address provided by getControllerConfig().
We replace the method NmBuildingAutomation::afterStartUp with the following code.
UaStatus NmBuildingAutomation::afterStartUp()
{
AirConditionerControllerObject *pAirConditioner = NULL;
FurnaceControllerObject *pFurnace = NULL;
OpcUa_UInt32 count = m_pCommIf->getCountControllers();
OpcUa_UInt32 i;
OpcUa_UInt32 controllerAddress;
BaCommunicationInterface::ControllerType controllerType;
createTypeNodes();
pFolder =
new UaFolder(
"BuildingAutomation",
UaNodeId(
"BuildingAutomation", getNameSpaceIndex()), m_defaultLocaleId);
ret = addNodeAndReference(OpcUaId_ObjectsFolder, pFolder, OpcUaId_Organizes);
for ( i=0; i<count; i++ )
{
ret = m_pCommIf->getControllerConfig(
i,
controllerType,
sControllerName,
controllerAddress);
if ( controllerType == BaCommunicationInterface::AIR_CONDITIONER )
{
pAirConditioner = new AirConditionerControllerObject(
sControllerName,
UaNodeId(sControllerName, getNameSpaceIndex()),
m_defaultLocaleId,
this,
controllerAddress,
m_pCommIf);
ret = addNodeAndReference(pFolder, pAirConditioner, OpcUaId_Organizes);
}
else
{
pFurnace = new FurnaceControllerObject(
sControllerName,
UaNodeId(sControllerName, getNameSpaceIndex()),
m_defaultLocaleId,
this,
controllerAddress,
m_pCommIf);
ret = addNodeAndReference(pFolder, pFurnace, OpcUaId_Organizes);
}
}
return ret;
}
getCountControllers() provides the number of configured controllers. This number is used to iterate over the controller list and to create the controller objects based on the returned configuration.
getControllerConfig() returns the configuration for a controller based on the passed index. It returns the controller type, name, and address. The type of the controller indicates whether we have to create an air conditioner controller or a furnace controller. The name and the address are used to intialize the object to create.
Step 7: Extending NmBuildingAutomation::readValues() to Access State Variables
Since afterStartUp() has been completed, we need to complete the readValues() implementation by also handling the read access to the State Variables. In order to achieve this, we have to replace
else
{
arrDataValues[i].setStatusCode(OpcUa_BadNotImplemented);
}
with
else
{
BaCommunicationInterface::ControllerState state;
status = m_pCommIf->getControllerState(
pUserData->deviceAddress(),
state);
{
arrDataValues[i].setValue(vTemp, OpcUa_True, OpcUa_False);
}
else
{
arrDataValues[i].setStatusCode(status.
statusCode());
}
}
The read value for the state can be received via BaCommunicationInterface::getControllerState() which takes a controller index (like getControllerData()) and returns the state of the controller.
Step 8: Implementing NmBuildingAutomation::writeValues()
We still have to implement NmBuildingAutomation::writeValues() in order to finish this lesson.
UaStatus NmBuildingAutomation::writeValues(
UaStatusCodeArray &arrStatusCodes)
{
OpcUa_UInt32 i;
OpcUa_UInt32 count = arrUaVariables.length();
arrStatusCodes.create(count);
for ( i=0; i<count; i++ )
{
if ( pVariable )
{
BaUserData* pUserData = (BaUserData*)pVariable->
getUserData();
if ( pUserData )
{
if ( pUserData->isState() == OpcUa_False )
{
OpcUa_Double value;
{
status = m_pCommIf->setControllerData(
pUserData->deviceAddress(),
pUserData->variableOffset(),
value);
}
}
else
{
arrStatusCodes[i] = OpcUa_BadNotWritable;
}
}
else
{
arrStatusCodes[i] = OpcUa_BadInternalError;
}
}
else
{
arrStatusCodes[i] = OpcUa_BadInternalError;
}
}
return ret;
}
writeValues() provides an array of UaVariable interface pointers used to indicate which variables should be written. The second parameter is an array of OpcUa_DataValue pointers containing the values to be written. This method returns an array of OpcUa_StatusCode values indicating the success for every requested variable.
After creating the output array for the write results, the requested variables are processed in a loop.
For every variable, the user data is requested by using the method UaNode::getUserData(). The returned user data object is casted to BaUserData. The user data is then used to detect if the state or one of the data variables is requested.
If the state is requested, an error OpcUa_BadNotWritable is returned.
If a data value is requested, the method setControllerData of the communication interface is called. The device address and the offset stored in the user data are used to set the new value. The returned status is set to the corresponding out parameter array element.
Step 9: Run Application
Compile and run the server application.
When connecting to the server with UaExpert, the simulated devices show up in the Address Space. Drag and drop some variables to the Default DA View window to monitor their values (see screenshot).
Figure 3-4 Simulated controllers in UaExpert