For representing the machine we created in the previous lesson, we will use structures that hold all the information needed to interact with the server. If there would be an underlying protocol to get the data, the structures would contain the protocol information needed for retrieving the required data.
Content:
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:
enum _UserDataType
{
UserDataTemperature,
UserDataMachine,
UserDataMachineSwitch
};
typedef enum _UserDataType UserDataType;
struct _UserDataCommon
{
UserDataType Type;
};
typedef struct _UserDataCommon UserDataCommon;
struct _TemperatureSensor
{
UserDataType Type;
OpcUa_Double *pValue;
};
typedef struct _TemperatureSensor TemperatureSensor;
struct _MachineSwitch
{
UserDataType Type;
OpcUa_Boolean *pValue;
};
typedef struct _MachineSwitch MachineSwitch;
struct _Machine
{
UserDataType Type;
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 strucures and to append them to the nodes that are being created.
OpcUa_StatusCode CustomProvider_CreateMachine(OpcUa_StringA a_sMachineName,
OpcUa_StringA 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;
...
pNewMachine = (Machine*)OpcUa_Alloc(sizeof(Machine));
OpcUa_ReturnErrorIfAllocFailed(pNewMachine);
OpcUa_MemSet(pNewMachine, 0, sizeof(Machine));
pNewMachine->Type = UserDataMachine;
...
...
pNewHeaterSwitch = (MachineSwitch*)OpcUa_Alloc(sizeof(MachineSwitch));
OpcUa_ReturnErrorIfAllocFailed(pNewHeaterSwitch);
OpcUa_MemSet(pNewHeaterSwitch, 0, sizeof(MachineSwitch));
pNewHeaterSwitch->Type = UserDataMachineSwitch;
pNewMachine->pHeaterSwitch = pNewHeaterSwitch;
Step 3: Connecting the User Data Structures to Real World Data
After the user data structures have been created, we can connect them to the 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.
uStatus = CustomProvider_CreateMachine("MyCustomMachine",
"MyCustomTemperatureSensor",
(OpcUa_BaseNode*)pCustomProvider,
&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 being done by 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. The browse functions do not need to be changed as the address space information is still stored in the OpcUa_BaseNode structures.
In the affected functions, we need to check if 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 is shown:
UserDataCommon *pUserData = OpcUa_Null;
...
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;
}
}
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
{
UaServer_ReadInternal(pNode, a_pReadCtx, i);
}