High Performance OPC UA Server SDK  1.4.1.263
Lesson 5: Adding Conditions

This lesson explains how to add own event types and fire events.

Files used in this lesson:

Preliminary Note

Conditions in OPC UA are strongly tied to Events, basically Conditions are Events, that are persisted in the server to indicate the current state (the Condition). This tie is also visible in the address space, all Conditions are subtypes of the ConditionType, which itself is a subtype of the BaseEventType. So from that perspective every Condition is also an Event, however the server will create an Event as a snapshot from a Condition to send it to the client only at certain times. It is also possible for Conditions to be exposed as Object in the address space, but this is optional, so a client may not be able to see Conditions directly.

alarms_conditions_hierarchy.png
Type hierarchy for Events, Conditions and Alarms

Alarms are a subtype of Conditons (with AcknowledgeableConditionType inbetween), so these are nothing special, just Conditions with a few extra Variables and Methods like other subtypes of the ConditionType. This lesson will show how to implement the simplest Alarm, the AlarmConditionType to cover the basics for implementing other Alarms or other types of Conditions.

As Conditions rely on Events it is strongly recommended to finish the previous lesson about Events first. There are also a few Methods involved, so Lesson 3 about Method implementation is also recommended.

Step 1: Manage Conditions

There are multiple Conditions in the server, so there is some management for the Conditions required. For each Condition there is some additional application specific information required, so a custom struct for Conditons is defined:

struct custom_provider_condition {
struct uaserver_event *cond;
ua_node_t node;
bool active;
bool acked;
};

The first member is the struct used by the SDK to represent Conditions, as Conditions are so similar to Events, the SDK uses the same struct uaserver_event to represent both. However to be a Condition the struct must be created using a different function as will be shown further below. The second member is the handle of the node the Condition is associated to, in this example the Conditions will be associated to the Variables created in Lesson 2 and depending on their current value and their EURange the Condition will be active. The remaining members are states of the Condition, these could also be read from the Condition struct of the SDK, but this way is easier and less complicated.

This example will use only a small number of Conditions, so there is only a small array required and few simple lookup functions. For larger amounts of Conditions it is recommended to implement a more efficient lookup algorithm. For connecting the values of the Condition instances to the respective Conditions there is also a Condition store:

#define CUSTOM_PROVIDER_MAX_CONDITIONS 5
/* condition store for condition instances */
static struct uaserver_condition_store g_condition_store;
/* array for storing conditions */
static struct custom_provider_condition g_conditions[CUSTOM_PROVIDER_MAX_CONDITIONS];
static int g_num_conditions = 0;
/* lookup condition by ConditionId */
struct custom_provider_condition *custom_provider_condition_find_by_id(const struct ua_nodeid *id)
{
const struct ua_nodeid *cond_id;
int i;
for (i = 0; i < g_num_conditions; i++) {
cond_id = uaserver_condition_get_condition_id(g_conditions[i].cond);
if (cond_id && ua_nodeid_compare(cond_id, id) == 0) {
return &g_conditions[i];
}
}
return NULL;
}
/* lookup condition by associated node handle */
static struct custom_provider_condition *custom_provider_condition_find_by_node(ua_node_t node)
{
int i;
for (i = 0; i < g_num_conditions; i++) {
if (g_conditions[i].node == node) {
return &g_conditions[i];
}
}
return NULL;
}

Step 2: Create Conditions

In this example the Conditions exist during the complete lifetime of the server, so these are created at server startup in the condition init function called by the provider init function. This function initializes the Condition store and Condition Methods then it iterates through the child nodes of the Event Notifier added in the last lesson as these are also the nodes, for which Conditions are created:

