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.
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.
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.
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.
4 Responses
how can i do if i want to use an exist database like mssql to record the history data?
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) || …)
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?!
Hello,
please open a bug report on https://github.com/open62541/open62541 for the issue