ANSI C UA Server SDK  1.5.0.312
 All Data Structures Functions Variables Typedefs Enumerations Enumerator Modules Pages
Lesson 6: Adding Support for Alarms & Conditions

The goal of this lesson is to show how to provide alarm conditions in the address space.

Files used in this lesson:

Step 1: Adding TemperatureAlarmType

Figure 6-1 shows the new alarm type that we will create in this lesson.

Figure 6-1 TemperatureAlarmType

gettingstarted1_lesson06_temperaturealarmtype.png

In this lesson, the machine will be extended to have an alarm that is active whenever the temperature sensor’s value is below or above certain limits. The alarm type will have the same event fields as the event type of Lesson 5: Adding Support for Events. For this purpose, CustomProvider_RegisterEventTypes is extended to create and register the new alarm type (see custom_provider_events.c).

/* TemperatureAlarmType */
a_pStartingNodeId->Identifier.Numeric++;
g_TemperatureAlarmTypeId = a_pStartingNodeId->Identifier.Numeric;
parentEventTypeId.NamespaceIndex = 0;
parentEventTypeId.Identifier.Numeric = OpcUaId_LimitAlarmType;
/* Register TemperatureAlarmType as new event type in the SDK */
uStatus = UaServer_Events_RegisterEventType(a_pStartingNodeId, &parentEventTypeId);
OpcUa_GotoErrorIfBad(uStatus);
/* Register the event fields of TemperatureAlarmType in the SDK */
/* SwitchPosition */
OpcUa_QualifiedName_Clear(&eventField.BrowsePath.Elements[0]);
eventField.BrowsePath.Elements[0].NamespaceIndex = g_uCustomProvider_NamespaceIndex;
OpcUa_String_StrnCpy(&eventField.BrowsePath.Elements[0].Name,
OpcUa_String_FromCString("SwitchPosition"),
OPCUA_STRING_LENDONTCARE);
g_TemperatureAlarmType_IndexSwitchPosition = UaServer_Events_RegisterEventField(a_pStartingNodeId,
&eventField);
/* CurrentTemperature */
OpcUa_QualifiedName_Clear(&eventField.BrowsePath.Elements[0]);
eventField.BrowsePath.Elements[0].NamespaceIndex = g_uCustomProvider_NamespaceIndex;
OpcUa_String_StrnCpy(&eventField.BrowsePath.Elements[0].Name,
OpcUa_String_FromCString("CurrentTemperature"),
OPCUA_STRING_LENDONTCARE);
g_TemperatureAlarmType_IndexCurrentTemperature = UaServer_Events_RegisterEventField(a_pStartingNodeId,
&eventField);

In order to inform clients about the new alarm type, the respective nodes of the type and the event fields have to be created in the server’s address space. The code is similar as shown in the previous lesson.