/* initialize eventing for the provider */
int custom_provider_condition_init(void)
{
ua_node_t node, dst_node;
ua_ref_t ref;
int ret;
ret = uaserver_condition_store_init(&g_condition_store, NULL);
if (ret != 0) return ret;
ret = custom_provider_condition_methods_init();
if (ret != 0) return ret;
node = ua_node_find_numeric(g_custom_provider_nsidx, EVENT_NOTIFIER_ID);
if (node == UA_NODE_INVALID) return UA_EBADNOTFOUND;
ua_node_foreach(ref, node) {
if (ua_reference_type(ref) != UA_NODE_HASEVENTSOURCE) {
continue;
}
dst_node = ua_reference_target(ref);
ret = custom_provider_create_alarm(dst_node);
if (ret != 0) return ret;
}
return 0;
}

The actual Conditions - which are also Alarms in this lesson - are created in a helper function. Conditions are created with uaserver_condition_create, which requires a number of arguments:

  • ConditionId: Unique NodeId for the condition, in case the conditon is exposed in the address space this must be its NodeId.
  • ConditionName: Identifies the condition instance that the event originated from. It can be used together with the SourceName in a user display to distinguish between different condition instances
  • ConditionType: Type of the condition, must be a subtype of the ConditionType node.
  • ConditionSource: Node the condition is assocciated with.
  • ConditionClassId: Specifies in which domain this Condition is used, must be a subtype of BaseConditionClassType.

The created Condition has a number of fields just like an Event, uaserver_condition_create will take care of all mandatory fields from the CondtionType (and the BaseEventType), but the fields from the AcknowledgeableConditionType and the AlarmConditionType must still be set. These fields can be set with the event setter functions as seen in the last lesson, many of these field are of the TwoStateVariableType, so a a setter for this specific type was added to the SDK.

Next an instance of the Condition is created in the address space, this will be covered in detail in the next step. At last the Conditon is added to the Condition management inside the provider.

static int custom_provider_create_alarm(ua_node_t variable_node)
{
struct uaserver_event *cond = NULL;
struct ua_nodeid n;
int ret;
if (g_num_conditions == CUSTOM_PROVIDER_MAX_CONDITIONS) {
return UA_EBADOUTOFRESOURCE;
}
/* find ConditionType node */
type = ua_node_find0(UA_ID_ALARMCONDITIONTYPE);
/* create ConditionId */
ua_nodeid_set_numeric(&n, g_custom_provider_nsidx, CONDITION_INSTANCE_START_ID + (50*g_num_conditions));
/* create the condition struct provided by the SDK */
cond = uaserver_condition_create(&n, ua_node_get_displayname(variable_node), type, variable_node, ua_node_find0(UA_ID_PROCESSCONDITIONCLASSTYPE));
/* *** AcknowledgeableConditionType *** */
/* AckedState */
ret = uaserver_event_set_twostatevariable(cond, "AckedState", 0, "en", "Acked", true);
if (ret != 0) goto error;
/* ConfirmedState */
ret = uaserver_event_set_twostatevariable(cond, "ConfirmedState", 0, "en", "Confirmed", true);
if (ret != 0) goto error;
/* *** AlarmConditionType *** */
/* ActiveState */
ret = uaserver_event_set_twostatevariable(cond, "ActiveState", 0, "en", "Inactive", false);
if (ret != 0) goto error;
/* InputNode */
ret = ua_node_get_nodeid(variable_node, &n);
if (ret != 0) goto error;
ret = uaserver_event_set_nodeid(cond, uaserver_eventfield_lookup_qn("InputNode", 0), &n);
if (ret != 0) goto error;
/* SuppressedOrShelved */
ret = uaserver_event_set_bool(cond, uaserver_eventfield_lookup_qn("SuppressedOrShelved", 0), false);
if (ret != 0) goto error;
/* create the instance in the address space for that condition */
ret = custom_provider_create_alarm_instance(cond, type, variable_node);
if (ret != 0) goto error;
/* set the provider condition struct */
g_conditions[g_num_conditions].node = variable_node;
g_conditions[g_num_conditions].cond = cond;
g_conditions[g_num_conditions].active = false;
g_conditions[g_num_conditions].acked = true;
g_num_conditions++;
return 0;
error:
if (cond) uaserver_condition_delete(cond);
return ret;
}

