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 _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 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,
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;
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,
TemperatureSensor **a_ppNewTemperatureSensor)
{
pNewSensor = (TemperatureSensor*)OpcUa_Alloc(sizeof(TemperatureSensor));
OpcUa_ReturnErrorIfAllocFailed(pNewSensor);
OpcUa_MemSet(pNewSensor, 0, sizeof(TemperatureSensor));
pNewSensor->Type = UserDataTemperature;
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.
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 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;
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))
{
{
pRes->
Results[i].SourceTimestamp = OpcUa_DateTime_UtcNow();
}
{
pRes->
Results[i].ServerTimestamp = OpcUa_DateTime_UtcNow();
}
}
}
else
{
}
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”.