ANSI C UA Server SDK  1.5.0.312
 All Data Structures Functions Variables Typedefs Enumerations Enumerator Modules Pages
Lesson 3: Connecting the Nodes to Real World Data

For representing the machine we created in the previouslesson", we will use structures that hold all the information needed to interact with the server. If there was an underlying protocol to get the data, the structures would contain the protocol information needed for retrieving the required data.

Files used in this lesson:

Step 1: Introducing Custom User Data Structures

Our example has three types of structures that inherit from one UserDataCommon structure. This makes it possible to get the type of user data stored in our nodes and to cast them to the appropriate type (see custom_provider_helper.h):

/* Enum of custom user data types */
enum _UserDataType
{
UserDataTemperature,
UserDataMachine,
UserDataMachineSwitch
};
typedef enum _UserDataType UserDataType;
/* All user data structs contain the same header with type information.
* This concept is application specific and only an example.
* You can store whatever you like in UserData.
*/
struct _UserDataCommon
{
UserDataType Type; /* Currently only the type info is needed in the common header */
};
typedef struct _UserDataCommon UserDataCommon;
struct _TemperatureSensor
{
/* User data header */
UserDataType Type;
/* Protocol information */
OpcUa_Double *pValue;
};
typedef struct _TemperatureSensor TemperatureSensor;
struct _MachineSwitch
{
/* User data header */
UserDataType Type;
/* Protocol information */
OpcUa_Boolean *pValue;
};
typedef struct _MachineSwitch MachineSwitch;
struct _Machine
{
/* User data header */
UserDataType Type;
/* Machine data */
TemperatureSensor *pTemperatureSensor;
MachineSwitch *pHeaterSwitch;
};
typedef struct _Machine Machine;

Step 2: Appending the User Data to the Created Nodes

The next step is to create instances of the user data structures and to append them to the nodes that will be created (see custom_provider.c).