Step 3: Create Condition Instances (optional)

This step shows how create an instance of a Condition in the address space. The instance is not required by the OPC UA Specification and it is perfectly fine to use the Condition without a representation in the address space, anybody not obligated to create a Condition instance may view this as a general example on how to create instances or just skip this step and continue with the next one.

The instance is created with the SDK's instance functionality, which allows fine grained control over the nodes being added to the instance. In this case only the mandatory nodes are added to the instance with the exception of the Confirm Method, the ConfirmedState and its Property, the Id. To achieve this a struct is defined to hold the handles for these optional nodes to access in the callback:

struct create_alarm_ctx {
ua_node_t *optional_nodes;
uint32_t num_optional_nodes;
};

To create the instance, the array for optional nodes is created and some information for the instance is gathered. Then the ua_instance_ctx is created and the parent node for the instance and callbacks/callback data is set. The instance itself is created with ua_instance_new, inside this function the callback shown further below will be called. Now the instance and all child nodes exists, but the values are still missing, so another function is called to connect the instance nodes to the condition fields: uaserver_condition_link_to_node. It is called with the Condition store and a function provided by the Condition store, this directly connects the values of the nodes to the condition fields, so when a field is updated e.g. with uaserver_event_set_* functions, the next client read of corresponding node will yield the updated value. At last an additional reference is added from the Variable node to the new instance, as the HasCondition reference is non-hierachic the instance node will not be shown by clients like UaExpert, so another hierachical reference is added.

static int custom_provider_create_alarm_instance(struct uaserver_event *cond, ua_node_t type, ua_node_t variable_node)
{
const struct ua_variant *val;
const struct ua_nodeid *id;
struct ua_instance_ctx *ctx = NULL;
struct create_alarm_ctx alarm_ctx;
const char *name;
int ret;
/* array of optional nodes, but still to be added to the instance */
ua_node_t optional[] = {
ua_node_find0(UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_CONFIRM),
ua_node_find0(UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_CONFIRMEDSTATE),
ua_node_find0(UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_CONFIRMEDSTATE_ID),
};
/* the nodeid for the instance is the ConditionId */
if (val == NULL || val->type != UA_VT_NODEID) return UA_EBADINTERNALERROR;
id = val->value.nodeid;
/* the displayname of the Variable is used as displayname and browsename for the instance */
name = ua_node_get_displayname(variable_node);
if (name == NULL) {
ret = UA_EBAD;
goto out;
}
/* create instance_ctx */
if (ctx == NULL) {
ret = UA_EBAD;
goto out;
}
/* set parent and callback */
ua_instance_set_parent(ctx, variable_node, ua_node_find0(UA_ID_HASCONDITION));
ua_instance_set_pre_create_cb(ctx, custom_provider_pre_create_cb);
alarm_ctx.optional_nodes = optional;
alarm_ctx.num_optional_nodes = countof(optional);
ua_instance_set_cb_data(ctx, &alarm_ctx);
/* let the SDK create the actual instance */
ret = ua_instance_new(ctx, id, type, id->nsindex, name, name);
if (ret != 0) goto out;
/* connect values of the instance with the condition */
cond,
&g_condition_store,
(uaserver_condition_add_node_to_store_t) uaserver_condition_store_add_node
);
if (ret < 0) goto out;
/* add reference, so the instance is visible in clients like UaExpert */
ua_reference_add0(variable_node, ua_instance_get_new_node(ctx), UA_ID_HASCOMPONENT);
ret = 0;
out:
if (ctx) ua_instance_ctx_delete(ctx);
return ret;
}

A note for those using a static address space: It is possible to have the Condition instances in a static address space, in this case the values of the nodes can also be connected to the instance using uaserver_condition_link_to_node and the Condition store, but it is required that the store indices of the generated instances match the store index of the Condition store. The generated value indices should start at zero and need to be unique and coninuous among all nodes of all instances.

