Initiale Unterstützung für Server mit historischem Datenzugriff in open62541

Inhalt
Toolbox

Ein OPC UA Server, der den historischen Zugriff unterstützt, ermöglicht es Clients, auf historische Daten oder historische Ereignisse zuzugreifen. Ein solcher Server kann als Prozess- oder Ereignishistoriker fungieren. open62541-basierte Server fehlten bis vor kurzem komplett, um diese Anwendungsfälle zu unterstützen. basysKom hat die Server-API erweitert, um den Zugriff auf historische Daten mit Hilfe der in OPC UA Teil 11 spezifizierten "Read Raw"-Funktionalität zu unterstützen. Dieser Artikel gibt einen Überblick über diese API und ein kurzes Tutorial, wie man sie verwendet.

Übersicht über den open62541 historischen Datenzugriff

Der Großteil der neuen Funktionalität ist in einem Plugin namens UA_HistoryDatabase enthalten, das drei wesentliche API-Elemente enthält.

  • UA_HistoryDatabase (gleicher Name wie das Plugin), die die Hauptschnittstelle zwischen dem Server selbst und dem Plugin enthält.
  • UA_HistoryDataBackend, das die Integration mit einer spezifischen Datenbank implementiert. Eine Beispielimplementierung, die auf einer einfachen In-Memory-Datenbank basiert, wird zur Verfügung gestellt.
  • UA_HistoryDataGathering, das das Sammeln und Speichern von Daten kapselt. 

Das folgende Diagramm gibt einen Überblick über das Zusammenspiel dieser API-Elemente.

open62541 - Zugang zu historischen Daten

Tutorial

Das folgende Tutorial zeigt zunächst, wie man einen einfachen Server mit einem einzigen variablen Knoten erstellt und diesen Server bei jeder Änderung so erweitert, dass dieser Wert in einer In-Memory-Datenbank gespeichert wird.

Fangen wir mit einem Server an:

#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;
}

Der resultierende Server kann durchsucht und die Variable gelesen werden. Wir verwenden UAExpert für diese Aufgaben. 

Anfängliche Unterstützung für Server mit historischem Datenzugriff in open62541 1

Wir erweitern dieses Beispiel nun so, dass der Server in der Lage sein wird, Anfragen nach historischen Werten für diesen Knoten zu bearbeiten. Jedes der folgenden Snippets enthält genügend Kontext, so dass klar ist, wo der Code in den ursprünglichen Servercode eingefügt werden muss.

[...]
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");
[...]

Als Nächstes konfigurieren wir den Knoten so, dass die Datenbank jedes Mal aktualisiert wird, wenn der Knotenwert festgelegt wird.

[...]
                                                 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));
[...]

Auch hier verlassen wir uns auf den UAExpert, um den Server zu validieren. Zuerst aktualisieren wir die Variable ein paar Mal (so dass wir tatsächlich einige historische Werte haben). Als nächstes wollen wir sie lesen. Dazu verwenden wir die "History Trend View", die über das Dokument → Add → Document Type verfügbar ist. Die resultierende Ansicht zeigt entweder ein Diagramm oder (in einem zweiten Tab) eine Liste Ihrer historischen Werte. Wenn keine Werte dargestellt werden oder die Liste leer ist, stellen Sie sicher, dass das richtige Zeitintervall in der Anfrage verwendet wird.

Anfängliche Unterstützung für Server mit historischem Datenzugriff in open62541 2

Schlussfolgerung & zukünftige Arbeit

Es ist nun möglich, open62541-basierte Server zu erstellen, die das Sammeln und Lesen von historischen Datenwerten unterstützen. Es wurde eine Reihe von Plugin-Schnittstellen definiert, mit denen Funktionen wie die Unterstützung von zusätzlichen Datenbanken (z.B. sqlite) implementiert werden können. Es muss noch zusätzliche Arbeit geleistet werden, um weitere Details zum Lesen der Historie zu unterstützen, wie z.B. "read event", "read modified", "read processed" oder "read at a time". Auch der Dienst zur Aktualisierung der Historie muss noch implementiert werden.

Neben dem Beitrag zum Projekt bietet basysKom auch kommerziellen Support für den open62541 OPC UA Stack. Sprechen Sie mit uns wenn Sie an Projekt-/Anwendungsunterstützung, der Implementierung fehlender Funktionen oder der Behebung von Fehlern interessiert sind.

Eine Antwort

Schreibe einen Kommentar

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

Peter Rustler

Peter Rustler

Peter Rustler ist Backend Software Engineer für industrielle und embedded Anwendungen. Er arbeitet seit 2011 für die basysKom GmbH, wo er Kunden berät. Er ist an verschiedenen Projekten wie open62541 und Qt-Nfc beteiligt. Er hat einen starken Hintergrund in Embedded Linux, Systemprogrammierung, verteilten Systemen und Anwendungsentwicklung. Er hat einen Bachelor of Computer Science der Hochschule Darmstadt und eine Ausbildung in Kommunikationselektronik.
Aktie auf facebook
Aktie auf twitter
Aktie auf linkedin
Aktie auf reddit
Aktie auf xing
Aktie auf email
Aktie auf stumbleupon
Aktie auf whatsapp
Aktie auf pocket