ANSI C Based OPC UA Client/Server SDK  1.8.0.369
Lesson 7: Adding Support for Historical Access

In this lesson we will explain how to add historizing capabilities to a variable node.

Files used in this lesson:

Step 1: Setting up a Data Logger

For logging values of a variable node, we will create a data logger as a first step. Two sample implementations are delivered with the SDK (SQLiteLogger or FileLogger), but it is possible to create your own implementation of a data logger and use it instead. In this example we will use one of the sample implementations, depending on which is enabled. By default, FileLogger is enabled in the SDK. This can be changed using CMake.

Add the following include directory:
<SDK_INSTALL_DIR>/include/datalogger

First we need to know the NodeId of the variable node we want to log (here we want to log the temperature value of the temperature sensor), so we extend the TemperatureSensor by a NodeId member and an Int member for storing the handle to the data logger item (see custom_provider_helper.h).

struct _TemperatureSensor
{
/* User data header */
UserDataType Type;
/* Protocol information */
OpcUa_Double *pValue;
/* Data logging information */
OpcUa_NodeId nodeIdTemperatureValue;
OpcUa_Int hDataLogItemTemperatureValue;
};
typedef struct _TemperatureSensor TemperatureSensor;

Clients need to know that the Temperature variable has history available, thus it is necessary to set the AccessLevel accordingly, adding OpcUa_AccessLevels_HistoryRead to the default read/write: (see custom_provider.c):