The callback called by ua_instance_new ensures the correct nodes are added to the instance in the correct way. For optinal nodes only these in the optional nodes array are added, mandatory nodes are always added. For all added nodes, Methods are only added by reference, so a only a reference to the declaration node is created, for all other nodes a copy of the declaration node is created:

static enum ua_instance_node_action custom_provider_pre_create_cb(
struct ua_instance_ctx *ctx,
ua_node_t declaration_node,
enum ua_nodeclass nc,
struct ua_nodeid *nodeid)
{
struct create_alarm_ctx *alarm_ctx = ua_instance_get_cb_data(ctx);
uint32_t i;
UA_UNUSED(nodeid);
if (m_rule == UA_INSTANCE_MODELLING_RULE_OPTIONAL) {
/* check if optional node is to be added */
for (i = 0; i < alarm_ctx->num_optional_nodes; i++) {
if (declaration_node == alarm_ctx->optional_nodes[i]) {
if (nc == UA_NODECLASS_METHOD) return UA_INSTANCE_NODE_REFERENCE;
}
}
/* ignore optional node */
}
/* add methods by reference, copy otherwise */
if (nc == UA_NODECLASS_METHOD) return UA_INSTANCE_NODE_REFERENCE;
}

Step 4: Change Conditions and Generate Events

So far the provider has multiple Conditions and even nodes in the address space to represent these, but they are all static, so in this step the Conditions will be given life. Just like with the Events in the last lesson a notify function is added to the value store to notify about new values written to one of the variables:

newval = value->value.value.ui32;
/* check range */
if (newval > g_custom_provider_ranges[idx].eu_high ||
newval < g_custom_provider_ranges[idx].eu_low) {
custom_provider_event_notifiy_value_change(node, newval, true);
custom_provider_condition_notify_value_change(node, newval, true);
} else {
custom_provider_event_notifiy_value_change(node, newval, false);
custom_provider_condition_notify_value_change(node, newval, false);
}

This function sets the Condition associated to the variable active whenever the new value written is out of range of the EURange, when a value inside the EURange is written the Condition is set back to inactive:

int custom_provider_condition_notify_value_change(ua_node_t node, uint32_t value, bool outofrange)
{
struct custom_provider_condition *condition;
struct uaserver_event *cond;
int ret;
UA_UNUSED(value);
condition = custom_provider_condition_find_by_node(node);
if (condition == NULL) return UA_EBADNOTFOUND;
cond = condition->cond;
if (outofrange) {
if (condition->active) {
/* condition is already active, nothing to do */
return 0;
}
/* reset confirmed and acknowledged state */
ret = uaserver_event_set_twostatevariable(cond, "ConfirmedState", 0, "en", "Unconfirmed", false);
if (ret != 0) goto error;
ret = uaserver_event_set_twostatevariable(cond, "AckedState", 0, "en", "Unacked", false);
if (ret != 0) goto error;
condition->acked = false;
ret = uaserver_event_set_twostatevariable(cond, "ActiveState", 0, "en", "Active", true);
if (ret != 0) goto error;
ret = uaserver_event_set_severity(cond, 500);
if (ret != 0) goto error;
if (ret != 0) goto error;
condition->active = true;
if (ret != 0) goto error;
} else {
if (!condition->active) {
/* condition is already inactive, nothing to do */
return 0;
}
ret = uaserver_event_set_twostatevariable(cond, "ActiveState", 0, "en", "Inactive", false);
if (ret != 0) goto error;
ret = uaserver_event_set_severity(cond, 200);
if (ret != 0) goto error;
if (ret != 0) goto error;
condition->active = false;
if (ret != 0) goto error;
}
return 0;
error:
return ret;
}

