basysKom AnwendungsEntwicklung

Initial support for servers with historical data access in open62541
Essential Summary
An OPC UA server supporting historical access allows clients to access historical data or historical events. Such a server can act as a process or event historian. open62541-based servers were till recently completely missing the ability to support these use cases. basysKom has extended the server API to support access to historical data using the „read raw“ functionality specified in OPC UA part 11. This allows to create a simple process historian. This article provides an overview about this API and a short tutorial how to use it.

Overview about the open62541 historical data access

Most of the new functionality is contained within a plugin called UA_HistoryDatabase. This plugin contains three main API elements.

  • UA_HistoryDatabase (same name as the plugin) which contains the main interface between the server itself and the plugin.
  • UA_HistoryDataBackend which implements the integration with a specific database. A sample implementation based on a simple in-memory database is provided.
  • UA_HistoryDataGathering which encapsulates the gathering and storage of data. 

The following diagram gives an overview how these API elements interact.

open62541 - historical data access

Tutorial

The following tutorial will first demonstrate how to create a simple server, hosting a single variable node. We will extend this server to store this value in an in-memory database each time it changes.

Let’s start with a server:

#include "open62541.h"
#include 
 
static UA_Boolean running = true;
static void stopHandler(int sign) {
    (void)sign;
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
    running = false;
}
 
 
int main(void) {
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);
 
    UA_ServerConfig *config = UA_ServerConfig_new_default();
    UA_Server *server = UA_Server_new(config);
    /* Define the attribute of the uint32 variable node */
    UA_VariableAttributes attr = UA_VariableAttributes_default;
    UA_UInt32 myUint32 = 40;
    UA_Variant_setScalar(&attr.value, &myUint32, &UA_TYPES[UA_TYPES_UINT32]);
    attr.description = UA_LOCALIZEDTEXT("en-US","myUintValue");
    attr.displayName = UA_LOCALIZEDTEXT("en-US","myUintValue");
    attr.dataType = UA_TYPES[UA_TYPES_UINT32].typeId;
    attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
 
    /* Add the variable node to the information model */
    UA_NodeId uint32NodeId = UA_NODEID_STRING(1, "myUintValue");
    UA_QualifiedName uint32Name = UA_QUALIFIEDNAME(1, "myUintValue");
    UA_NodeId parentNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    UA_NodeId parentReferenceNodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES);
    UA_NodeId outNodeId;
    UA_NodeId_init(&outNodeId);
    UA_StatusCode retval = UA_Server_addVariableNode(server,
                                                     uint32NodeId,
                                                     parentNodeId,
                                                     parentReferenceNodeId,
                                                     uint32Name,
                                                     UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
                                                     attr,
                                                     NULL,
                                                     &outNodeId);
    fprintf(stderr, "UA_Server_addVariableNode %s\n", UA_StatusCode_name(retval));
    retval = UA_Server_run(server, &running);
    fprintf(stderr, "UA_Server_run %s\n", UA_StatusCode_name(retval));
    UA_Server_run_shutdown(server);
    UA_Server_delete(server);
    UA_ServerConfig_delete(config);
    return (int)retval;
}

The resulting server can be browsed and the variable can be read. We use UAExpert for this tasks. 

Initial support for servers with historical data access in open62541 1 basysKom, HMI Dienstleistung, Qt, Cloud, Azure

We now extend this example so that the server will be able to handle request for historical values for this node. Each of the following snippets contains enough context so it is clear where to add the code into the initial server code.

[...]
UA_ServerConfig *config = UA_ServerConfig_new_default();
 
 
// Instanciate a UA_HistoryDataGathering with an initial node id store size of 1.
 
