UA Ansi C Server Professional  1.3.1.232
 All Data Structures Functions Variables Typedefs Enumerations Enumerator Groups Pages
SDK Tutorial - How to use alarms in your application

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 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.
struct _UserDataCommon
{
UserDataType Type;
};
typedef struct _UserDataCommon UserDataCommon;
struct _Machine
{
// user data header
UserDataType Type;
// user data (machine data)
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()
{
...
// create Machine object
uStatus = UaProvider_Sample_CreateMachine("MyMachine",
"MyTemperatureSensor",
(OpcUa_BaseNode*)pFolderEvents,
&NodeId,
&g_pMyMachine);
OpcUa_GotoErrorIfBad(uStatus);
// assign simulation variables
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)
{
...
// simulate sensors of MyMachine
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;
}
// check the Machine for value changes
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;
// check temperature
oldTemperature = OpcUa_Variable_GetValue(a_pMachine->pTemperatureSensor->pTemperatureProperty)->Value.Double;
if (oldTemperature != *a_pMachine->pTemperatureSensor->pValue)
{
// temperature has changed, so set it to TemperatureSensor property of the Machine object
OpcUa_Variant newValue;
OpcUa_Variant_Initialize(&newValue);
OpcUa_Variant_Clone(OpcUa_Variable_GetValue(a_pMachine->pTemperatureSensor->pTemperatureProperty), &newValue);
newValue.Value.Double = *a_pMachine->pTemperatureSensor->pValue;
OpcUa_Variable_SetValue(a_pMachine->pTemperatureSensor->pTemperatureProperty, &newValue);
// check if the temperature has over-/underrun certain values and fire events if necessary
if (oldTemperature < 100 && *a_pMachine->pTemperatureSensor->pValue >= 100)
{
UaServer_Event *pHighAlarm = UaServer_Events_GetConditionByNodeId(g_UaProviderSample_uNamespaceIndex,
&a_pMachine->pTemperatureSensor->HighAlarmId);
UaProvider_Sample_FireAcknowledgeableConditionTypeEvent(pHighAlarm);
}
if (oldTemperature > 0 && *a_pMachine->pTemperatureSensor->pValue <= 0)
{
UaServer_Event *pLowAlarm = UaServer_Events_GetConditionByNodeId(g_UaProviderSample_uNamespaceIndex,
&a_pMachine->pTemperatureSensor->LowAlarmId);
UaProvider_Sample_FireAcknowledgeableConditionTypeEvent(pLowAlarm);
}
if (oldTemperature >= 100 && *a_pMachine->pTemperatureSensor->pValue < 100)
{
UaServer_Event *pHighAlarm = UaServer_Events_GetConditionByNodeId(g_UaProviderSample_uNamespaceIndex,
&a_pMachine->pTemperatureSensor->HighAlarmId);
UaProvider_Sample_DisableAcknowledgeableConditionTypeEvent(pHighAlarm);
}
if (oldTemperature <= 0 && *a_pMachine->pTemperatureSensor->pValue > 0)
{
UaServer_Event *pLowAlarm = UaServer_Events_GetConditionByNodeId(g_UaProviderSample_uNamespaceIndex,
&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:

IFMETHODIMP(UaProvider_Sample_WriteAsync)(UaServer_ProviderWriteContext* a_pWriteCtx)
{
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;
// we will send exactly one callback
UaServer_Atomic_Increment(&a_pWriteCtx->nOutstandingCbs);
for (i = 0; i < pReq->NoOfNodesToWrite; i++)
{
if (pReq->NodesToWrite[i].NodeId.NamespaceIndex == g_UaProviderSample_uNamespaceIndex)
{
UaServer_GetNode(pAddressSpace, &pReq->NodesToWrite[i].NodeId, &pNode);
if (pNode)
{
...
// send events
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;
// check if UserData is available.
// if type of data is UserDataMachineSwitch, write data to it's value.
UserDataCommon *pUserData = (UserDataCommon*)OpcUa_BaseNode_GetUserData(pNode);
if (pUserData && pUserData->Type == UserDataMachineSwitch)
{
MachineSwitch *pSwitchData = (MachineSwitch*)pUserData;
*pSwitchData->pValue = pReq->NodesToWrite[i].Value.Value.Value.Boolean;
}
}
}
}
}
// send callback
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:

IFMETHODIMP(UaProvider_Sample_CallAsync)(UaServer_ProviderCallContext* a_pCallContext)
{
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;
// we will send exactly one callback
UaServer_Atomic_Increment(&a_pCallContext->nOutstandingCbs);
for (i = 0; i < pRequest->NoOfMethodsToCall; i++)
{
// is a method of the UA address space called?
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:
{
// get the condition instance by it's ConditionId
UaServer_Event *pEvent = UaServer_Events_GetConditionByNodeId(g_UaProviderSample_uNamespaceIndex,
&pRequest->MethodsToCall[i].ObjectId);
if (pEvent != OpcUa_Null)
{
UaProvider_Sample_CallAcknowledge(pEvent, &pRequest->MethodsToCall[i], &pResponse->Results[i]);
}
break;
}
...
default:
break;
}
}
}
// send callback
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;
// check if method is called correctly
...
// set the event to acknowledged and fire it
...
// get BaseNode of the SourceNode of the event and extract it's UserData
pSourceNode = ((OpcUa_Variant*)UaServer_Vector_Get(&a_pEvent->EventFields, BaseEventTypeField_SourceNode))->Value.NodeId;
if (pSourceNode != OpcUa_Null)
{
OpcUa_BaseNode *pBaseNode;
UaServer_GetNode(&g_pSampleProvider->AddressSpace, pSourceNode, &pBaseNode);
if (pBaseNode != OpcUa_Null)
{
UserDataCommon *pUserData = (UserDataCommon*)OpcUa_BaseNode_GetUserData(pBaseNode);
if (pUserData && pUserData->Type == UserDataTemperature)
{
// UserData is of type UserDataTemperature, so the source of the event was a TemperatureSensor.
// now we can cast the UserData to a TemperatureSensor struct and use it as needed.
TemperatureSensor *pSensorData = (TemperatureSensor*)pUserData;
OpcUa_ReferenceParameter(pSensorData);
}
}
}
}
else
{
a_pResult->StatusCode = OpcUa_BadArgumentsMissing;
}
return OpcUa_Good;
}