When changing the state of a Condition there are two important mechanisms:

  • Retain: Retain is a Property of the ConditionType and must be set to True when a Condition changes to a state that is interesting for a client wishing to synchronize its state with the server's state. Changing this Property must be done via the uaserver_condition_retain_enable/uaserver_condition_retain_disable functions as these will not only set the Property but also manage the Conditions in a list. This allows the client to request all the Conditions of interest by using the Refresh Method, which is implemented by the SDK. The logic to determine the value of this Property is according to the OPC UA Specification server specific, but typically all active Alarms would have the Retain Property set to enabled, however it is also possible for inactive Alarms to have their Retain Property enabled.
  • Generate Events: Sometimes an Event must be generated from a Condition, the OPC UA Specification explicitly requires this for changes to the following components of a Condition: Quality, Severity and Comment. It also says this may not be the complete list as subtypes may define additional Variables that trigger Event generation and in general, changes to Variables of the types TwoStateVariableType or ConditionVariableType would trigger Event generation. As Conditions and Events are of the same struct uaserver_event it is possible to just call uaserver_eventnotifier_fire_event with the Condition to generate an Event, however it is required for each Event generated from a Condition to have a unique EventId. So for convenience the SDK provides the function uaserver_condition_fire_event, which will update the EventId with a new unique value, update the time of the Condition and fire an Event.

Step 5: Implement Condition Methods

The Condittions are working now, but there are a few Methods which still need to be implemented. This step mostly follows the instructions from Lesson 3 on how to implement Methods, the code can be found in custom_provider_condition_methods.c.

Init

The init function registers the method handlers, these are registered for the global scope to cover the case where no instance of the Condition exists and this is the only way to get called:

static struct uaserver_call_table_numeric g_condition_methods[] = {
{&g_uaprovider_server_nsidx, UINT32_MAX, UA_ID_CONDITIONTYPE_ADDCOMMENT, custom_provider_condition_method_handler2},
{&g_uaprovider_server_nsidx, UINT32_MAX, UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_CONFIRM, custom_provider_condition_method_handler2},
{&g_uaprovider_server_nsidx, UINT32_MAX, UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_ACKNOWLEDGE, custom_provider_condition_method_handler2},
{&g_uaprovider_server_nsidx, UINT32_MAX, UA_ID_CONDITIONTYPE_ENABLE, custom_provider_condition_method_handler1},
{&g_uaprovider_server_nsidx, UINT32_MAX, UA_ID_CONDITIONTYPE_DISABLE, custom_provider_condition_method_handler1},
};
int custom_provider_condition_methods_init(void)
{
return uaserver_call_table_register_numeric(g_condition_methods, countof(g_condition_methods));
}

Check Condition

As the handler functions are registered for all objects, these will have to check the ObjectId (which is the ConditionId for Conditions) themselves. This function will lookup the Condition and also verify the EventId provided by the caller matches the current EventId of the Condition. Furthermore there is an access check which will only allow clients that have the permissions to write to the associated variable to also call the Method. This check however is application specific and may done by some other logic:

static ua_statuscode custom_provider_method_check_condition(const struct ua_nodeid *object_id,
const struct ua_bytestring *event_id,
struct uasession_session *session,
struct custom_provider_condition **cond)
{
/* lookup the condition */
*cond = custom_provider_condition_find_by_id(object_id);
if (*cond == NULL) return UA_SCBADNODEIDUNKNOWN;
/* check the event_id of the condition */
if (event_id) {
const struct ua_variant *cond_event_id = uaserver_event_get_field_value((*cond)->cond, uaserver_eventfield_base_event_id);
if (cond_event_id == NULL || cond_event_id->type != UA_VT_BYTESTRING) return UA_SCBADINTERNALERROR;
if (ua_bytestring_compare(event_id, &cond_event_id->value.bs) != 0) {
}
}
#ifdef UA_AUTHORIZATION_SUPPORT
/* use related node to check access permissions */
return ua_authorization_is_writable((*cond)->node, &session->user_ctx);
#else
UA_UNUSED(session);
return 0;
#endif
}

Handler 1