The function CustomProvider_CreateMachine is used to create and initialize an instance of the new alarm type. In our case, the lifetime of the alarm is as long as the machine’s, so we will append the alarm to the user data of the machine to be able to work with it later (see custom_provider.c for the complete code):

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)
{
...
/* Create and initialize TemperatureAlarm */
nodeId.NamespaceIndex = g_uCustomProvider_NamespaceIndex;
nodeId.Identifier.Numeric = g_TemperatureAlarmTypeId;
pNewMachine->pTemperatureAlarm = UaServer_Events_CreateEvent(&nodeId);
uStatus = CustomProvider_InitializeTemperatureAlarm(pNewMachine,
pNewMachine->pTemperatureAlarm,
a_pStartingNodeId);
OpcUa_GotoErrorIfBad(uStatus);

The helper function CustomProvider_InitializeTemperatureAlarm is used to set reasonable initial values to all the event fields that won’t change during the lifetime of the alarm. For the event in the last lesson this was done inside CustomProvider_FireHeaterSwitchedEvent, as the event was fired and then deleted immediately; the TemperatureAlarm however is stored permanently with the Machine, this is why we use the custom initialization function CustomProvider_InitializeTemperatureAlarm (see custom_provider_events.c for the complete code).

Step 2: Firing the TemperatureAlarm

In our example, we use the CustomProvider_SimulationTimerCallback function to check whether the alarm should be sent. A static variable is used to remember whether the alarm is currently active. Depending on this variable and the current temperature of the machine, the alarm is sent with either active or inactive state.

IFMETHODIMP(CustomProvider_SimulationTimerCallback)(OpcUa_Void *a_pvCallbackData,
OpcUa_Timer a_hTimer,
OpcUa_UInt32 a_msecElapsed)
{
static OpcUa_Boolean bLastSwitchPosition = OpcUa_False;
static OpcUa_Boolean bTemperatureAlarmActive = OpcUa_False;
...
if ( bTemperatureAlarmActive == OpcUa_False &&
(g_bMyCustomMachineTemperature >= 100.0 || g_bMyCustomMachineTemperature <= 0.0) )
{
CustomProvider_FireTemperatureAlarm(g_pMyCustomMachine, g_pMyCustomMachine->pTemperatureAlarm);
bTemperatureAlarmActive = OpcUa_True;
}
else if ( bTemperatureAlarmActive != OpcUa_False &&
(g_bMyCustomMachineTemperature < 100.0 && g_bMyCustomMachineTemperature > 0.0) )
{
CustomProvider_FireTemperatureAlarm(g_pMyCustomMachine, g_pMyCustomMachine->pTemperatureAlarm);
bTemperatureAlarmActive = OpcUa_False;
}

The convenience function CustomProvider_FireTemperatureAlarm is used to fill the dynamic event fields of the alarm and to fire it. Each time the alarm is fired, a new EventId is set and all timestamps are updated (see custom_provider_events.c for full code).

OpcUa_StatusCode CustomProvider_FireTemperatureAlarm(Machine *a_pMachine, UaServer_Event *a_pTemperatureAlarm)
{
...
/* Set event fields of BaseEventType */
UaServer_Events_SetEventId(a_pTemperatureAlarm, &bsEventId);
UaServer_Events_SetTime(a_pTemperatureAlarm, OpcUa_DateTime_UtcNow());
UaServer_Events_SetReceiveTime(a_pTemperatureAlarm, OpcUa_DateTime_UtcNow());

The EnabledState, Retain and ActiveState event fields are set depending on the state of the machine as well. This results in the alarm being displayed (Retain == true) only if the temperature is above or below the limits, otherwise the alarm is inactive and not displayed.

if (*a_pMachine->pTemperatureSensor->pValue >= 100.0 || *a_pMachine->pTemperatureSensor->pValue <= 0.0)
{
if (*a_pMachine->pTemperatureSensor->pValue >= 100.0)
{
UaServer_Events_SetMessage(a_pTemperatureAlarm, "en", "Temperature above the limit!");
}
else if (*a_pMachine->pTemperatureSensor->pValue <= 0.0)
{
UaServer_Events_SetMessage(a_pTemperatureAlarm, "en", "Temperature below the limit!");
}
/* Set event fields of ConditionType */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_True;
UaServer_Events_SetEventField(a_pTemperatureAlarm, ConditionTypeField_EnabledState_Id, &value);
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_True;
UaServer_Events_SetEventField(a_pTemperatureAlarm, ConditionTypeField_Retain, &value);
/* Set event fields of AcknowledgeableConditionType */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_False;
UaServer_Events_SetEventField(a_pTemperatureAlarm, AcknowledgeableConditionTypeField_AckedState_Id, &value);
/* Set event fields of AlarmConditionType */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_True;
UaServer_Events_SetEventField(a_pTemperatureAlarm, AlarmConditionTypeField_ActiveState_Id, &value);
}
else
{
/* Set event fields of ConditionType */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_False;
UaServer_Events_SetEventField(a_pTemperatureAlarm, ConditionTypeField_EnabledState_Id, &value);
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_False;
UaServer_Events_SetEventField(a_pTemperatureAlarm, ConditionTypeField_Retain, &value);
/* Set event fields of AcknowledgeableConditionType */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_False;
UaServer_Events_SetEventField(a_pTemperatureAlarm, AcknowledgeableConditionTypeField_AckedState_Id, &value);
/* Set event fields of AlarmConditionType */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = OpcUa_False;
UaServer_Events_SetEventField(a_pTemperatureAlarm, AlarmConditionTypeField_ActiveState_Id, &value);
}

Finally the custom event fields SwitchPosition and CurrentTemperature are filled with current values and the alarm is fired:

/* Set custom event fields */
value.Datatype = OpcUaType_Boolean;
value.Value.Boolean = *a_pMachine->pHeaterSwitch->pValue;
UaServer_Events_SetEventField(a_pTemperatureAlarm, g_TemperatureAlarmType_IndexSwitchPosition, &value);
value.Datatype = OpcUaType_Double;
value.Value.Double = *a_pMachine->pTemperatureSensor->pValue;
UaServer_Events_SetEventField(a_pTemperatureAlarm, g_TemperatureAlarmType_IndexCurrentTemperature, &value);
uStatus = UaServer_Events_FireEvent(a_pTemperatureAlarm);

Step 3: Reacting to Method Calls on the Machine’s TemperatureAlarm

The AcknowledgeableConditionType defines methods that can be called on its instances. Thus, we have to react on calls of this methods in the CallAsync callback method of the provider. The instance of the alarm the method was called on can easily be retrieved by the UaServer_Events_GetConditionByNodeId method provided by the SDK (see custom_provider_call.c for full code):

IFMETHODIMP(CustomProvider_CallAsync)(UaServer_ProviderCallContext *a_pCallContext)
{
...
else if (pRequest->MethodsToCall[i].ObjectId.NamespaceIndex == g_uCustomProvider_NamespaceIndex
&& 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:
{
UaServer_Event *pEvent = UaServer_Events_GetConditionByNodeId(g_uCustomProvider_NamespaceIndex,
&pRequest->MethodsToCall[i].ObjectId);
if (pEvent != OpcUa_Null)
{
CustomProvider_CallAcknowledge(pEvent, &pRequest->MethodsToCall[i], &pResponse->Results[i]);
}
break;
}
default:
break;
}
}

The following code shows how to retrieve the Machine instance that generated the alarm which currently gets acknowledged.

OpcUa_StatusCode CustomProvider_CallAcknowledge(UaServer_Event *a_pEvent, OpcUa_CallMethodRequest* a_pRequest, OpcUa_CallMethodResult* a_pResult)
{
if (a_pRequest->NoOfInputArguments == 2)
{
a_pResult->StatusCode = OpcUa_Good;
...
/* get BaseNode of the SourceNode of the event and extract its UserData */
pSourceNode = ((OpcUa_Variant*)UaBase_Vector_Get(&a_pEvent->EventFields, BaseEventTypeField_SourceNode))->Value.NodeId;
if (pSourceNode != OpcUa_Null)
{
OpcUa_BaseNode *pBaseNode;
UaServer_GetNode(&g_pCustomProvider->AddressSpace, pSourceNode, &pBaseNode);
if (pBaseNode != OpcUa_Null)
{
UserDataCommon *pUserData = (UserDataCommon*)OpcUa_BaseNode_GetUserData(pBaseNode);
if (pUserData && pUserData->Type == UserDataMachine)
{
/* UserData is of type UserDataMachine, so the source of the event was a Machine.
now we can cast the UserData to a Machine struct and use it as needed. */
Machine *pMachineData = (Machine*)pUserData;
OpcUa_ReferenceParameter(pMachineData);
}
}
}

Step 4: Receiving Alarms with a Client

After implementing firing of alarms, we can test if they are received by a client.

For receiving events with UaExpert, create an Event View by clicking “Add Document”, selecting “Event View” and clicking “Add” (see Figure 6-2).

Figure 6-2 Create an Event View using UaExpert

gettingstarted1_lesson05_create_eventview_uaexpert.png

Now you can drag and drop either the “Server” or the “MyCustomMachine” object into the Configuration section of the Event View. The client will then subscribe to events that are emitted by the according object.

If the temperature excesses 100 degrees or falls below 0 degrees, the machine’s TemperatureAlarm is fired and displayed in the “Alarms” tab of the Event View. When Toggling the switch and thus causing the temperature to change to a value between 0 and 100 degrees, the alarm will be disabled as the Retain event field is set to false. This is the signal for the client to not display the alarm anymore and it will disappear from the “Alarms” tab of the Event View (see Figure 6-3).

Figure 6-3 Receive Alarms using UaExpert

gettingstarted1_lesson06_receive_machine_alarms_with_uaexpert.png