High Performance OPC UA Server SDK
1.4.1.263
|
This lesson explains how to add own event types and fire events.
Files used in this lesson:
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 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.
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:
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:
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:
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:
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.
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:
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.
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:
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:
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:
When changing the state of a Condition there are two important mechanisms:
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.
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:
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:
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:
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:
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:
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.
After building the example and connecting with UAExpert the Event View can be opened by using the Document->Add dialog:
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:
When switching to the Alarms tab only Alarms are shown:
By right-clicking on an Alarm it is possible to call its Acknowledge, Confirm and Add Comment Methods implemented above.