There is a total of 5 Methods to implment, however there are only 2 distinct sets of input/output arguments, so here are only two argument handler functions implement, which will dispatch calls by the MethodId to the correct function.

The first of these handlers takes care of the Enable and Disable Methods, which have no input arguments at all. Still the function needs to check the client actually sends no arguments, it also needs to check the ObjectId is a valid Condition using the function above. The Conditions in this example are always enabled, so depending on the Method called only a different bad statuscode is returned:

static ua_statuscode custom_provider_condition_method_handler1(
struct uaprovider_call_ctx *ctx,
const struct ua_callmethodrequest *req,
struct ua_callmethodresult *res)
{
ua_statuscode status_code;
struct custom_provider_condition *cond;
/* check argument types */
status_code = uaserver_call_utility_check_arguments(NULL, 0, req, res);
if (status_code != 0) return status_code;
/* get and validate the condition */
status_code = custom_provider_method_check_condition(&req->object_id, NULL, ctx->session, &cond);
if (status_code != 0) return status_code;
/* enable/disable is not supported, so there is not much to do here */
switch (req->method_id.identifier.numeric) {
case UA_ID_CONDITIONTYPE_ENABLE:
case UA_ID_CONDITIONTYPE_DISABLE:
default:
}
}

Handler 2

The second handler is a bit more complex as there are 2 input arguments which need to be validated. It will also lookup the Condition and call the related function for the actual handling:

static ua_statuscode custom_provider_condition_method_handler2(
struct uaprovider_call_ctx *ctx,
const struct ua_callmethodrequest *req,
struct ua_callmethodresult *res)
{
const struct uaserver_call_utility_arg in_args[] = {
{NULL, 0, UA_VT_BYTESTRING, UASERVER_CALL_UTILITY_FLAG_NONE},
{NULL, 0, UA_VT_LOCALIZEDTEXT, UASERVER_CALL_UTILITY_FLAG_NONE},
};
ua_statuscode status_code;
struct ua_bytestring *event_id;
struct ua_localizedtext *comment;
struct custom_provider_condition *cond;
/* check argument types */
status_code = uaserver_call_utility_check_arguments(in_args, countof(in_args), req, res);
if (status_code != 0) return status_code;
event_id = &req->input_arguments[0].value.bs;
comment = req->input_arguments[1].value.lt;
/* get and validate the condition */
status_code = custom_provider_method_check_condition(&req->object_id, event_id, ctx->session, &cond);
if (status_code != 0) return status_code;
/* dispatch to the correct function */
switch (req->method_id.identifier.numeric) {
case UA_ID_CONDITIONTYPE_ADDCOMMENT:
return custom_provider_condition_add_comment(cond, ctx->session, comment);
case UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_CONFIRM:
return custom_provider_condition_confirm(cond, ctx->session, comment);
case UA_ID_ACKNOWLEDGEABLECONDITIONTYPE_ACKNOWLEDGE:
return custom_provider_condition_acknowledge(cond, ctx->session, comment);
default:
}
}

Function Implementation

The actual implementations depend on the Method, all of them set the provided comment and the associated SourceTimestamp and ClientUserId. The acknowledge and confirm functions also set the respective new state after checking the current state. The acknowledge function reads the acked state from the provider's Condition struct, the confirm function reads the confirmed state directly from the Condition, both are viable options. As all of them change the Comment field, all need to generate a new event from the Condition:

