The provided sample code shows how to create a Machine object that has a temperature sensor and sends events if the temperature exceeds or falls below certain limit values.
The complete sample code is available in the sample provider delivered with the SDK.
Please read the Alarms & Events section before working through this tutorial.
Contents:
Creating the needed object instances
All data needed to manage a Machine and it's sensor is encapsulated in structs that are set as UserData of the according object instances in the address space. The different structs 'inherit' from UserDataCommon, which has a member to determine the type of the UserData.
enum _UserDataType
{
UserDataTemperature,
UserDataMachine,
UserDataMachineSwitch
};
typedef enum _UserDataType UserDataType;
struct _UserDataCommon
{
UserDataType Type;
};
typedef struct _UserDataCommon UserDataCommon;
struct _Machine
{
UserDataType Type;
OpcUa_NodeId NodeId;
TemperatureSensor *pTemperatureSensor;
MachineSwitch *pHeaterSwitch;
};
typedef struct _Machine Machine;
...
To create a Machine object instance easily, convenience methods are used that create the object instance in the address space and create all references needed. Also, the Machine instance is registered as EventNotifier and the TemperatureSensor as EventSource at the event module of the SDK.
After the Machine object has been created, simulation variables are assigned to it to simulate rise and fall of the temperature.
OpcUa_Boolean g_MyMachineHeaterSwitch = OpcUa_False;
OpcUa_Double g_MyMachineTemperature = 80.0;
Machine *g_pMyMachine = OpcUa_Null;
static OpcUa_StatusCode UaProvider_Sample_CreateAddressSpace()
{
...
uStatus = UaProvider_Sample_CreateMachine("MyMachine",
"MyTemperatureSensor",
(OpcUa_BaseNode*)pFolderEvents,
&NodeId,
&g_pMyMachine);
OpcUa_GotoErrorIfBad(uStatus);
g_pMyMachine->pHeaterSwitch->pValue = &g_MyMachineHeaterSwitch;
g_pMyMachine->pTemperatureSensor->pValue = &g_MyMachineTemperature;
}
Checking the sensor value and emitting events during runtime
When the server is running, the simulation values are changed depending on the Machine's switch state. If it's switched on, the temperature is increased, otherwise the temperature declines slowly.
Also, the Machine is checked regularly as described below.
IFMETHODIMP(UaProvider_Sample_DynamicVarUpdate)(OpcUa_Void* a_pvCallbackData,
OpcUa_Timer a_hTimer,
OpcUa_UInt32 a_msecElapsed)
{
...
if (g_MyMachineHeaterSwitch == OpcUa_True && g_MyMachineTemperature < 110.0)
{
g_MyMachineTemperature += 0.5;
}
else if (g_MyMachineHeaterSwitch == OpcUa_False && g_MyMachineTemperature > -10.0)
{
g_MyMachineTemperature -= 0.5;
}
UaProvider_Sample_UpdateMachine(g_pMyMachine);
When checking the Machine's state, the temperature is compared to it's previous value. If it has changed, the new temperature is written to it's representing variable in the server's address space. Also, events are fired if necessary:
OpcUa_Void UaProvider_Sample_UpdateMachine(Machine *a_pMachine)
{
OpcUa_Double oldTemperature;
if (oldTemperature != *a_pMachine->pTemperatureSensor->pValue)
{
OpcUa_Variant newValue;
OpcUa_Variant_Initialize(&newValue);
newValue.Value.Double = *a_pMachine->pTemperatureSensor->pValue;
if (oldTemperature < 100 && *a_pMachine->pTemperatureSensor->pValue >= 100)
{
&a_pMachine->pTemperatureSensor->HighAlarmId);
UaProvider_Sample_FireAcknowledgeableConditionTypeEvent(pHighAlarm);
}
if (oldTemperature > 0 && *a_pMachine->pTemperatureSensor->pValue <= 0)
{
&a_pMachine->pTemperatureSensor->LowAlarmId);
UaProvider_Sample_FireAcknowledgeableConditionTypeEvent(pLowAlarm);
}
if (oldTemperature >= 100 && *a_pMachine->pTemperatureSensor->pValue < 100)
{
&a_pMachine->pTemperatureSensor->HighAlarmId);
UaProvider_Sample_DisableAcknowledgeableConditionTypeEvent(pHighAlarm);
}
if (oldTemperature <= 0 && *a_pMachine->pTemperatureSensor->pValue > 0)
{
&a_pMachine->pTemperatureSensor->LowAlarmId);
UaProvider_Sample_DisableAcknowledgeableConditionTypeEvent(pLowAlarm);
}
}
}
Switching the Machine on and off
As the Machine has a switch that should be controllable, we have to react on write calls on the according variable. If the variable is written, the value of the MachineSwitch is updated accordingly. This is done in the write callback method of the provider:
{
OpcUa_WriteRequest *pReq;
OpcUa_WriteResponse *pRes;
int i;
OpcUa_BaseNode *pNode;
UaServer_AddressSpace *pAddressSpace = &(g_pSampleProvider->AddressSpace);
OpcUa_ReturnErrorIfArgumentNull(a_pWriteCtx);
pReq = a_pWriteCtx->pRequest;
pRes = a_pWriteCtx->pResponse;
UaServer_Atomic_Increment(&a_pWriteCtx->nOutstandingCbs);
for (i = 0; i < pReq->NoOfNodesToWrite; i++)
{
if (pReq->NodesToWrite[i].NodeId.NamespaceIndex == g_UaProviderSample_uNamespaceIndex)
{
if (pNode)
{
...
if (OpcUa_BaseNode_GetType(pNode) == eVariable &&
pReq->NodesToWrite[i].AttributeId == OpcUa_Attributes_Value &&
pReq->NodesToWrite[i].Value.Value.ArrayType == OpcUa_VariantArrayType_Scalar &&
pReq->NodesToWrite[i].Value.Value.Datatype == OpcUaType_Boolean)
{
OpcUa_Variable *pVariable = (OpcUa_Variable*)pNode;
if (pUserData && pUserData->Type == UserDataMachineSwitch)
{
MachineSwitch *pSwitchData = (MachineSwitch*)pUserData;
*pSwitchData->pValue = pReq->NodesToWrite[i].Value.Value.Value.Boolean;
}
}
}
}
}
UaServer_WriteComplete(a_pWriteCtx);
return OpcUa_Good;
}
Reacting to method calls on the Machine's condition alarm
The AcknowledgeableConditionType defines methods that can be called on it's instances. Thus, we have to react on calls of this methods in the call callback method of the provider. The instance of the condition the method was called on can easily be retrieved by the UaServer_Events_GetConditionByNodeId method provided by the SDK:
{
OpcUa_CallRequest *pRequest;
OpcUa_CallResponse *pResponse;
OpcUa_Int32 i;
OpcUa_NodeId *pMethodId = OpcUa_Null;
OpcUa_ReturnErrorIfArgumentNull(a_pCallContext);
pRequest = a_pCallContext->pRequest;
pResponse = a_pCallContext->pResponse;
UaServer_Atomic_Increment(&a_pCallContext->nOutstandingCbs);
for (i = 0; i < pRequest->NoOfMethodsToCall; i++)
{
if (pRequest->MethodsToCall[i].ObjectId.NamespaceIndex == g_UaProviderSample_uNamespaceIndex &&
pRequest->MethodsToCall[i].MethodId.NamespaceIndex == 0 &&
pRequest->MethodsToCall[i].MethodId.IdentifierType == OpcUa_IdentifierType_Numeric)
{
switch (pRequest->MethodsToCall[i].MethodId.Identifier.Numeric)
{
case OpcUaId_AcknowledgeableConditionType_Acknowledge:
{
&pRequest->MethodsToCall[i].ObjectId);
if (pEvent != OpcUa_Null)
{
UaProvider_Sample_CallAcknowledge(pEvent, &pRequest->MethodsToCall[i], &pResponse->Results[i]);
}
break;
}
...
default:
break;
}
}
}
UaServer_CallComplete(a_pCallContext);
return OpcUa_Good;
}
The following code shows how to retrieve the TemperatureSensor instance that generated the condition which currently gets acknowledged.
OpcUa_StatusCode UaProvider_Sample_CallAcknowledge(
UaServer_Event *a_pEvent,
OpcUa_CallMethodRequest* a_pRequest,
OpcUa_CallMethodResult* a_pResult)
{
if (a_pRequest->NoOfInputArguments == 2)
{
a_pResult->StatusCode = OpcUa_Good;
...
...
if (pSourceNode != OpcUa_Null)
{
OpcUa_BaseNode *pBaseNode;
if (pBaseNode != OpcUa_Null)
{
if (pUserData && pUserData->Type == UserDataTemperature)
{
TemperatureSensor *pSensorData = (TemperatureSensor*)pUserData;
OpcUa_ReferenceParameter(pSensorData);
}
}
}
}
else
{
a_pResult->StatusCode = OpcUa_BadArgumentsMissing;
}
return OpcUa_Good;
}