OpcUa_StatusCode CustomProvider_CreateMachine(const OpcUa_CharA *a_sMachineName,
const OpcUa_CharA *a_sTemperatureSensorName,
OpcUa_BaseNode *a_pOwner,
OpcUa_NodeId *a_pMachineTypeId,
OpcUa_NodeId *a_pTemperatureSensorTypeId,
OpcUa_NodeId *a_pStartingNodeId,
Machine **a_ppNewMachine)
{
Machine *pNewMachine = OpcUa_Null;
MachineSwitch *pNewHeaterSwitch = OpcUa_Null;
...
/* Create and initialize new machine struct */
pNewMachine = (Machine*)OpcUa_Alloc(sizeof(Machine));
OpcUa_ReturnErrorIfAllocFailed(pNewMachine);
OpcUa_MemSet(pNewMachine, 0, sizeof(Machine));
pNewMachine->Type = UserDataMachine;
...
/* Set new machine struct as user data of the new node */
OpcUa_BaseNode_SetUserData(pObject, pNewMachine);
...
/* Create and initialize new heater switch struct */
pNewHeaterSwitch = (MachineSwitch*)OpcUa_Alloc(sizeof(MachineSwitch));
OpcUa_ReturnErrorIfAllocFailed(pNewHeaterSwitch);
OpcUa_MemSet(pNewHeaterSwitch, 0, sizeof(MachineSwitch));
pNewHeaterSwitch->Type = UserDataMachineSwitch;
OpcUa_BaseNode_SetUserData(pVariable, pNewHeaterSwitch);
pNewMachine->pHeaterSwitch = pNewHeaterSwitch;

Following the same pattern, the user data of the temperature sensor is created:

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 and initialize new temperature sensor struct */
pNewSensor = (TemperatureSensor*)OpcUa_Alloc(sizeof(TemperatureSensor));
OpcUa_ReturnErrorIfAllocFailed(pNewSensor);
OpcUa_MemSet(pNewSensor, 0, sizeof(TemperatureSensor));
pNewSensor->Type = UserDataTemperature;
...
/* Set new TemperatureSensor struct as user data of the new node */
OpcUa_BaseNode_SetUserData(pVariable, pNewSensor);

Step 3: Connecting the User Data Structures to Real World Data

After the user data structures have been created, we can connect them to real world data. In our case there are two global variables that are simulated by a timer. Instead of this, the appropriate protocol information would be set in the user data in a scenario with an underlying protocol.

/* Create instance of MachineType */
uStatus = CustomProvider_CreateMachine("MyCustomMachine",
"MyCustomTemperatureSensor",
(OpcUa_BaseNode*)pCustomProvider,
OpcUa_BaseNode_GetId(pMachineType),
OpcUa_BaseNode_GetId(pTemperatureSensorType),
&nodeId,
&g_pMyCustomMachine);
OpcUa_GotoErrorIfBad(uStatus);
g_pMyCustomMachine->pHeaterSwitch->pValue = &g_bMyCustomMachineSwitch;
g_pMyCustomMachine->pTemperatureSensor->pValue = &g_bMyCustomMachineTemperature;
g_bMyCustomMachineSwitch = OpcUa_False;
g_bMyCustomMachineTemperature = 25.0;

The simulation of our real world data is realized using a timer which is triggered every 250 ms. It calls the CustomProvider_SimulationTimerCallback function and increases or decreases the temperature depending on the state of the machine’s heater switch.

Step 4: Extending the UA Service Callback Functions for Working with User Data

Until now, the UA service callback functions were generic functions, working just on the OpcUa_BaseNode structures, without any knowledge about the user data contained in them. This needs to be changed, as now the values of our newly created nodes are not stored in the OpcUa_BaseNode itself, but in the user data structures.

The functions to be changed are CustomProvider_ReadAsync, CustomProvider_WriteAsync and CustomProvider_SampleData. There’s no need to change the browse functions as the address space information is still stored in the OpcUa_BaseNode structures.

In the affected functions, we need to check whether a node contains user data. If so, we get the type of user data, cast to the appropriate type, and execute the desired actions. As an example, the interesting part of CustomProvider_ReadAsync (custom_provider_read.c) is shown. See the files custom_provider_write.c and custom_provider_subscription.c for CustomProvider_WriteAsync and CustomProvider_SampleData

UserDataCommon *pUserData = OpcUa_Null;
...
pUserData = (UserDataCommon*)OpcUa_BaseNode_GetUserData(pNode);
if (pUserData != OpcUa_Null &&
a_pReadCtx->pRequest->NodesToRead[i].AttributeId == OpcUa_Attributes_Value)
{
switch (pUserData->Type)
{
case UserDataTemperature:
{
TemperatureSensor *pSensor = (TemperatureSensor*)pUserData;
pRes->Results[i].Value.Datatype = OpcUaType_Double;
pRes->Results[i].Value.Value.Double = *pSensor->pValue;
pRes->Results[i].StatusCode = OpcUa_Good;
break;
}
case UserDataMachineSwitch:
{
MachineSwitch *pSwitch = (MachineSwitch*)pUserData;
pRes->Results[i].Value.Datatype = OpcUaType_Boolean;
pRes->Results[i].Value.Value.Boolean = *pSwitch->pValue;
pRes->Results[i].StatusCode = OpcUa_Good;
break;
}
default:
{
pRes->Results[i].StatusCode = OpcUa_BadNotReadable;
break;
}
}
/* Set timestamps */
if (OpcUa_IsGood(pRes->Results[i].StatusCode))
{
if (a_pReadCtx->pRequest->TimestampsToReturn == OpcUa_TimestampsToReturn_Source ||
a_pReadCtx->pRequest->TimestampsToReturn == OpcUa_TimestampsToReturn_Both)
{
pRes->Results[i].SourceTimestamp = OpcUa_DateTime_UtcNow();
}
if (a_pReadCtx->pRequest->TimestampsToReturn == OpcUa_TimestampsToReturn_Server ||
a_pReadCtx->pRequest->TimestampsToReturn == OpcUa_TimestampsToReturn_Both)
{
pRes->Results[i].ServerTimestamp = OpcUa_DateTime_UtcNow();
}
}
}
else
{
/* Use the SDK's helper function to process the request */
UaServer_ReadInternal(pNode, a_pReadCtx, i);
}

Step 5: Run Application

Compile and run the server application. To test the simulation, monitor the variable “Temperature” and write the value attribute of the variable “HeaterSwitch” several times. You will notice the rising/falling value of “Temperature”, depending on the state of “HeaterSwitch”.