static ua_statuscode custom_provider_condition_add_comment(struct custom_provider_condition *cond, struct uasession_session *session, struct ua_localizedtext *comment)
{
int ret;
if (ret != 0) goto error;
ret = uaserver_event_set_datetime(cond->cond, uaserver_eventfield_lookup_va(2, "Comment", 0, "SourceTimestamp", 0), now);
if (ret != 0) goto error;
if (ret != 0) goto error;
ret = uaserver_condition_fire_event(cond->cond);
if (ret != 0) goto error;
return 0;
error:
}
static ua_statuscode custom_provider_condition_acknowledge(struct custom_provider_condition *cond, struct uasession_session *session, struct ua_localizedtext *comment)
{
int ret;
if (cond->acked) return UA_SCBADCONDITIONBRANCHALREADYACKED;
ret = uaserver_event_set_twostatevariable(cond->cond, "AckedState", 0, NULL, "Acked", true);
if (ret != 0) goto error;
if (ret != 0) goto error;
ret = uaserver_event_set_datetime(cond->cond, uaserver_eventfield_lookup_va(2, "Comment", 0, "SourceTimestamp", 0), now);
if (ret != 0) goto error;
if (ret != 0) goto error;
ret = uaserver_condition_fire_event(cond->cond);
if (ret != 0) goto error;
cond->acked = true;
return 0;
error:
}
static ua_statuscode custom_provider_condition_confirm(struct custom_provider_condition *cond, struct uasession_session *session, struct ua_localizedtext *comment)
{
const struct ua_variant *confirmed_state;
int ret;
/* read the ConfirmedState directly from the condition */
confirmed_state = uaserver_event_get_field_value(cond->cond, uaserver_eventfield_lookup_va(2, "ConfirmedState", 0, "Id", 0));
if (confirmed_state == NULL || confirmed_state->type != UA_VT_BOOLEAN) return UA_SCBADINTERNALERROR;
if (confirmed_state->value.b) return UA_SCBADCONDITIONBRANCHALREADYCONFIRMED;
ret = uaserver_event_set_twostatevariable(cond->cond, "ConfirmedState", 0, NULL, "Confirmed", true);
if (ret != 0) goto error;
if (ret != 0) goto error;
ret = uaserver_event_set_datetime(cond->cond, uaserver_eventfield_lookup_va(2, "Comment", 0, "SourceTimestamp", 0), now);
if (ret != 0) goto error;
if (ret != 0) goto error;
ret = uaserver_condition_fire_event(cond->cond);
if (ret != 0) goto error;
return 0;
error:
}

Step 6: Delete Conditions

In this example the Conditions exist through the complete lifetime of the server, so they only need to be deleted at server shutdown. The custom_provider_cleanup function from custom_provider.c calls custom_provider_condition_clear which deletes all conditions. In this lesson the address space would be deleted anyway, so it is not really necessary to remove the instances, but in a server with dynamically created Conditions it is important to clean these up properly to avoid memory leaks at runtime. For a server that does not create instances it is sufficient to call uaserver_condition_delete.

/* clear eventing for the provider */
void custom_provider_condition_clear(void)
{
int i;
for (i = 0; i < g_num_conditions; i++) {
/* remove connection from node to value */
/* remove values from the condition store */
uaserver_condition_store_remove_condition(&g_condition_store, g_conditions[i].cond);
/* delete the instance in the address space */
ua_instance_remove(NULL, g_conditions[i].node);
/* delete the condition */
uaserver_condition_delete(g_conditions[i].cond);
}
g_num_conditions = 0;
uaserver_condition_store_clear(&g_condition_store);
return;
}

Step 7: Activate and View Conditions with UAExpert

After building the example and connecting with UAExpert the Event View can be opened by using the Document->Add dialog:

uaexpert_add_event_view.png

Event notifiers can be added via drag and drop in the Configuration window, for the example either the MyEventNotifier or Server object can be added. The Events windows will then show the RefreshStartEvent and RefreshEndEvent, but there are no active Conditions yet. To activate a Condition, a value that is out of range of the EURange must be written to a variable, so e.g. by writting a value of 100 to Variable1 the Condition for Variable1 is activated and can be seen in the Events window. Additional to the Condition also the OutOfRangeEvent from the previous lesson is shown:

uaexpert_variable1_event.png

When switching to the Alarms tab only Alarms are shown:

uaexpert_variable1_alarm.png

By right-clicking on an Alarm it is possible to call its Acknowledge, Confirm and Add Comment Methods implemented above.