OpcUa_StatusCode CustomProvider_CreateTemperatureSensor(const OpcUa_CharA *a_sSensorName,
OpcUa_BaseNode *a_pOwner,
OpcUa_NodeId *a_pTemperatureSensorTypeId,
OpcUa_NodeId *a_pStartingNodeId,
TemperatureSensor **a_ppNewTemperatureSensor)
{
...
/* Create Temperature property */
a_pStartingNodeId->Identifier.Numeric++;
uStatus = UaServer_CreateDataVariable(pAddressSpace,
&pVariable,
(OpcUa_BaseNode*)pObject,
a_pStartingNodeId->Identifier.Numeric,
g_uCustomProvider_NamespaceIndex,
"Temperature");
OpcUa_GotoErrorIfBad(uStatus);
OpcUa_Variable_SetAccessLevel(pVariable, OpcUa_AccessLevels_CurrentReadOrWrite | OpcUa_AccessLevels_HistoryRead);

The new member nodeIdTemperatureValue of the extended TemperatureSensor structure is initialized and set in CustomProvider_CreateTemperatureSensor:

OpcUa_NodeId_Initialize(&pNewSensor->nodeIdTemperatureValue);
OpcUa_NodeId_CopyTo(a_pStartingNodeId, &pNewSensor->nodeIdTemperatureValue);

Using the NodeId of the temperature value variable, we can now create a data logger and the corresponding data logger item. The data logger handle is stored in a global variable g_hDataLogger which is used to clean it up on shutdown. The item handle returned by UaServer_DataLoggerItemData_Create is stored in the user data associated with the temperature value variable.

void CustomProvider_SetupDataLogger()
{
OpcUa_BaseNode *pNode = 0;
UaServer_AddressSpace *pAddressSpace = &g_pCustomProvider->AddressSpace;
/* create file logger */
#if HAVE_DATA_LOGGER_FILE_BACKEND
g_hDataLogger = UaServer_FileLogger_Create( "historian" );
#endif
if (g_hDataLogger < 0) return;
/* add Temperature to data logging */
UaServer_GetNode(pAddressSpace, &g_pMyCustomMachine->pTemperatureSensor->nodeIdTemperatureValue, &pNode);
if ( pNode )
{
TemperatureSensor *pTemperatureSensor = (TemperatureSensor*)OpcUa_BaseNode_GetUserData(pNode);
UaServer_DataLoggerItemData_Create(&g_pMyCustomMachine->pTemperatureSensor->nodeIdTemperatureValue,
100,
100,
OpcUa_Null,
g_hDataLogger,
&pTemperatureSensor->hDataLogItemTemperatureValue);
}
/* start historian data logging */
UaServer_DataLogger_Start( g_hDataLogger );
}

Step 2: Implementing HistoryReadRawModified in the Provider

In order to make the recorded history values available to clients, we need to implement HistoryReadRawModified in our provider and forward the calls to the data logger. A new callback function is created and set in the provider interface:

IFMETHODIMP(CustomProvider_HistoryReadRawModifiedAsync)(UaServer_ProviderHistoryReadRawModifiedContext* a_pHistoryReadRawModifiedCtx);
...
IFMETHODIMP(CustomProvider_Initialize)(UaServer_Provider *a_pProvider,
UaServer_pProviderInterface *a_pProviderInterface)
{
...
a_pProviderInterface->HistoryReadRawModifiedAsync = CustomProvider_HistoryReadRawModifiedAsync;

The implementation of this function can be found in custom_provider_historyread.c. Just like in other services, we check whether we are responsible for processing the request by comparing the namespace indexes. If so, we check whether the variable is history readable and whether it contains user data of type UserDataTemperature.

IFMETHODIMP(CustomProvider_HistoryReadRawModifiedAsync)(UaServer_ProviderHistoryReadRawModifiedContext* a_pHistoryReadRawModifiedCtx)
{
...
/* process nodes */
for (i = 0; i < a_pHistoryReadRawModifiedCtx->NoOfNodesToRead; i++)
{
/* check if the node is from our provider's namespace */
if (a_pHistoryReadRawModifiedCtx->pNodesToRead[i].NodeId.NamespaceIndex == g_uCustomProvider_NamespaceIndex)
{
/* get the node pointer from the NodeId */
UaServer_GetNode(pAddressSpace, &a_pHistoryReadRawModifiedCtx->pNodesToRead[i].NodeId, &pNode);
if (pNode)
{
...
if (OpcUa_BaseNode_GetId(pNode)->IdentifierType == OpcUa_IdentifierType_Numeric)
{
OpcUa_HistoryReadResult *pHistoryResult = &a_pHistoryReadRawModifiedCtx->pResponse->Results[i];
UserDataCommon *pUserData = (UserDataCommon*)OpcUa_BaseNode_GetUserData(pNode);
if (pUserData && pUserData->Type == UserDataTemperature)
{

Knowing that we have a history for this node, we can forward the call to the data logger, providing the handles to the data logger itself and the data logger item (stored in the user data of the node). This is everything that needed to be done to enable history data logging for a node and for providing the values to clients.

TemperatureSensor *pTemperatureSensorData = (TemperatureSensor*)pUserData;
a_pHistoryReadRawModifiedCtx->pResponse->Results[i].StatusCode = UaServer_DataLogger_ReadValues(
g_hDataLogger,
pTemperatureSensorData->hDataLogItemTemperatureValue,
a_pHistoryReadRawModifiedCtx->pHistoryReadRawModifiedDetails,
a_pHistoryReadRawModifiedCtx->TimestampsToReturn,
a_pHistoryReadRawModifiedCtx->ReleaseContinuationPoints,
&a_pHistoryReadRawModifiedCtx->pNodesToRead[i],
pHistoryResult,
a_pHistoryReadRawModifiedCtx->RequestHeader.TimeoutHint);

Step 3: Reading out the History with a Client

After having set up history data logging for the temperature sensor value, we can read it out using a client.

For reading history values with UaExpert, create a History Trend View by clicking “Add Document”, selecting “History Trend View” and clicking “Add” (see Figure 7-1).

Figure 7-1 Create a History Trend View using UaExpert

gettingstarted1_lesson07_create_historytrendview_uaexpert.png

Now you can drag and drop the “Temperature” variable of “MyCustomTemperatureSensor” into the Configuration section of the History Trend View. The client will then read the history values of the variable and display them in the “Numeric Values” tab in the History Data section of the view (see Figure 7-2).

By changing “Start Time” and “End Time” you can change the range of values the client requests from the server, clicking “Update” issues an update of the history values.

Figure 7-2 Read History using UaExpert

gettingstarted1_lesson07_read_history_with_uaexpert.png