High Performance OPC UA Server SDK
1.4.1.263
|
This lesson explains how to implement access to data from a asynchronously connnected device.
This is achieved by implementing custom service handler functions to provide the values.
Files used in this lesson:
The server is similar to Lesson 2: Custom Provider with Value Stores and provides three 32bit unsigned integer variables from a arbitrary device. The difference is in the implementation, in this lesson the service handlers are implemented directly instead of using the value store for accessing the device. Integrating a device that way is more complex, but gives more freedom in the implementation and allows mass operations, asynchronous read from the device or the direct handling of monitored items by the device.
The custom provider is again initialized with the init function in custom_provider.c that is similar to lesson 2. The custom store to access the device has been removed, but the memorystore is still there and will contain the property values. Also the registered service handler functions have changed to the functions that will be implemented in this lesson:
Creating the nodes happens in custom_provider_nodes.c and is also very similar to lesson 2. There is a function for creating the property EURange and insert its value in the memorystore and there is a function for creating the variable node for the device's values. The difference here is the initial values of the device cannot be added to the store, but is directly written into the array. Also the store index must not be set, but the valueindex is still used to link the node to the value in the array:
The read service handler is implemented in custom_provider_read.c and consists of two functions. The first is a helper function for applying some checks and reading the value, this function is also used to read values for monitored items. In detail it needs to check the nodeclass, the access level, the access permissions and the indexrange. Then the valueindex from the node is retrieved, its range is checked and the value is copied from the value array to the result. At last the sourcetimestamp and servertimestamp must be added, if requested:
The second function is the actual read service handler. As parameter it gets passed the uaprovider_read_ctx with the attached ua_readrequest and ua_readresponse. It iterates over all nodes in the read request to be read and skips all nodes that don't belong to the custom provider's namespace. Next the node handle for the current nodeid is looked up and the indexrange is parsed. Now the value can actually be read, all non-value attributes are handled by uaserver_read_internal. For value attributes it is checked if the store index is set, which means here the value is in the memory store. In this case the value is also read using uaserver_read_internal, that will read the value from the store. If the store index is not set, then the value must be read from the device by the helper function implemented above. When the loop is finished uaserver_read_complete is called to tell the SDK this provider has finished processing the request:
This implementation reads each value separatly from memory, so it effectively behaves like the implementation from lesson 2 using the value store. But this implementation allows not only for a synchronous read of values, it would also be possible to start a read operation for all values from the device and let the function return without calling uaserver_read_complete. When all operations have asynchronously returned from the device the response can be created and uaserver_read_complete must be called for the SDK to finished processing the response.
Implementing the write service handler is analog to the read service handler, except for being implemented in one function only. This function takes an uaprovider_write_ctx with the attached ua_writerequest and ua_writeresponse. Again it needs to iterate through all nodes that are to be written and skips nodes from foreign namespaces. This write implementation only allows to write the value attribute, so all other attributes are skipped and only nodes from class variable are allowed. Writting with indexrange is also not supported, as the example nodes have scalars only. If the store index is set, the uaserver_write_internal function is used to write the value into the store and continue with the next node. If the store index is not set the node is from the device and the access level and access permissions must be checked. Then the value index is retrieved from the node and verified it is in bound of the array. After checking the datatype of the value to write it can finally be written to the value array representing the device. When all nodes are written, uaserver_write_complete must be used for the SDK to continue processing the response:
Like in the read implementation it is also possible to process requests asynchronously by first starting the write operation in the service handler function and finish it later in another function by calling uaserver_write_complete when all write operations have finished.
This implementation of the monitored item service handlers uses a timer for each monitored item to poll the value with the sampling rate and inform the monitored item about the new value. If the underlying device has a better way for reporting data changes it should be used. So this implementation is mainly about explaining the monitored item mechanism and showing what to do in the different service handler functions, but there may be a better way on how to do it in your implementation.
When a client requests to create monitored items, for each item a ua_monitoreditem struct is allocated and passed to the add item service handler. The handler function retrieves the node handle and session from the monitored item.
If a deadband filter is requested for the monitored item it is verified the value is a numeric datatype, otherwise the item must be rejected. The deadband filter itself is applied by the SDK when calling ua_monitoreditem_new_value.
If the deadband filter is of type percent deadband, the SDK needs to know the EURange of the value. The example uses ua_monitoreditem_node_read_eurange to find that property and read its value from the local address space, however this assumes the property does not change and thus only creates the monitored item if the EURange property is not writeable. The low and high value of the EURange are set on the montiored item, so these can be used for applying the deadband filter. Also note that percent deadband is only active if the CMake option UASERVER_SUPPORT_DEADBAND_PERCENT is enabled, otherwise the SDK rejects monitored items with percent deadband filters and this code can be omitted.
Next the initial value to send to the client is read. If the statuscode of the this value is bad, there is no monitored item created by the provider and the statuscode is returned and the SDK will delete the ua_monitoreditem struct and send the statuscode to the client as result. However there are few bad statuscodes which allow the monitored item to be created as it may get readable later on.
If the initial value is OK, a timer is created with the monitored item's sampling interval and a timer callback function that samples the value. If your device cannot meet the requested sampling interval it may use a different sampling interval and write it in the monitored item, this interval is then passed to the client as revised sampling interval.
The timer id is stored in the user data of the monitored item. The user data can be freely used by the provider to store own information about the monitored item. Finally the initial value is passed to the monitored item and the operation result is set to a good statuscode:
After the SDK has called the add item function for all monitored items it will call the subscribe function. The provider may start a mass operation to register all monitored items at the device. When the operation has finished uaserver_subscribe_complete must be called for the SDK to continue processing the monitored item. The uaserver_subscribe_complete does not need to be called in the subscribe callback, it may also be called later after an asynchronous operation has finished.
The monitored item has a field for the operation result. In the add item function it was already set to good, but it is evaluated only after the uaserver_subscribe_complete call. So it may also be set later to propagate an error during the (asynchronous) operation on the device to the SDK.
The value of the monitored item is sampled by the timer callback function. The ua_monitoreditem struct is passed as the data parameter, which needs to be cast to the monitored item. It is then used to get the node handle and session to read the value, which is given to the monitored item. The ua_monitoreditem_new_value function takes care of detecting a data change, applying filters and adding the value to the queue:
When a client requests to modify monitored items the SDK calls the modify item handler function for each item to be modified. The SDK handles the change of the queue size and sets the new filter.
The provider needs to check the new filter to handle a possible deadband, these steps are identical to the add item operation. The new filters (deadband, datachange trigger and timestamps to return) are set at the monitored item before the modify function is called, so if your device supports any of these filters they can be updated in the modify operation. If the modify function returns a bad statuscode, these filters are reset by the SDK.
Furthermore the provider needs to applythe new sampling interval. It is passed along the monitored item as parameter to the modify item function, so the function checks whether it changed and creates a new timer with the new sampling interval and set the new interval at the monitored item. If your device does not support the requested sampling interval this function may abort and return a bad statuscode or change the sampling interval to a nearby interval. The actually used sampling interval must always be set at the monitored item and will be returned to the client as revised sampling interval. This implementation needs to remove the old timer and write the new timer id into the user data. The operation succeeded, so the operation result is set to good and the function can return:
As in the add operation the SDK will call the provider's subscribe function and the provider needs to call uaserver_subscribe_complete. This would allow an asynchronous implementation of the modify operation. If the modify operation fails, the item must be left in the same state as before, it especially must not stop sampling.
The set_item_mode callback is called when the client uses the SetMonitoringMode Service to change the monitoring mode of the item. When the mode is change to disabled the provider may stop sampling to save resources and continue when it is changed back to sampling or reporting. The SDK does also track the current monitoring mode and discard values for disabled items automatically, so it is not required to have a fully functional implementation, a dummy implementation like in this example is sufficient:
The remove item handler is called for each monitored item to be deleted, either by request from the client or if the associated subscription is deleted. The sample implementation only needs to delete the monitored item's timer and set the operation result to good:
Again the subscribe function is called to give the provider the opportunity to perform the remove as mass and/or asynchronous operation.
The previous sections give an example how the monitored items of a subscriptions are added, modified or removed. However there are some constraints and condidtions in the monitored item mechanism, that should be kept in mind when implementing an own provider:
Compile and run the server application. When connecting to the server with a UA Client (e.g. UaExpert) the newly created nodes are visible in the server’s address space. It is also possible to read and subscribe the values and write to the variables.