// The store will grow if additional nodes are registered, but this is expensive.
UA_HistoryDataGathering gathering = UA_HistoryDataGathering_Default(1);
// configure the server with an instance of the UA_HistoryDatabase plugin
config->historyDatabase = UA_HistoryDatabase_default(gathering);
UA_Server *server = UA_Server_new(config);
UA_VariableAttributes attr = UA_VariableAttributes_default;
UA_UInt32 myUint32 = 40;
UA_Variant_setScalar(&attr.value, &myUint32, &UA_TYPES[UA_TYPES_UINT32]);
attr.description = UA_LOCALIZEDTEXT("en-US","myUintValue");
attr.displayName = UA_LOCALIZEDTEXT("en-US","myUintValue");
attr.dataType = UA_TYPES[UA_TYPES_UINT32].typeId;
 
// announce support for history read to clients
attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD;
// tell the server to enable historizing for this node
attr.historizing = true;
 
UA_NodeId uint32NodeId = UA_NODEID_STRING(1, "myUintValue");
UA_QualifiedName uint32Name = UA_QUALIFIEDNAME(1, "myUintValue");
[...]

Next we configure the node to update the database each time the nodes value is set.

[...]
                                                 NULL,
                                                 &outNodeId);
fprintf(stderr, "UA_Server_addVariableNode %s\n", UA_StatusCode_name(retval));
 
UA_HistorizingNodeIdSettings setting;
/* Use the default sample in-memory database. Reserve space for 3 nodes with an initial room of 100 values each.
 * This will also automaticaly grow if needed. */
setting.historizingBackend = UA_HistoryDataBackend_Memory(3, 100);
 
/* We want the server to serve a maximum of 100 values per request.
 * This value depends on the platform you are running the server on - a big
 * server can serve more values in a single request. */
setting.maxHistoryDataResponseSize = 100;
// update the database each time the node value is set
setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET;
// register the node for gathering data in the database
retval = gathering.registerNodeId(server, gathering.context, &outNodeId, setting);
fprintf(stderr, "registerNodeId %s\n", UA_StatusCode_name(retval));
retval = UA_Server_run(server, &running);
fprintf(stderr, "UA_Server_run %s\n", UA_StatusCode_name(retval));
[...]

Again we rely on the UAExpert to validate the server. First update the variable a few times (so we actually have some historical values). Next we want to read them.  For that we use the „History Trend View“ which is available via Document → Add → Document Type. The resulting view shows either a graph or (in a second tab) a list of your historical values. If no values are plotted or the list is empty make sure that the right time interval is used in the request.

Initial support for servers with historical data access in open62541 2 basysKom, HMI Dienstleistung, Qt, Cloud, Azure

Conclusion & future work

It is now possible to create open62541-based servers which support the gathering and reading of historical data values. A number of plugin interfaces has been defined which can be used to implement functionality such as support for additional databases (e.g. sqlite). There is additional work to be done to support other history read details such as „read event“, „read modified“, „read processed“ or „read at a time“. Also the history update service still needs to be implemented.

In addition to being a contributor to the project, basysKom also provides commercial support for the open62541 OPC UA stack. Talk to us if you are interested in project/application support, the implementation of missing features or bugfixing.

Peter Rustler

Peter Rustler

Peter Rustler is Backend Software Engineer on industrial and embedded applications. He works for basysKom GmbH since 2011, where he is consulting customers. Contributer to various projects such as open62541 and Qt-Nfc. He has a strong background in Embedded Linux, systems programming, distributed systems and application development. He holds a Bachelor of Computer Science from the University of Applied Sciences in Darmstadt and has an education in communication electronics.

4 Antworten

  1. In the ua_history_data_backend_memory.c:
    in the timestampsToReturnSupported_backend_memory() function:
    you’re always checking only the timestamp of the first value of the item?
    I think the verification should be on the values to be read that are not known at this level. So, this test is not mandatory and can be removed?
    if( …. || (timestampsToReturn == UA_TIMESTAMPSTORETURN_SERVER
    && !item->dataStore[0]->value.hasServerTimestamp) || …)

  2. Hello,
    Some Historical Access API missing features regarding the OPC UA specification part XI:
    S 6.4.3.2: „For an interval in which no data exists, if Bounding Values are not requested, then the corresponding StatusCode shall be Good_NoData“: Is not supported!!
    Data destruction is not properly carried out?!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy