The goal of this lesson is to show how to provide historical data in the Address Space.
The following figure shows the additional features provided by this lesson. The value of the Temperature variable will be historized and the historical information will be provided through the HistoryRead service. The historical configuration is provided through an additional object HA Configuration.
Figure 7-1 Object HA Configuration
The necessary steps to provide access to the history and to indicate the availability of the history are described in the following.
Step 1: Creating a HistoryManager
The HistoryManager interface is used to integrate history access functionality with the SDK. We will use the helper class HistoryManagerBase for this example. This helper class provides a simplified synchronous API for the history access handling. The following code should be added to a header file with the name historymanagerbuildingautomation.h.
#ifndef __HISTORYMANAGERBUILDINGAUTOMATION_H__
#define __HISTORYMANAGERBUILDINGAUTOMATION_H__
#include "historymanagerbase.h"
{
UA_DISABLE_COPY(HistoryManagerBuildingAutomation);
public:
HistoryManagerBuildingAutomation();
virtual ~HistoryManagerBuildingAutomation();
OpcUa_UInt32 maxValues,
OpcUa_Boolean returnBounds,
private:
};
#endif // __HISTORYMANAGERBUILDINGAUTOMATION_H__
The following code with the empty class implementation should be added to a file with the name historymanagerbuildingautomation.cpp
The class HistoryReadCPUserDataBA is used later on to implement the continuation point handling for history read raw. The next timestamp to return is used as continuation point data.
The following code is the empty implementation for the class HistoryManagerBuildingAutomation.
#include "historymanagerbuildingautomation.h"
{
public:
HistoryReadCPUserDataBA(){}
~HistoryReadCPUserDataBA(){}
};
HistoryManagerBuildingAutomation::HistoryManagerBuildingAutomation()
{
}
HistoryManagerBuildingAutomation::~HistoryManagerBuildingAutomation()
{
}
UaStatus HistoryManagerBuildingAutomation::readRaw (
OpcUa_UInt32 maxValues,
OpcUa_Boolean returnBounds,
{
return ret;
}
Step 2: Integrating HistoryManager into NodeManager
We need to connect the HistoryManager with the SDK and the Nodes we want provide historical information for.
This connection is done through the NodeManager. We need to create an instance of the HistoryManager in the NodeManager and need to register this instance with the NodeManagerBase implementation.
The following code needs to be added to the existing header file for the NmBuildingAutomation class.
#ifndef __NMBUILDINGAUTOMATION_H__
#define __NMBUILDINGAUTOMATION_H__
#include "nodemanagerbase.h"
class BaCommunicationInterface;
class HistoryManagerBuildingAutomation;
...
private:
BaCommunicationInterface* m_pCommIf;
HistoryManagerBuildingAutomation* m_pHistoryManager;
};
#endif // __NMBUILDINGAUTOMATION_H__
The following code needs to be added to the existing implementation file (nmbuildingautomation.cpp) for the NmBuildingAutomation class.
The additional code creates the HistoryManager instance and registers this instance with the NodeManager base class.
#include "nmbuildingautomation.h"
#include "buildingautomationtypeids.h"
#include "airconditionercontrollerobject.h"
#include "furnacecontrollerobject.h"
#include "opcua_analogitemtype.h"
#include "bacommunicationinterface.h"
#include "baeventdata.h"
#include "opcua_offnormalalarmtype.h"
#include "historymanagerbuildingautomation.h"
#include "opcua_historicaldataconfigurationtype.h"
NmBuildingAutomation::NmBuildingAutomation()
:
NodeManagerBase(
"urn:UnifiedAutomation:CppDemoServer:BuildingAutomation", OpcUa_True)
{
m_pCommIf = new BaCommunicationInterface;
m_pHistoryManager = new HistoryManagerBuildingAutomation;
setHistoryManager(m_pHistoryManager);
}
NmBuildingAutomation::~NmBuildingAutomation()
{
delete m_pHistoryManager;
delete m_pCommIf;
}
Step 3: Changes in the Information Model
After preparing the HistoryManager integration, we need to extend the information model and the address space to tell the client what kind of historical information is available.
There are two minor steps necessary to indicate that the Temperature variable provides historical data. The first step is to change the attribute’s AccessLevel and the Historizing to indicate that a client can read history in addition to the current value and that the value of the variable is currently historized.
The second step is to provide a HA Configuration object indicating the historical configuration for the variable. We can use the default settings of this object for the Temperature variable and we can share the object between all instances of the Temperature variable.
The changes of the attribute values and creation of the shared HA Configuration object are implemented with the following code in the method NmBuildingAutomation::createTypeNodes().
UaStatus NmBuildingAutomation::createTypeNodes()
{
...
...
UaNodeId(Ba_ControllerType_Temperature, getNameSpaceIndex()),
"Temperature",
getNameSpaceIndex(),
defaultValue,
Ua_AccessLevel_CurrentRead | Ua_AccessLevel_HistoryRead ,
this);
addStatus = addNodeAndReference(pControllerType, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
UaNodeId(Ba_ControllerType_Temperature_HA_Configuration, getNameSpaceIndex()),
"HA Configuration",
0,
this);
addStatus = addNodeAndReference(pAnalogItem, pHAConfig, OpcUaId_HasHistoricalConfiguration);
UA_ASSERT(addStatus.isGood());
We need to add an identifier for the HA Configuration to the file buildingautomationtypeids.h
#define Ba_ControllerType 1000
#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
#define Ba_ControllerType_StateCondition 1012
#define Ba_ControllerType_Temperature_HA_Configuration 1013
As a last step, we need to add a reference to the HA Configuration object to every Temperature variable instance. The following code is added to the constructor of the ControllerObject.
...
...
pInstanceDeclaration = pNodeManager->getInstanceDeclarationVariable(Ba_ControllerType_Temperature);
UA_ASSERT(pInstanceDeclaration!=NULL);
addStatus = pNodeManager->addNodeAndReference(this, pAnalogItem, OpcUaId_HasComponent);
UA_ASSERT(addStatus.isGood());
pUserData = new BaUserData(OpcUa_False, deviceAddress, 0);
UaNode* pUaConfigNode = pNodeManager->getNode(
UaNodeId(Ba_ControllerType_Temperature_HA_Configuration, pNodeManager->getNameSpaceIndex()));
if ( pUaConfigNode )
{
addStatus = pNodeManager->addUaReference(pAnalogItem, pUaConfigNode->
getUaReferenceLists(), OpcUaId_HasHistoricalConfiguration);
UA_ASSERT(addStatus.isGood());
pUaConfigNode->releaseReference();
}
Step 4: Implement Internal Historizing
After the preparation of the address space, we need to prepare the historizing and the history read. For the historizing, we can use internal SDK functionality to subscribe for value changes and to store them in a buffer. We are not implementing a data storage in this example. We are just filling a memory buffer with the last 1000 values for each variable to historize.
In a first step, we will add a new class HistorizedVariable and we will extend the interface of HistoryManagerBuildingAutomation.
The class HistorizedVariable in the following code is used to manage the information about the variable to historize and to provide the data buffer in memory.
The history manager class is extended in the following code to add variables to historize and to start the internal historizing.
#ifndef __HISTORYMANAGERBUILDINGAUTOMATION_H__
#define __HISTORYMANAGERBUILDINGAUTOMATION_H__
#include "historymanagerbase.h"
#include "uabasenodes.h"
#include "servermanager.h"
{
UA_DISABLE_COPY(HistorizedVariable);
public:
HistorizedVariable();
virtual ~HistorizedVariable();
OpcUa_UInt32 m_monitoredItemId;
OpcUa_Boolean m_isValid;
std::list<UaDataValue> m_values;
};
{
UA_DISABLE_COPY(HistoryManagerBuildingAutomation);
public:
HistoryManagerBuildingAutomation();
virtual ~HistoryManagerBuildingAutomation();
OpcUa_UInt32 maxValues,
OpcUa_Boolean returnBounds,
void shutDown();
void addVariableToHistorize(
UaNode* pNode);
private:
std::map<UaNodeId, HistorizedVariable*> m_mapVariables;
};
#endif // __HISTORYMANAGERBUILDINGAUTOMATION_H__
The following code contains the corresponding implementation for the new class HistorizedVariable and the additional functionality in HistoryManagerBuildingAutomation.
#include "historymanagerbuildingautomation.h"
#include "serverconfig.h"
#include "variablehandleuanode.h"
#define HISTORYMANAGERBA_SAMPLING_INTERVAL 500
#define HISTORYMANAGERBA_QUEUE_SIZE 2000
{
public:
HistoryReadCPUserDataBA(){}
~HistoryReadCPUserDataBA(){}
};
HistoryManagerBuildingAutomation::HistoryManagerBuildingAutomation()
: m_pSession(NULL),
m_pServerManager(NULL)
{
}
HistoryManagerBuildingAutomation::~HistoryManagerBuildingAutomation()
{
std::map<UaNodeId, HistorizedVariable*>::iterator it;
for ( it=m_mapVariables.begin(); it!=m_mapVariables.end(); it++ )
{
delete it->second;
it->second = NULL;
}
}
void HistoryManagerBuildingAutomation::addVariableToHistorize(
UaNode* pNode)
{
if ( (pNode != NULL) && (pNode->
nodeClass() == OpcUa_NodeClass_Variable) )
{
HistorizedVariable* pVariable = new HistorizedVariable;
m_mapVariables[pNode->
nodeId()] = pVariable;
}
}
void HistoryManagerBuildingAutomation::startUp(
ServerManager* pServerManager)
{
m_pServerManager = pServerManager;
if ( m_pSession )
{
{
sTemp.copyTo(&localeIDs[0]);
status = m_pSession->activate(0, NULL, localeIDs).
statusCode();
}
}
{
OpcUa_UInt32 i = 0;
std::map<UaNodeId, HistorizedVariable*>::iterator it;
dataMonitoredItems.create(m_mapVariables.size());
for(it=m_mapVariables.begin(); it!=m_mapVariables.end(); it++)
{
HistorizedVariable* pVariable = it->second;
pVariable->m_pVariable->nodeId().copyTo(&dataMonitoredItems[i].m_itemToMonitor.NodeId);
dataMonitoredItems[i].m_itemToMonitor.AttributeId = OpcUa_Attributes_Value;
dataMonitoredItems[i].m_requestedSamplingInterval = HISTORYMANAGERBA_SAMPLING_INTERVAL;
dataMonitoredItems[i].m_pDataCallback = pVariable;
i++;
}
{
i = 0;
for(it=m_mapVariables.begin(); it!=m_mapVariables.end(); it++)
{
HistorizedVariable* pVariable = it->second;
if ( dataMonitoredItems[i].m_createResult.isGood() )
{
pVariable->m_isValid = OpcUa_True;
pVariable->m_monitoredItemId = dataMonitoredItems[i].m_monitoredItemId;
if ( dataMonitoredItems[i].m_isInitialValueProvided != OpcUa_False )
{
pVariable->dataChange(dataMonitoredItems[i].m_initialValue);
}
}
i++;
}
}
}
}
void HistoryManagerBuildingAutomation::shutDown()
{
std::map<UaNodeId, HistorizedVariable*>::iterator it;
if ( m_pServerManager && m_pSession )
{
OpcUa_UInt32 i = 0;
monitoredItemIds.
create(m_mapVariables.size());
i = 0;
for(it=m_mapVariables.begin(); it!=m_mapVariables.end(); it++)
{
HistorizedVariable* pVariable = it->second;
monitoredItemIds[i] = pVariable->m_monitoredItemId;
i++;
}
m_pServerManager->deleteMonitoredItems(m_pSession, monitoredItemIds, results);
}
for ( it=m_mapVariables.begin(); it!=m_mapVariables.end(); it++ )
{
delete it->second;
it->second = NULL;
}
m_mapVariables.clear();
if( m_pSession )
{
m_pSession->releaseReference();
m_pSession = NULL;
}
}
HistorizedVariable::HistorizedVariable()
: m_pVariable(NULL),
m_monitoredItemId(0),
m_isValid(OpcUa_False)
{
}
HistorizedVariable::~HistorizedVariable()
{
}
void HistorizedVariable::dataChange(
const UaDataValue& dataValue)
{
m_values.push_back(dataValue);
if ( m_values.size() > HISTORYMANAGERBA_QUEUE_SIZE )
{
m_values.erase(m_values.begin());
}
}
{
return ret;
}
The last part of the historizing implementation is the use of the new functionality in the node manager.
As a first additional step the flag in the server capabilities indicating support of historical access for data is set.
The code that creates the controller objects is extended to register the Temperature variables of the controller objects with the history manager.
In addition, the startUp of history manager is called at the end of the node manager start up to start internal historizing. The shutDown of the history manager is called in the shut down of the node manager to stop the internal historizing.
UaStatus NmBuildingAutomation::afterStartUp()
{
...
createTypeNodes();
m_pHistoryManager->pHistoryServerCapabilities()->setAccessHistoryDataCapability(OpcUa_True);
...
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);
UA_ASSERT(ret.isGood());
ret = addUaReference(pAreaAirConditioner, pAirConditioner, OpcUaId_HasNotifier);
UA_ASSERT(ret.isGood());
registerEventNotifier(pAreaAirConditioner->
nodeId(), pAirConditioner->nodeId());
m_pHistoryManager->addVariableToHistorize(pAirConditioner->getTargetNodeByBrowseName(
UaQualifiedName(
"Temperature", getNameSpaceIndex())));
}
else
{
pFurnace = new FurnaceControllerObject(
sControllerName,
UaNodeId(sControllerName, getNameSpaceIndex()),
m_defaultLocaleId,
this,
controllerAddress,
m_pCommIf);
ret = addNodeAndReference(pFolder, pFurnace, OpcUaId_Organizes);
UA_ASSERT(ret.isGood());
ret = addUaReference(pAreaFurnace, pFurnace, OpcUaId_HasNotifier);
UA_ASSERT(ret.isGood());
registerEventNotifier(pAreaFurnace->nodeId(), pFurnace->nodeId());
m_pHistoryManager->addVariableToHistorize(pFurnace->getTargetNodeByBrowseName(
UaQualifiedName(
"Temperature", getNameSpaceIndex())));
}
}
m_isStarted = OpcUa_True;
m_pHistoryManager->startUp(m_pServerManager);
return ret;
}
UaStatus NmBuildingAutomation::beforeShutDown()
{
m_pHistoryManager->shutDown();
return ret;
}
Step 5: Implement History Read Raw
The following code contains the implementation for history read raw based on the data buffer in memory.
UaStatus HistoryManagerBuildingAutomation::readRaw (
OpcUa_UInt32 maxValues,
OpcUa_Boolean returnBounds,
{
OpcUa_ReferenceParameter(serviceContext);
OpcUa_ReferenceParameter(pReadValueId);
{
return OpcUa_BadNodeIdUnknown;
}
std::map<UaNodeId, HistorizedVariable*>::iterator it;
it = m_mapVariables.find(pUaNodeVariableHandle->pUaNode()->
nodeId());
if ( (it == m_mapVariables.end()) || (it->second->m_isValid == OpcUa_False) )
{
return OpcUa_BadNodeIdUnknown;
}
HistorizedVariable* pVariable = it->second;
OpcUa_UInt32 i = 0;
OpcUa_Int64 iStart = dtStart;
OpcUa_Int64 iEnd = dtEnd;
OpcUa_DateTime_Initialize(&nullDateTime);
HistoryReadCPUserDataBA* pContinuationPoint = (HistoryReadCPUserDataBA*)*ppContinuationPoint;
if ( pContinuationPoint )
{
dtStart = pContinuationPoint->m_newStartTime;
iStart = dtStart;
delete pContinuationPoint;
*ppContinuationPoint = NULL;
}
if ( maxValues == 0 )
{
maxValues = OpcUa_Int32_Max;
}
if ( iStart < iEnd )
{
std::list<UaDataValue>::iterator itValues;
dataValues.
create(pVariable->m_values.size());
for ( itValues=pVariable->m_values.begin(); itValues!=pVariable->m_values.end(); itValues++ )
{
if ( i == maxValues )
{
HistoryReadCPUserDataBA* pContinuationPoint = new HistoryReadCPUserDataBA;
pContinuationPoint->m_newStartTime = itValues->serverTimestamp();
*ppContinuationPoint = pContinuationPoint;
break;
}
OpcUa_Int64 iVal = dtVal;
if ( iVal < iStart )
{
continue;
}
if ( iVal > iEnd )
{
break;
}
if ( (i == 0) && (returnBounds != OpcUa_False) && (itValues != pVariable->m_values.begin()) )
{
itValues--;
itValues->
copyTo(&dataValues[i]);
{
dataValues[i].SourceTimestamp = nullDateTime;
}
{
dataValues[i].ServerTimestamp = nullDateTime;
}
itValues++;
i++;
}
itValues->copyTo(&dataValues[i]);
{
dataValues[i].SourceTimestamp = nullDateTime;
}
{
dataValues[i].ServerTimestamp = nullDateTime;
}
i++;
}
}
else
{
std::list<UaDataValue>::reverse_iterator ritValues;
dataValues.
create(pVariable->m_values.size());
for ( ritValues=pVariable->m_values.rbegin(); ritValues!=pVariable->m_values.rend(); ritValues++ )
{
if ( i == maxValues )
{
HistoryReadCPUserDataBA* pContinuationPoint = new HistoryReadCPUserDataBA;
pContinuationPoint->m_newStartTime = ritValues->serverTimestamp();
*ppContinuationPoint = pContinuationPoint;
break;
}
OpcUa_Int64 iVal = dtVal;
if ( iVal > iStart )
{
continue;
}
if ( iVal < iEnd )
{
break;
}
if ( (i == 0) && (returnBounds != OpcUa_False) && (ritValues != pVariable->m_values.rbegin()) )
{
ritValues--;
ritValues->
copyTo(&dataValues[i]);
{
dataValues[i].SourceTimestamp = nullDateTime;
}
{
dataValues[i].ServerTimestamp = nullDateTime;
}
ritValues++;
i++;
}
ritValues->copyTo(&dataValues[i]);
{
dataValues[i].SourceTimestamp = nullDateTime;
}
{
dataValues[i].ServerTimestamp = nullDateTime;
}
i++;
}
}
return ret;
}
Step 6: Test History Read with UaExpert
The history plugin of the UaExpert can be used to read the history of the Temperature variables.
Connect to the server with UaExpert and open the History Trend View. Drag and drop some Temperature variables to the Configuration window and click on Update. The historical data will be shown in the graph below (see figure 7-2
Figure 7-2 Historical Trend View in UaExpert