Revision control

++ // Copyright (C) 2017 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include "testserver.h"
++
++ #ifdef USE_SYSTEM_OPEN62541
++ #include <open62541/plugin/log_stdout.h>
++ #include <open62541/plugin/certificategroup_default.h>
++ #endif
++
++ #include "generated/namespace_qtopcuatestmodel_generated.h"
++ #include "generated/types_qtopcuatestmodel_generated.h"
++ #include "generated/nodeids_qtopcuatestmodel.h"
++
++ #include "qopen62541utils.h"
++ #include "qopen62541valueconverter.h"
++ #include <QtOpcUa/qopcuatype.h>
++
++ #include <QtCore/QDebug>
++ #include <QtCore/QLoggingCategory>
++ #include <QDir>
++ #include <QFile>
++ #include <QMap>
++
++ #include <cstring>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::Literals::StringLiterals;
++
++ static const size_t usernamePasswordsSize = 2;
++ static UA_UsernamePasswordLogin usernamePasswords[2] = {
++ {UA_STRING_STATIC("user1"), UA_STRING_STATIC("password")},
++ {UA_STRING_STATIC("user2"), UA_STRING_STATIC("password1")}};
++
++ const UA_UInt16 portNumber = 43344;
++
++ // Node ID conversion is included from the open62541 plugin but warnings from there should be logged
++ // using qt.opcua.testserver instead of qt.opcua.plugins.open62541 for usage in the test server
++ Q_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_OPEN62541, "qt.opcua.testserver")
++
++
++ TestServer::TestServer(QObject *parent) : QObject(parent)
++ {
++ m_historyDataBackend = UA_HistoryDataBackend_Memory(1, 10);
++ }
++
++ TestServer::~TestServer()
++ {
++ m_historyDataBackend.deleteMembers(&m_historyDataBackend);
++ UA_Server_delete(m_server);
++ }
++
++ bool TestServer::createInsecureServerConfig(UA_ServerConfig *config)
++ {
++ UA_StatusCode result = UA_ServerConfig_setMinimal(config, portNumber, nullptr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to create server config without encryption";
++ return false;
++ }
++
++ UA_String_clear(&config->applicationDescription.applicationUri);
++ config->applicationDescription.applicationUri = UA_STRING_ALLOC("urn:open62541.server.application");
++
++ result = UA_AccessControl_default(config, true, nullptr, usernamePasswordsSize, usernamePasswords);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to create access control";
++ return false;
++ }
++
++ // This is needed for COIN because the hostname returned by gethostname() is not resolvable.
++ UA_Array_delete(config->serverUrls, config->serverUrlsSize, &UA_TYPES[UA_TYPES_STRING]);
++ config->serverUrls = UA_String_new();
++ config->serverUrls[0] = UA_STRING_ALLOC("opc.tcp://localhost:43344");
++ config->serverUrlsSize = 1;
++
++ config->tcpReuseAddr = true;
++
++ return true;
++ }
++
++ #if defined UA_ENABLE_ENCRYPTION
++ static UA_ByteString loadFile(const QString &filePath) {
++ UA_ByteString fileContents = UA_STRING_NULL;
++ fileContents.length = 0;
++
++ QFile file(filePath);
++ if (!file.open(QFile::ReadOnly))
++ return fileContents;
++
++ const auto allocResult = UA_ByteString_allocBuffer(&fileContents, file.size());
++
++ if (allocResult != UA_STATUSCODE_GOOD)
++ return fileContents;
++
++ if (file.read(reinterpret_cast<char*>(fileContents.data), fileContents.length) != static_cast<qint64>(fileContents.length)) {
++ UA_ByteString_clear(&fileContents);
++ fileContents.length = 0;
++ return fileContents;
++ }
++ return fileContents;
++ }
++
++ bool TestServer::createSecureServerConfig(UA_ServerConfig *config)
++ {
++ const QString certificateFilePath = u":/pki/own/certs/open62541-testserver.der"_s;
++ const QString privateKeyFilePath = u":/pki/own/private/open62541-testserver.der"_s;
++
++ UA_ByteString certificate = loadFile(certificateFilePath);
++ UaDeleter<UA_ByteString> certificateDeleter(&certificate, UA_ByteString_clear);
++ UA_ByteString privateKey = loadFile(privateKeyFilePath);
++ UaDeleter<UA_ByteString> privateKeyDeleter(&privateKey, UA_ByteString_clear);
++
++ if (certificate.length == 0) {
++ UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
++ "Failed to load certificate %s", certificateFilePath.toLocal8Bit().constData());
++ return false;
++ }
++ if (privateKey.length == 0) {
++ UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
++ "Failed to load private key %s", privateKeyFilePath.toLocal8Bit().constData());
++ return false;
++ }
++
++ // Load the trustlist
++ QDir trustDir(u":/pki/trusted/certs"_s);
++ if (!trustDir.exists()) {
++ UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Trust directory does not exist");
++ return false;
++ }
++
++ const auto trustedCerts = trustDir.entryList(QDir::Files);
++ const size_t trustListSize = trustedCerts.size();
++ int i = 0;
++
++ // UA_STACKARRAY(UA_ByteString, trustList, trustListSize);
++ auto trustList = static_cast<UA_ByteString *>(UA_Array_new(trustListSize, &UA_TYPES[UA_TYPES_BYTESTRING]));
++ UaArrayDeleter<UA_TYPES_BYTESTRING> trustListDeleter(trustList, trustListSize);
++
++ for (const auto &entry : trustedCerts) {
++ trustList[i] = loadFile(trustDir.filePath(entry));
++ if (trustList[i].length == 0) {
++ UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Failed to load trusted certificate");
++ return false;
++ } else {
++ UA_LOG_DEBUG(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Trusted certificate %s loaded", entry.toLocal8Bit().constData());
++ }
++ ++i;
++ }
++
++ // Loading of a revocation list currently unsupported
++ UA_ByteString *revocationList = nullptr;
++ size_t revocationListSize = 0;
++
++ if (trustListSize == 0)
++ UA_LOG_WARNING(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
++ "No CA trust-list provided. Any remote certificate will be accepted.");
++
++ UA_ServerConfig_setBasics_withPort(config, 43344);
++
++ UA_String_clear(&config->applicationDescription.applicationUri);
++ config->applicationDescription.applicationUri = UA_STRING_ALLOC("urn:open62541.server.application");
++
++ // This is needed for COIN because the hostname returned by gethostname() is not resolvable.
++ UA_Array_delete(config->serverUrls, config->serverUrlsSize, &UA_TYPES[UA_TYPES_STRING]);
++ config->serverUrls = UA_String_new();
++ config->serverUrls[0] = UA_STRING_ALLOC("opc.tcp://localhost:43344");
++ config->serverUrlsSize = 1;
++
++ config->tcpReuseAddr = true;
++
++ {
++ UA_TrustListDataType list {};
++ list.specifiedLists = UA_TRUSTLISTMASKS_TRUSTEDCRLS | UA_TRUSTLISTMASKS_TRUSTEDCERTIFICATES;
++ list.trustedCertificates = trustList;
++ list.trustedCertificatesSize = trustListSize;
++ list.trustedCrlsSize = revocationListSize;
++ list.trustedCrls = revocationList;
++
++ auto defaultApplicationGroup = UA_NS0ID(SERVERCONFIGURATION_CERTIFICATEGROUPS_DEFAULTAPPLICATIONGROUP);
++ UA_StatusCode result = UA_CertificateGroup_Memorystore(&config->sessionPKI, &defaultApplicationGroup, &list, config->logging, nullptr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to initialize certificate verification";
++ return false;
++ }
++ }
++
++ {
++ UA_TrustListDataType list {};
++ list.specifiedLists = UA_TRUSTLISTMASKS_TRUSTEDCRLS | UA_TRUSTLISTMASKS_TRUSTEDCERTIFICATES;
++ list.trustedCertificates = trustList;
++ list.trustedCertificatesSize = trustListSize;
++ list.trustedCrlsSize = revocationListSize;
++ list.trustedCrls = revocationList;
++
++ auto defaultApplicationGroup = UA_NS0ID(SERVERCONFIGURATION_CERTIFICATEGROUPS_DEFAULTAPPLICATIONGROUP);
++ UA_StatusCode result = UA_CertificateGroup_Memorystore(&config->secureChannelPKI, &defaultApplicationGroup, &list, config->logging, nullptr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to initialize certificate verification";
++ return false;
++ }
++ }
++
++ // Add the security policies manually because we need to skip Basic128Rsa15 and Basic256
++ // if OpenSSL doesn't support SHA-1 signatures (e.g. RHEL 9).
++
++ // The None security policy is already present, but without a certificate
++ for (size_t i = 0; i < config->securityPoliciesSize; ++i) {
++ if (config->securityPolicies[i].clear)
++ config->securityPolicies[i].clear(&config->securityPolicies[i]);
++ }
++
++ UA_free(config->securityPolicies);
++ config->securityPoliciesSize = 0;
++ config->securityPolicies = nullptr;
++
++ UA_StatusCode retval = UA_ServerConfig_addSecurityPolicyNone(config, &certificate);
++ if(retval != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add security policy None";
++ return false;
++ }
++
++ if (Open62541Utils::checkSha1SignatureSupport()) {
++ retval = UA_ServerConfig_addSecurityPolicyBasic128Rsa15(config, &certificate, &privateKey);
++ if(retval != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add security policy Basic128Rsa15";
++ return false;
++ }
++
++ retval = UA_ServerConfig_addSecurityPolicyBasic256(config, &certificate, &privateKey);
++ if(retval != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add security policy Basic256";
++ return false;
++ }
++ }
++
++ retval = UA_ServerConfig_addSecurityPolicyAes256Sha256RsaPss(config, &certificate, &privateKey);
++ if (retval != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add SecurityPolicy#Aes256Sha256RsaPss";
++ return false;
++ }
++
++ retval = UA_ServerConfig_addSecurityPolicyBasic256Sha256(config, &certificate, &privateKey);
++ if(retval != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add security policy Basic256Sha256";
++ return false;
++ }
++
++ retval = UA_ServerConfig_addSecurityPolicyAes128Sha256RsaOaep(config, &certificate, &privateKey);
++ if(retval != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add security policy Aes128Sha256RsaOaep";
++ return false;
++ }
++
++ UA_StatusCode result = UA_AccessControl_default(config, true, nullptr, usernamePasswordsSize, usernamePasswords);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to create access control";
++ return false;
++ }
++
++ result = UA_ServerConfig_addAllEndpoints(config);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add endpoints";
++ return false;
++ }
++
++ return true;
++ }
++ #endif
++
++ bool TestServer::init(bool noNonePolicyPassword)
++ {
++ bool success;
++
++ m_server = UA_Server_new();
++
++ if (!m_server)
++ return false;
++
++ // This member is managed by the server
++ m_config = UA_Server_getConfig(m_server);
++
++ #if defined UA_ENABLE_ENCRYPTION
++ success = createSecureServerConfig(m_config);
++ m_config->allowNonePolicyPassword = !noNonePolicyPassword;
++ #else
++ success = createInsecureServerConfig(m_config);
++ m_config->allowNonePolicyPassword = true;
++ #endif
++
++ m_config->maxReferencesPerNode = 10;
++
++ m_gathering = UA_HistoryDataGathering_Default(1);
++ m_config->historyDatabase = UA_HistoryDatabase_default(m_gathering);
++
++ if (!success || !m_config)
++ return false;
++
++ return true;
++ }
++
++ int TestServer::registerNamespace(const QString &ns)
++ {
++ return UA_Server_addNamespace(m_server, ns.toUtf8().constData());
++ }
++
++ ManagedUaNodeId TestServer::addFolder(const QString &nodeString, const QString &displayName, const QString &description)
++ {
++ UA_NodeId resultNode;
++ UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
++
++ oAttr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", displayName.toUtf8().constData());
++ if (description.size())
++ oAttr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
++
++ UA_StatusCode result;
++ UA_NodeId requestedNodeId = Open62541Utils::nodeIdFromQString(nodeString);
++
++ UA_QualifiedName nodeBrowseName = UA_QUALIFIEDNAME_ALLOC(requestedNodeId.namespaceIndex, displayName.toUtf8().constData());
++
++ result = UA_Server_addObjectNode(m_server,
++ requestedNodeId,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ nodeBrowseName,
++ UA_NODEID_NULL,
++ oAttr,
++ nullptr,
++ &resultNode);
++
++ UA_QualifiedName_clear(&nodeBrowseName);
++ UA_NodeId_clear(&requestedNodeId);
++ UA_ObjectAttributes_clear(&oAttr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add folder:" << nodeString << " :" << result;
++ return UA_NODEID_NULL;
++ }
++ return resultNode;
++ }
++
++ ManagedUaNodeId TestServer::addObject(const UA_NodeId &parentFolder, int namespaceIndex, const QString &objectName)
++ {
++ UA_NodeId resultNode;
++ UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
++
++ if (objectName.size())
++ oAttr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", objectName.toUtf8().constData());
++
++ UA_QualifiedName nodeBrowseName = UA_QUALIFIEDNAME_ALLOC(namespaceIndex, objectName.toUtf8().constData());
++
++ UA_StatusCode result;
++ result = UA_Server_addObjectNode(m_server, UA_NODEID_NULL,
++ parentFolder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ nodeBrowseName,
++ UA_NODEID_NULL,
++ oAttr,
++ nullptr,
++ &resultNode);
++
++ UA_QualifiedName_clear(&nodeBrowseName);
++ UA_ObjectAttributes_clear(&oAttr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add object to folder:" << result;
++ return UA_NODEID_NULL;
++ }
++ return resultNode;
++ }
++
++ ManagedUaNodeId TestServer::addVariableWithWriteMask(const UA_NodeId &folder, const QString &variableNode, const QString &name, const QVariant &value,
++ QOpcUa::Types type, quint32 writeMask)
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(variableNode);
++
++ const QString description = u"Description for %1"_s.arg(variableNode);
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = QOpen62541ValueConverter::toOpen62541Variant(value, type);
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", name.toUtf8().constData());
++ attr.dataType = attr.value.type ? attr.value.type->typeId : UA_TYPES[UA_TYPES_BOOLEAN].typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
++ attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
++ attr.writeMask = writeMask;
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = attr.displayName.text;
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ &resultId);
++
++
++ UA_NodeId_clear(&variableNodeId);
++ UA_VariableAttributes_clear(&attr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add variable:" << result;
++ return UA_NODEID_NULL;
++ }
++
++ return resultId;
++ }
++
++ ManagedUaNodeId TestServer::addVariable(const UA_NodeId &folder, const QString &variableNode, const QString &name, const QVariant &value,
++ QOpcUa::Types type, QList<quint32> arrayDimensions, int valueRank, bool enableHistorizing, quint32 historyNumValuesPerNode)
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(variableNode);
++
++ const QString description = u"Description for %1"_s.arg(variableNode);
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = QOpen62541ValueConverter::toOpen62541Variant(value, type);
++ attr.valueRank = valueRank;
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", name.toUtf8().constData());
++ attr.dataType = attr.value.type ? attr.value.type->typeId : UA_TYPES[UA_TYPES_BOOLEAN].typeId;
++ if (enableHistorizing) {
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_HISTORYREAD | UA_ACCESSLEVELMASK_STATUSWRITE | UA_ACCESSLEVELMASK_TIMESTAMPWRITE;
++ } else {
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE | UA_ACCESSLEVELMASK_STATUSWRITE | UA_ACCESSLEVELMASK_TIMESTAMPWRITE;
++ }
++ attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
++ attr.historizing = enableHistorizing;
++
++ if (arrayDimensions.size()) {
++ attr.arrayDimensionsSize = arrayDimensions.size();
++ attr.arrayDimensions = arrayDimensions.data();
++ }
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = attr.displayName.text;
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ &resultId);
++
++ // Prevent deletion of the QList's value by UA_VariableAttribute_clear
++ attr.arrayDimensions = nullptr;
++ attr.arrayDimensionsSize = 0;
++
++ if (enableHistorizing) {
++ UA_HistorizingNodeIdSettings setting;
++ setting.historizingBackend = m_historyDataBackend;
++ setting.maxHistoryDataResponseSize = historyNumValuesPerNode;
++ setting.historizingUpdateStrategy = UA_HISTORIZINGUPDATESTRATEGY_VALUESET;
++ result = m_gathering.registerNodeId(m_server, m_gathering.context, &resultId, setting);
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not register node for historical data:" << result << "for node" << variableNode;
++ return UA_NODEID_NULL;
++ }
++ }
++
++ UA_NodeId_clear(&variableNodeId);
++ UA_VariableAttributes_clear(&attr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add variable:" << result << "for node" << variableNode;
++ return UA_NODEID_NULL;
++ }
++
++ return resultId;
++ }
++
++ ManagedUaNodeId TestServer::addEmptyArrayVariable(const UA_NodeId &folder, const QString &variableNode, const QString &name)
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(variableNode);
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", name.toUtf8().constData());
++ attr.dataType = UA_TYPES[UA_TYPES_BOOLEAN].typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ;
++ attr.valueRank = 1;
++ attr.arrayDimensionsSize = 1;
++ attr.arrayDimensions = UA_UInt32_new();
++ *attr.arrayDimensions = 1;
++ attr.value.arrayLength = 0;
++ attr.value.type = &UA_TYPES[UA_TYPES_BOOLEAN];
++ attr.value.data = UA_EMPTY_ARRAY_SENTINEL;
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = attr.displayName.text;
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ &resultId);
++
++ UA_NodeId_clear(&variableNodeId);
++ UA_VariableAttributes_clear(&attr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add empty array variable:" << result;
++ return UA_NODEID_NULL;
++ }
++
++ return resultId;
++ }
++
++ UA_StatusCode TestServer::multiplyMethod(UA_Server *server, const UA_NodeId *sessionId, void *sessionHandle, const UA_NodeId *methodId, void *methodContext, const UA_NodeId *objectId, void *objectContext, size_t inputSize, const UA_Variant *input, size_t outputSize, UA_Variant *output)
++ {
++ Q_UNUSED(server)
++ Q_UNUSED(sessionId)
++ Q_UNUSED(sessionHandle)
++ Q_UNUSED(methodId)
++ Q_UNUSED(methodContext)
++ Q_UNUSED(objectId)
++ Q_UNUSED(objectContext)
++
++ if (inputSize < 2)
++ return QOpcUa::UaStatusCode::BadArgumentsMissing;
++ if (inputSize > 2)
++ return QOpcUa::UaStatusCode::BadTooManyArguments;
++ if (outputSize != 1)
++ return QOpcUa::UaStatusCode::BadInvalidArgument;
++
++ double arg1 = *static_cast<double *>(input[0].data);
++ double arg2 = *static_cast<double *>(input[1].data);
++
++ double temp = arg1 * arg2;
++ UA_Variant_setScalarCopy(output, &temp, &UA_TYPES[UA_TYPES_DOUBLE]);
++
++ return UA_STATUSCODE_GOOD;
++ }
++
++ ManagedUaNodeId TestServer::addMultiplyMethod(const UA_NodeId &folder, const QString &variableNode, const QString &description)
++ {
++ UA_NodeId methodNodeId = Open62541Utils::nodeIdFromQString(variableNode);
++
++ UA_Argument inputArguments[2];
++ UA_Argument_init(&inputArguments[0]);
++ UA_Argument_init(&inputArguments[1]);
++
++ inputArguments[0].description = UA_LOCALIZEDTEXT_ALLOC("en", "First value");
++ inputArguments[0].name = UA_STRING_ALLOC("The first double");
++ inputArguments[0].dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
++ inputArguments[0].valueRank = -1;
++
++ inputArguments[1].description = UA_LOCALIZEDTEXT_ALLOC("en", "Second value");
++ inputArguments[1].name = UA_STRING_ALLOC("The second double");
++ inputArguments[1].dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
++ inputArguments[1].valueRank = -1;
++
++ UA_Argument outputArgument;
++ UA_Argument_init(&outputArgument);
++
++ outputArgument.description = UA_LOCALIZEDTEXT_ALLOC("en", "The product of the two arguments");
++ outputArgument.name = UA_STRING_ALLOC("The product of the two arguments");
++ outputArgument.dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
++ outputArgument.valueRank = -1;
++
++ UA_MethodAttributes attr = UA_MethodAttributes_default;
++
++ attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", variableNode.toUtf8().constData());
++ attr.executable = true;
++
++ UA_QualifiedName nodeBrowseName = UA_QUALIFIEDNAME_ALLOC(methodNodeId.namespaceIndex, "multiplyArguments");
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addMethodNode(m_server, methodNodeId, folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
++ nodeBrowseName,
++ attr, &multiplyMethod,
++ 2, inputArguments,
++ 1, &outputArgument,
++ nullptr, &resultId);
++
++ UA_QualifiedName_clear(&nodeBrowseName);
++ UA_NodeId_clear(&methodNodeId);
++ UA_MethodAttributes_clear(&attr);
++ UA_Argument_clear(&inputArguments[0]);
++ UA_Argument_clear(&inputArguments[1]);
++ UA_Argument_clear(&outputArgument);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add variable:" << result;
++ return UA_NODEID_NULL;
++ }
++ return resultId;
++ }
++
++ UA_StatusCode TestServer::multipleOutputArgumentsMethod(UA_Server *server, const UA_NodeId *sessionId, void *sessionHandle, const UA_NodeId *methodId, void *methodContext, const UA_NodeId *objectId, void *objectContext, size_t inputSize, const UA_Variant *input, size_t outputSize, UA_Variant *output)
++ {
++ Q_UNUSED(server)
++ Q_UNUSED(sessionId)
++ Q_UNUSED(sessionHandle)
++ Q_UNUSED(methodId)
++ Q_UNUSED(methodContext)
++ Q_UNUSED(objectId)
++ Q_UNUSED(objectContext)
++
++ if (inputSize < 2)
++ return QOpcUa::UaStatusCode::BadArgumentsMissing;
++ if (inputSize > 2)
++ return QOpcUa::UaStatusCode::BadTooManyArguments;
++ if (outputSize != 2)
++ return QOpcUa::UaStatusCode::BadInvalidArgument;
++
++ double arg1 = *static_cast<double *>(input[0].data);
++ double arg2 = *static_cast<double *>(input[1].data);
++
++ double product = arg1 * arg2;
++ auto someText = UA_LOCALIZEDTEXT_ALLOC("en-US", "some text argument");
++ UA_Variant_setScalarCopy(&output[0], &product, &UA_TYPES[UA_TYPES_DOUBLE]);
++ UA_Variant_setScalarCopy(&output[1], &someText, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
++
++ return UA_STATUSCODE_GOOD;
++ }
++
++ ManagedUaNodeId TestServer::addMultipleOutputArgumentsMethod(const UA_NodeId &folder, const QString &variableNode, const QString &description)
++ {
++ UA_NodeId methodNodeId = Open62541Utils::nodeIdFromQString(variableNode);
++
++ UA_Argument inputArguments[2];
++ UA_Argument_init(&inputArguments[0]);
++ UA_Argument_init(&inputArguments[1]);
++
++ inputArguments[0].description = UA_LOCALIZEDTEXT_ALLOC("en", "First value");
++ inputArguments[0].name = UA_STRING_ALLOC("The first double");
++ inputArguments[0].dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
++ inputArguments[0].valueRank = -1;
++
++ inputArguments[1].description = UA_LOCALIZEDTEXT_ALLOC("en", "Second value");
++ inputArguments[1].name = UA_STRING_ALLOC("The second double");
++ inputArguments[1].dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
++ inputArguments[1].valueRank = -1;
++
++ UA_Argument outputArgument[2];
++ UA_Argument_init(&outputArgument[0]);
++ UA_Argument_init(&outputArgument[1]);
++
++ outputArgument[0].description = UA_LOCALIZEDTEXT_ALLOC("en", "The product of the two arguments");
++ outputArgument[0].name = UA_STRING_ALLOC("The product of the two arguments");
++ outputArgument[0].dataType = UA_TYPES[UA_TYPES_DOUBLE].typeId;
++ outputArgument[0].valueRank = -1;
++
++ outputArgument[1].description = UA_LOCALIZEDTEXT_ALLOC("en", "Some additional text argument");
++ outputArgument[1].name = UA_STRING_ALLOC("Text argument");
++ outputArgument[1].dataType = UA_TYPES[UA_TYPES_LOCALIZEDTEXT].typeId;
++ outputArgument[1].valueRank = -1;
++
++ UA_MethodAttributes attr = UA_MethodAttributes_default;
++
++ attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", variableNode.toUtf8().constData());
++ attr.executable = true;
++
++ UA_QualifiedName nodeBrowseName = UA_QUALIFIEDNAME_ALLOC(methodNodeId.namespaceIndex, "multipleOutputArguments");
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addMethodNode(m_server, methodNodeId, folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
++ nodeBrowseName,
++ attr, &multipleOutputArgumentsMethod,
++ 2, inputArguments,
++ 2, outputArgument,
++ nullptr, &resultId);
++
++ UA_QualifiedName_clear(&nodeBrowseName);
++ UA_NodeId_clear(&methodNodeId);
++ UA_MethodAttributes_clear(&attr);
++ UA_Argument_clear(&inputArguments[0]);
++ UA_Argument_clear(&inputArguments[1]);
++ UA_Argument_clear(&outputArgument[0]);
++ UA_Argument_clear(&outputArgument[1]);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add variable:" << result;
++ return UA_NODEID_NULL;
++ }
++ return resultId;
++ }
++
++ ManagedUaNodeId TestServer::addAddNamespaceMethod(const UA_NodeId &folder, const QString &variableNode, const QString &description)
++ {
++ UA_NodeId methodNodeId = Open62541Utils::nodeIdFromQString(variableNode);
++
++ UA_Argument inputArguments[1];
++ UA_Argument_init(&inputArguments[0]);
++
++ inputArguments[0].description = UA_LOCALIZEDTEXT_ALLOC("en", "Namespace name to be added");
++ inputArguments[0].name = UA_STRING_ALLOC("Namespace name");
++ inputArguments[0].dataType = UA_TYPES[UA_TYPES_STRING].typeId;
++ inputArguments[0].valueRank = -1;
++
++ UA_Argument outputArgument;
++ UA_Argument_init(&outputArgument);
++
++ outputArgument.description = UA_LOCALIZEDTEXT_ALLOC("en", "Index of the added namespace");
++ outputArgument.name = UA_STRING_ALLOC("Namespace index");
++ outputArgument.dataType = UA_TYPES[UA_TYPES_UINT16].typeId;
++ outputArgument.valueRank = -1;
++
++ UA_MethodAttributes attr = UA_MethodAttributes_default;
++
++ attr.description = UA_LOCALIZEDTEXT_ALLOC("en-US", description.toUtf8().constData());
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", variableNode.toUtf8().constData());
++ attr.executable = true;
++
++ UA_QualifiedName nodeBrowseName = UA_QUALIFIEDNAME_ALLOC(methodNodeId.namespaceIndex, "addNamespace");
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addMethodNode(m_server, methodNodeId, folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
++ nodeBrowseName,
++ attr, &addNamespaceMethod,
++ 1, inputArguments,
++ 1, &outputArgument,
++ nullptr, &resultId);
++
++ UA_QualifiedName_clear(&nodeBrowseName);
++ UA_NodeId_clear(&methodNodeId);
++ UA_MethodAttributes_clear(&attr);
++ UA_Argument_clear(&inputArguments[0]);
++ UA_Argument_clear(&outputArgument);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add method:" << result;
++ return UA_NODEID_NULL;
++ }
++ return resultId;
++ }
++
++ UA_StatusCode TestServer::addNamespaceMethod(UA_Server *server, const UA_NodeId *sessionId, void *sessionHandle, const UA_NodeId *methodId, void *methodContext, const UA_NodeId *objectId, void *objectContext, size_t inputSize, const UA_Variant *input, size_t outputSize, UA_Variant *output)
++ {
++ Q_UNUSED(sessionId)
++ Q_UNUSED(sessionHandle)
++ Q_UNUSED(methodId)
++ Q_UNUSED(methodContext)
++ Q_UNUSED(objectId)
++ Q_UNUSED(objectContext)
++
++ if (inputSize < 1)
++ return QOpcUa::UaStatusCode::BadArgumentsMissing;
++ if (inputSize > 1)
++ return QOpcUa::UaStatusCode::BadTooManyArguments;
++ if (outputSize != 1)
++ return QOpcUa::UaStatusCode::BadInvalidArgument;
++
++ if (input[0].type != &UA_TYPES[UA_TYPES_STRING])
++ return UA_STATUSCODE_BADTYPEMISMATCH;
++
++ QString uri = QOpen62541ValueConverter::scalarToQt<QString>(static_cast<UA_String *>(input[0].data));
++ UA_UInt16 namespaceIndex = UA_Server_addNamespace(server, uri.toUtf8().constData());
++ UA_Variant_setScalarCopy(output, &namespaceIndex, &UA_TYPES[UA_TYPES_UINT16]);
++ return UA_STATUSCODE_GOOD;
++ }
++
++ ManagedUaNodeId TestServer::addNodeWithFixedTimestamp(const UA_NodeId &folder, const QString &nodeId, const QString &displayName)
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(nodeId);
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = QOpen62541ValueConverter::toOpen62541Variant(QDateTime(QDate(2012, 12, 19), QTime(13, 37)), QOpcUa::DateTime);
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en_US", displayName.toUtf8().constData());
++ attr.dataType = attr.value.type->typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ; // Read Only
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = attr.displayName.text;
++
++ UA_NodeId resultId;
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ folder,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ NULL,
++ &resultId);
++
++ UA_VariableAttributes_clear(&attr);
++ UA_NodeId_clear(&variableNodeId);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add variable:" << result;
++ return UA_NODEID_NULL;
++ }
++
++ return resultId;
++ }
++
++ UA_StatusCode TestServer::addServerStatusTypeTestNodes(const UA_NodeId &parent)
++ {
++ const auto createTestValue = [](int index) -> UA_ServerStatusDataType {
++ UA_ServerStatusDataType s;
++ UA_ServerStatusDataType_init(&s);
++
++ const UA_DateTime testDateTime = UA_DATETIME_UNIX_EPOCH + 1691996809123 * UA_DATETIME_MSEC;
++
++ s.currentTime = testDateTime + index * UA_DATETIME_MSEC;
++ s.startTime = testDateTime + index * UA_DATETIME_MSEC;
++ s.secondsTillShutdown = 23;
++ s.shutdownReason = UA_LOCALIZEDTEXT_ALLOC("en", "ShutdownReason");
++ s.state = UA_SERVERSTATE_TEST;
++ s.buildInfo.buildDate = testDateTime + index * UA_DATETIME_MSEC;
++ s.buildInfo.buildNumber = UA_STRING_ALLOC("BuildNumber");
++ s.buildInfo.manufacturerName = UA_STRING_ALLOC("ManufacturerName");
++ s.buildInfo.productName = UA_STRING_ALLOC("ProductName");
++ s.buildInfo.productUri = UA_STRING_ALLOC("ProductUri");
++ s.buildInfo.softwareVersion = UA_STRING_ALLOC("SoftwareVersion");
++
++ return s;
++ };
++
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(u"ns=3;s=ServerStatusScalar"_s);
++
++ auto value = UA_ServerStatusDataType_new();
++ *value = createTestValue(0);
++ auto var = UA_Variant();
++ UA_Variant_init(&var);
++ UA_Variant_setScalar(&var, value, &UA_TYPES[UA_TYPES_SERVERSTATUSDATATYPE]);
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = var;
++ attr.valueRank = UA_VALUERANK_SCALAR;
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", "ServerStatusScalar");
++ attr.dataType = attr.value.type->typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = UA_STRING_STATIC("ServerStatusScalar");
++
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ nullptr);
++
++ UA_NodeId_clear(&variableNodeId);
++ UA_VariableAttributes_clear(&attr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add scalar server status variable";
++ return result;
++ }
++ }
++
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(u"ns=3;s=ServerStatusArray"_s);
++
++ auto value = static_cast<UA_ServerStatusDataType *>(UA_Array_new(2, &UA_TYPES[UA_TYPES_SERVERSTATUSDATATYPE]));
++ for (int i = 0; i < 2; ++i)
++ value[i] = createTestValue(i);
++ auto var = UA_Variant();
++ UA_Variant_init(&var);
++ UA_Variant_setArray(&var, value, 2, &UA_TYPES[UA_TYPES_SERVERSTATUSDATATYPE]);
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = var;
++ attr.valueRank = UA_VALUERANK_ONE_DIMENSION;
++ quint32 arrayDimensions[] = { 0 };
++ attr.arrayDimensionsSize = 1;
++ attr.arrayDimensions = arrayDimensions;
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", "ServerStatusArray");
++ attr.dataType = attr.value.type->typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = UA_STRING_STATIC("ServerStatusArray");
++
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ nullptr);
++
++ attr.arrayDimensionsSize = 0;
++ attr.arrayDimensions = nullptr;
++
++ UA_NodeId_clear(&variableNodeId);
++ UA_VariableAttributes_clear(&attr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add array server status variable:" << UA_StatusCode_name(result);
++ return result;
++ }
++ }
++
++ {
++ UA_NodeId variableNodeId = Open62541Utils::nodeIdFromQString(u"ns=3;s=ServerStatusMultiDimensionalArray"_s);
++
++ auto value = static_cast<UA_ServerStatusDataType *>(UA_Array_new(4, &UA_TYPES[UA_TYPES_SERVERSTATUSDATATYPE]));
++ for (int i = 0; i < 4; ++i)
++ value[i] = createTestValue(i);
++ auto var = UA_Variant();
++ UA_Variant_init(&var);
++ UA_Variant_setArray(&var, value, 4, &UA_TYPES[UA_TYPES_SERVERSTATUSDATATYPE]);
++
++ quint32 arrayDimensions[] = { 2, 2 };
++
++ var.arrayDimensionsSize = 2;
++ var.arrayDimensions = arrayDimensions;
++
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = var;
++ attr.valueRank = UA_VALUERANK_TWO_DIMENSIONS;
++ attr.arrayDimensionsSize = 2;
++ attr.arrayDimensions = arrayDimensions;
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", "ServerStatusMultiDimensionalArray");
++ attr.dataType = attr.value.type->typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ | UA_ACCESSLEVELMASK_WRITE;
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = variableNodeId.namespaceIndex;
++ variableName.name = UA_STRING_STATIC("ServerStatusMultiDimensionalArray");
++
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ variableNodeId,
++ parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ nullptr);
++
++ attr.arrayDimensionsSize = 0;
++ attr.arrayDimensions = nullptr;
++ attr.value.arrayDimensionsSize = 0;
++ attr.value.arrayDimensions = nullptr;
++
++ UA_NodeId_clear(&variableNodeId);
++ UA_VariableAttributes_clear(&attr);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add multi dimensional array server status variable:" << UA_StatusCode_name(result);
++ return result;
++ }
++ }
++
++ return UA_STATUSCODE_GOOD;
++ }
++
++ UA_StatusCode TestServer::addLocalizedTextNodeWithCallback(const UA_NodeId &parent)
++ {
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.displayName = UA_LocalizedText{UA_STRING_STATIC("en"), UA_STRING_STATIC("LocalizedTextWithCallback")};
++ attr.dataType = UA_TYPES[UA_TYPES_LOCALIZEDTEXT].typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ; // Read Only
++
++ const UA_QualifiedName variableName{2, UA_STRING_STATIC("LocalizedTextWithCallback")};
++
++ auto variableNodeId = UA_NODEID_STRING_ALLOC(2, "LocalizedTextWithCallback");
++
++ const auto result = UA_Server_addCallbackValueSourceVariableNode(m_server, variableNodeId, parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName, UA_NODEID_NULL, attr,
++ { readLocalizedTextCallback, nullptr },
++ this, nullptr);
++
++ UA_NodeId_clear(&variableNodeId);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not add variable:" << result;
++ return result;
++ }
++
++ return result;
++ }
++
++ // The event test methods are based on the open62541 tutorial_server_events.c example
++ UA_StatusCode TestServer::generateEventCallback(UA_Server *server,
++ const UA_NodeId *sessionId, void *sessionHandle,
++ const UA_NodeId *methodId, void *methodContext,
++ const UA_NodeId *objectId, void *objectContext,
++ size_t inputSize, const UA_Variant *input,
++ size_t outputSize, UA_Variant *output) {
++ Q_UNUSED(sessionId)
++ Q_UNUSED(sessionHandle)
++ Q_UNUSED(methodId)
++ Q_UNUSED(methodContext)
++ Q_UNUSED(objectId)
++ Q_UNUSED(objectContext)
++ Q_UNUSED(outputSize)
++ Q_UNUSED(output)
++
++ // Setup event
++ quint16 eventSeverity = 100;
++
++ if (inputSize && input[0].type == &UA_TYPES[UA_TYPES_UINT16] && input[0].data) {
++ eventSeverity = *reinterpret_cast<quint16 *>(input[0].data);
++ }
++
++ qDebug() << "Creating event with severity" << eventSeverity;
++
++ UA_KeyValueMap eventFields{};
++ UA_StatusCode ret = UA_Server_createEvent(server, UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER), UA_NODEID_NUMERIC(2, 12345),
++ eventSeverity, UA_LocalizedText{ UA_STRING_STATIC("en"),
++ UA_STRING_STATIC("An event has been generated")},
++ &eventFields, nullptr, nullptr);
++
++ if (ret != UA_STATUSCODE_GOOD) {
++ qWarning() << "Could not create event:" << UA_StatusCode_name(ret);
++ return ret;
++ }
++
++ return ret;
++ }
++
++ void TestServer::readHistoryEventCallback(UA_Server *server, void *hdbContext, const UA_NodeId *sessionId, void *sessionContext,
++ const UA_RequestHeader *requestHeader, const UA_ReadEventDetails *historyReadDetails,
++ UA_TimestampsToReturn timestampsToReturn, UA_Boolean releaseContinuationPoints,
++ size_t nodesToReadSize, const UA_HistoryReadValueId *nodesToRead, UA_HistoryReadResponse *response,
++ UA_HistoryEvent * const * const historyData)
++ {
++ Q_UNUSED(server)
++ Q_UNUSED(hdbContext)
++ Q_UNUSED(sessionId)
++ Q_UNUSED(sessionContext)
++ Q_UNUSED(requestHeader)
++ Q_UNUSED(timestampsToReturn)
++ Q_UNUSED(releaseContinuationPoints)
++
++ QMap<UA_DateTime, UA_LocalizedText> historyEvents {
++ { UA_DateTime_fromUnixTime(1694153836 - 120 * 60), UA_LocalizedText{UA_STRING_STATIC("en"), UA_STRING_STATIC("Message 1")}},
++ { UA_DateTime_fromUnixTime(1694153836 - 60 * 60), UA_LocalizedText{UA_STRING_STATIC("en"), UA_STRING_STATIC("Message 2")}},
++ { UA_DateTime_fromUnixTime(1694153836), UA_LocalizedText{UA_STRING_STATIC("en"), UA_STRING_STATIC("Message 3")}}
++ };
++
++ const auto historian1Id = u"ns=2;s=EventHistorian"_s;
++ const auto historian2Id = u"ns=2;s=EventHistorian2"_s;
++
++ for (size_t i = 0; i < nodesToReadSize; ++i) {
++ const auto idToRead = QOpen62541ValueConverter::scalarToQt<QString, UA_NodeId>(&nodesToRead[i].nodeId);
++
++ if (idToRead != historian1Id && idToRead != historian2Id) {
++ response->results[i].statusCode = UA_STATUSCODE_BADNODEIDINVALID;
++ continue;
++ }
++
++ if (releaseContinuationPoints)
++ continue;
++
++ QMap<UA_DateTime, UA_LocalizedText> eventsToReturn;
++ UA_DateTime continuationPoint = 0;
++
++ UA_DateTime providedContinuationPoint = 0;
++ if (nodesToRead[i].continuationPoint.length) {
++ if (nodesToRead[i].continuationPoint.length == sizeof(UA_DateTime)) {
++ providedContinuationPoint = *reinterpret_cast<UA_DateTime *>(nodesToRead[i].continuationPoint.data);
++ } else {
++ response->results[i].statusCode = UA_STATUSCODE_BADCONTINUATIONPOINTINVALID;
++ continue;
++ }
++ }
++
++ const auto isHistorian2 = QOpen62541ValueConverter::scalarToQt<QString, UA_NodeId>(&nodesToRead[i].nodeId) == historian2Id;
++
++ for (auto it = historyEvents.constBegin(); it != historyEvents.constEnd(); ++it) {
++ if (isHistorian2 && it.key() == UA_DateTime_fromUnixTime(1694153836))
++ break;
++
++ if (providedContinuationPoint && it.key() < providedContinuationPoint)
++ continue;
++
++ if (it.key() < historyReadDetails->startTime || it.key() > historyReadDetails->endTime)
++ continue;
++
++ if (eventsToReturn.size() && eventsToReturn.size() == historyReadDetails->numValuesPerNode) {
++ continuationPoint = it.key();
++ break;
++ }
++
++ eventsToReturn.insert(it.key(), it.value());
++ }
++
++ historyData[i]->eventsSize = eventsToReturn.size();
++ historyData[i]->events = static_cast<UA_HistoryEventFieldList *>(UA_Array_new(eventsToReturn.size(), &UA_TYPES[UA_TYPES_HISTORYEVENTFIELDLIST]));
++
++ size_t j = 0;
++ for (auto it = eventsToReturn.constBegin(); it != eventsToReturn.constEnd(); ++it, ++j) {
++ historyData[i]->events[j].eventFieldsSize = historyReadDetails->filter.selectClausesSize;
++ historyData[i]->events[j].eventFields = static_cast<UA_Variant *>(
++ UA_Array_new(historyData[i]->events[j].eventFieldsSize, &UA_TYPES[UA_TYPES_VARIANT]));
++
++ for (size_t k = 0; k < historyReadDetails->filter.selectClausesSize; ++k) {
++ if (historyReadDetails->filter.selectClauses[k].browsePathSize != 1)
++ continue;
++
++ const auto timeName = UA_QualifiedName{0, UA_STRING_STATIC("Time")};
++ const auto messageName = UA_QualifiedName{0, UA_STRING_STATIC("Message")};
++ auto path = historyReadDetails->filter.selectClauses[k].browsePath;
++
++ if (UA_QualifiedName_equal(path, &timeName)) {
++ UA_Variant_setScalarCopy(&historyData[i]->events[j].eventFields[k], &it.key(), &UA_TYPES[UA_TYPES_DATETIME]);
++ } else if (UA_QualifiedName_equal(path, &messageName)) {
++ UA_Variant_setScalarCopy(&historyData[i]->events[j].eventFields[k], &it.value(), &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
++ }
++ }
++ }
++
++ if (continuationPoint) {
++ response->results[i].continuationPoint.length = sizeof(UA_DateTime);
++ auto dt = UA_DateTime_new();
++ *dt = continuationPoint;
++ response->results[i].continuationPoint.data = reinterpret_cast<UA_Byte *>(dt);
++ }
++ }
++ }
++
++ UA_StatusCode TestServer::readLocalizedTextCallback(UA_Server *server, const UA_NodeId *sessionId, void *sessionContext,
++ const UA_NodeId *nodeId, void *nodeContext, UA_Boolean includeSourceTimeStamp,
++ const UA_NumericRange *range, UA_DataValue *value)
++ {
++ Q_UNUSED(sessionContext);
++ Q_UNUSED(nodeId);
++ Q_UNUSED(nodeContext);
++ Q_UNUSED(includeSourceTimeStamp);
++ Q_UNUSED(range);
++
++ UA_Variant localeIdsVar;
++ UA_Variant_init(&localeIdsVar);
++
++ auto result = UA_Server_getSessionAttribute(server, sessionId, {0, UA_STRING_STATIC("localeIds")}, &localeIdsVar);
++
++ if (result != UA_STATUSCODE_GOOD)
++ return result;
++
++ if (localeIdsVar.type != &UA_TYPES[UA_TYPES_STRING])
++ return UA_STATUSCODE_BADINTERNALERROR;
++
++ const auto localeIds = static_cast<UA_LocaleId *>(localeIdsVar.data);
++
++ const auto english = UA_LocalizedText{ UA_STRING_STATIC("en"), UA_STRING_STATIC("Hello")};
++ const auto german = UA_LocalizedText{ UA_STRING_STATIC("de"), UA_STRING_STATIC("Guten Tag")};
++ const auto french = UA_LocalizedText{ UA_STRING_STATIC("fr"), UA_STRING_STATIC("Bonjour")};
++
++ const UA_LocalizedText *resultValue = &english;
++
++ for (size_t i = 0; i < localeIdsVar.arrayLength; ++i) {
++ const auto currentLocaleId = QOpen62541ValueConverter::scalarToQt<QString, UA_String>(&localeIds[i]);
++ if (currentLocaleId.startsWith(u"de"_s))
++ resultValue = &german;
++ if (currentLocaleId.startsWith(u"fr"_s))
++ resultValue = &french;
++ }
++
++ UA_Variant_setScalarCopy(&value->value, resultValue, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
++ value->hasValue = true;
++
++ return UA_STATUSCODE_GOOD;
++ }
++
++ UA_StatusCode TestServer::run(volatile bool *running)
++ {
++ return UA_Server_run(m_server, running);
++ }
++
++ UA_StatusCode TestServer::addEventTrigger(const UA_NodeId &parent)
++ {
++ UA_ObjectTypeAttributes objectAttr = UA_ObjectTypeAttributes_default;
++ auto browseName = UA_QUALIFIEDNAME_ALLOC(2, "QtOpcUaEventType");
++ auto result = UA_Server_addObjectTypeNode(m_server, UA_NODEID_NUMERIC(2, 12345),
++ UA_NODEID_NUMERIC(0, UA_NS0ID_BASEEVENTTYPE),
++ UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE),
++ browseName,
++ objectAttr, nullptr, nullptr);
++
++ UA_QualifiedName_clear(&browseName);
++
++ if (result != UA_STATUSCODE_GOOD)
++ return result;
++
++ UA_MethodAttributes methodAttr = UA_MethodAttributes_default;
++ methodAttr.description = UA_LOCALIZEDTEXT_ALLOC("en","Generates an event");
++ methodAttr.displayName = UA_LOCALIZEDTEXT_ALLOC("en","Generates an event");
++ methodAttr.executable = true;
++ methodAttr.userExecutable = true;
++
++ UA_Argument severityArgument;
++ UA_Argument_init(&severityArgument);
++ severityArgument.name = UA_STRING_ALLOC("Severity");
++ severityArgument.dataType = UA_TYPES[UA_TYPES_UINT16].typeId;
++ severityArgument.valueRank = -1;
++ severityArgument.description = UA_LOCALIZEDTEXT_ALLOC("en", "The severity for the event to emit");
++
++ UA_NodeId eventTriggerMethodId = UA_NODEID_STRING_ALLOC(2, "TriggerEvent");
++
++ browseName = UA_QUALIFIEDNAME_ALLOC(2, "TriggerEvent");
++ result = UA_Server_addMethodNode(m_server, eventTriggerMethodId,
++ parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT),
++ browseName,
++ methodAttr, &generateEventCallback,
++ 1, &severityArgument, 0, nullptr, nullptr, nullptr);
++
++ UA_NodeId_clear(&eventTriggerMethodId);
++ UA_QualifiedName_clear(&browseName);
++ UA_MethodAttributes_clear(&methodAttr);
++ UA_Argument_clear(&severityArgument);
++
++ return result;
++ }
++
++ static UA_DataTypeArray customTypes = {
++ nullptr,
++ UA_TYPES_QTOPCUATESTMODEL_COUNT,
++ &UA_TYPES_QTOPCUATESTMODEL[0],
++ false
++ };
++
++ // This method must be called after the other test namespaces have been added
++ UA_StatusCode TestServer::addEncoderTestModel()
++ {
++ auto result = namespace_qtopcuatestmodel_generated(m_server);
++
++ if (result != UA_STATUSCODE_GOOD)
++ return result;
++
++ auto config = UA_Server_getConfig(m_server);
++ customTypes.next = config->customDataTypes;
++ config->customDataTypes = &customTypes;
++
++ {
++ auto data = UA_QtTestStructType_new();
++
++ data->enumMember = UA_QTTESTENUMERATION_FIRSTOPTION;
++ data->int64ArrayMemberSize = 3;
++ data->int64ArrayMember = static_cast<UA_Int64 *>(UA_Array_new(3, &UA_TYPES[UA_TYPES_INT64]));
++ data->int64ArrayMember[0] = std::numeric_limits<qint64>::max();
++ data->int64ArrayMember[1] = std::numeric_limits<qint64>::max() - 1;
++ data->int64ArrayMember[2] = std::numeric_limits<qint64>::min();
++
++ data->stringMember = UA_STRING_ALLOC("TestString");
++ data->localizedTextMember = UA_LOCALIZEDTEXT_ALLOC("en", "TestText");
++ data->qualifiedNameMember = UA_QUALIFIEDNAME_ALLOC(1, "TestName");
++
++ data->nestedStructMember.doubleSubtypeMember = 42;
++
++ data->nestedStructArrayMemberSize = 2;
++ data->nestedStructArrayMember = static_cast<UA_QtInnerTestStructType *>(
++ UA_Array_new(2, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTINNERTESTSTRUCTTYPE]));
++
++ data->nestedStructArrayMember[0].doubleSubtypeMember = 23.0;
++ data->nestedStructArrayMember[1].doubleSubtypeMember = 42.0;
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalar(&var, data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTTESTSTRUCTTYPE]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_NESTEDSTRUCT), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write nested struct node";
++ return result;
++ }
++ }
++
++ {
++ UA_QtTestUnionType data;
++ UA_QtTestUnionType_init(&data);
++
++ data.switchField = UA_QTTESTUNIONTYPESWITCH_MEMBER1;
++ data.fields.member1 = 42;
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalarCopy(&var, &data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTTESTUNIONTYPE]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_UNIONWITHFIRSTMEMBER), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write union struct node";
++ return result;
++ }
++ }
++
++ {
++ UA_QtTestUnionType data;
++ UA_QtTestUnionType_init(&data);
++
++ data.switchField = UA_QTTESTUNIONTYPESWITCH_MEMBER2;
++ data.fields.member2.doubleSubtypeMember = 23.0;
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalarCopy(&var, &data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTTESTUNIONTYPE]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_UNIONWITHSECONDMEMBER), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write union struct node";
++ return result;
++ }
++ }
++
++ {
++ auto data = UA_QtStructWithOptionalFieldType_new();
++
++ data->mandatoryMember = 42.0;
++ data->optionalMember = UA_Double_new();
++ *data->optionalMember = 23.0;
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalar(&var, data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTSTRUCTWITHOPTIONALFIELDTYPE]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_STRUCTWITHOPTIONALFIELD), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write optional field struct node";
++ return result;
++ }
++ }
++
++ {
++ UA_QtStructWithOptionalFieldType data;
++ UA_QtStructWithOptionalFieldType_init(&data);
++
++ data.mandatoryMember = 42.0;
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalarCopy(&var, &data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTSTRUCTWITHOPTIONALFIELDTYPE]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_STRUCTWIHOUTOPTIONALFIELD), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write optional field struct node";
++ return result;
++ }
++ }
++
++ {
++ auto data = UA_QtTestStructWithDiagnosticInfo_new();
++
++ data->diagnosticInfoMember.hasSymbolicId = true;
++ data->diagnosticInfoMember.symbolicId = 1;
++ data->diagnosticInfoMember.hasNamespaceUri = true;
++ data->diagnosticInfoMember.namespaceUri = 2;
++ data->diagnosticInfoMember.hasLocale = true;
++ data->diagnosticInfoMember.locale = 3;
++ data->diagnosticInfoMember.hasLocalizedText = true;
++ data->diagnosticInfoMember.localizedText = 4;
++ data->diagnosticInfoMember.hasAdditionalInfo = true;
++ data->diagnosticInfoMember.additionalInfo = UA_STRING_ALLOC("My additional info");
++ data->diagnosticInfoMember.hasInnerStatusCode = true;
++ data->diagnosticInfoMember.innerStatusCode = UA_STATUSCODE_BADINTERNALERROR;
++ data->diagnosticInfoMember.hasInnerDiagnosticInfo = true;
++
++ data->diagnosticInfoMember.innerDiagnosticInfo = UA_DiagnosticInfo_new();
++ data->diagnosticInfoMember.innerDiagnosticInfo->hasAdditionalInfo = true;
++ data->diagnosticInfoMember.innerDiagnosticInfo->additionalInfo = UA_STRING_ALLOC("My inner additional info");
++
++ data->diagnosticInfoArrayMemberSize = 2;
++ data->diagnosticInfoArrayMember = static_cast<UA_DiagnosticInfo *>(UA_Array_new(2, &UA_TYPES[UA_TYPES_DIAGNOSTICINFO]));
++
++ UA_DiagnosticInfo_copy(&data->diagnosticInfoMember, &data->diagnosticInfoArrayMember[0]);
++
++ data->diagnosticInfoArrayMember[1].hasLocale = true;
++ data->diagnosticInfoArrayMember[1].locale = 1;
++ data->diagnosticInfoArrayMember[1].hasInnerStatusCode = true;
++ data->diagnosticInfoArrayMember[1].innerStatusCode = UA_STATUSCODE_BADTYPEMISMATCH;
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalar(&var, data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTTESTSTRUCTWITHDIAGNOSTICINFO]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_STRUCTWITHDIAGNOSTICINFO), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write diagnostic info field struct node";
++ return result;
++ }
++ }
++
++ {
++ auto data = UA_QtTestStructWithDataValue_new();
++
++ auto stringArray = static_cast<UA_String *>(UA_Array_new(4, &UA_TYPES[UA_TYPES_STRING]));
++ stringArray[0] = UA_STRING_ALLOC("TestString 1");
++ stringArray[1] = UA_STRING_ALLOC("TestString 2");
++ stringArray[2] = UA_STRING_ALLOC("TestString 3");
++ stringArray[3] = UA_STRING_ALLOC("TestString 4");
++ UA_Variant_setArray(&data->dataValueMember.value, stringArray, 4, &UA_TYPES[UA_TYPES_STRING]);
++
++ data->dataValueMember.value.arrayLength = 4;
++ data->dataValueMember.value.arrayDimensions = static_cast<quint32 *>(UA_Array_new(2, &UA_TYPES[UA_TYPES_UINT32]));
++ data->dataValueMember.value.arrayDimensions[0] = 2;
++ data->dataValueMember.value.arrayDimensions[1] = 2;
++ data->dataValueMember.value.arrayDimensionsSize = 2;
++
++ data->dataValueMember.hasValue = true;
++ data->dataValueMember.status = UA_STATUSCODE_BADINTERNALERROR;
++ data->dataValueMember.hasStatus = true;
++ data->dataValueMember.serverTimestamp = UA_DateTime_fromUnixTime(1698655307);
++ data->dataValueMember.hasServerTimestamp = true;
++ data->dataValueMember.sourceTimestamp = UA_DateTime_fromUnixTime(1698655306);
++ data->dataValueMember.hasSourceTimestamp = true;
++ data->dataValueMember.serverPicoseconds = 23;
++ data->dataValueMember.hasServerPicoseconds = true;
++ data->dataValueMember.sourcePicoseconds = 42;
++ data->dataValueMember.hasSourcePicoseconds = true;
++
++ const quint64 num = 42;
++ UA_Variant_setScalarCopy(&data->variantMember, &num, &UA_TYPES[UA_TYPES_UINT64]);
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalar(&var, data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTTESTSTRUCTWITHDATAVALUE]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_STRUCTWITHDATAVALUE), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write data value field struct node";
++ return result;
++ }
++ }
++
++ {
++ auto data = UA_QtRecursiveTestStruct_new();
++
++ auto nestedArray = static_cast<UA_QtRecursiveTestStruct *>(UA_Array_new(2, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTRECURSIVETESTSTRUCT]));
++ nestedArray[0].stringMember = UA_STRING_ALLOC("Nested string 1");
++ nestedArray[1].stringMember = UA_STRING_ALLOC("Nested string 2");
++
++ nestedArray[0].recursiveArrayMember = UA_QtRecursiveTestStruct_new();
++ nestedArray[0].recursiveArrayMemberSize = 1;
++ nestedArray[0].recursiveArrayMember->stringMember = UA_STRING_ALLOC("Innermost string");
++
++ data->recursiveArrayMember = nestedArray;
++ data->recursiveArrayMemberSize = 2;
++ data->stringMember = UA_STRING_ALLOC("Outer string");
++
++ UA_Variant var;
++ UA_Variant_init(&var);
++ UA_Variant_setScalar(&var, data, &UA_TYPES_QTOPCUATESTMODEL[UA_TYPES_QTOPCUATESTMODEL_QTRECURSIVETESTSTRUCT]);
++
++ result = UA_Server_writeValue(m_server, UA_NODEID_NUMERIC(4, UA_QTOPCUATESTMODELID_DECODERTESTNODES_RECURSIVESTRUCT), var);
++ UA_Variant_clear(&var);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to write recursive struct node";
++ return result;
++ }
++ }
++
++ return result;
++ }
++
++ UA_StatusCode TestServer::addUnreadableVariableNode(const UA_NodeId &parent)
++ {
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = QOpen62541ValueConverter::toOpen62541Variant(42, QOpcUa::Int32);
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", "VariableWithoutReadPermission");
++ attr.dataType = attr.value.type->typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_WRITE; // Write only
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = parent.namespaceIndex;
++ variableName.name = attr.displayName.text;
++
++ auto nodeId = UA_NODEID_STRING_ALLOC(parent.namespaceIndex, "VariableWithoutReadPermission");
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ nodeId,
++ parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ nullptr);
++ UA_NodeId_clear(&nodeId);
++ UA_VariableAttributes_clear(&attr);
++ return result;
++ }
++
++ UA_StatusCode TestServer::addByteStringNodeIdWithNullIdVariableNode(const UA_NodeId &parent)
++ {
++ UA_VariableAttributes attr = UA_VariableAttributes_default;
++ attr.value = QOpen62541ValueConverter::toOpen62541Variant(42, QOpcUa::Int32);
++ attr.displayName = UA_LOCALIZEDTEXT_ALLOC("en-US", "VariableWithByteStringWithNullByteId");
++ attr.dataType = attr.value.type->typeId;
++ attr.accessLevel = UA_ACCESSLEVELMASK_READ;
++
++ UA_QualifiedName variableName;
++ variableName.namespaceIndex = parent.namespaceIndex;
++ variableName.name = attr.displayName.text;
++
++ UA_NodeId nodeId = UA_NODEID_NULL;
++ nodeId.namespaceIndex = 1;
++ nodeId.identifierType = UA_NODEIDTYPE_BYTESTRING;
++ const auto bytes = QByteArray::fromBase64(u"AAABAAIADoo="_s.toLatin1());
++ UA_ByteString_allocBuffer(&nodeId.identifier.byteString, bytes.length());
++ memcpy(nodeId.identifier.byteString.data, bytes.constData(), bytes.length());
++
++ UA_StatusCode result = UA_Server_addVariableNode(m_server,
++ nodeId,
++ parent,
++ UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ variableName,
++ UA_NODEID_NULL,
++ attr,
++ nullptr,
++ nullptr);
++ UA_NodeId_clear(&nodeId);
++ UA_VariableAttributes_clear(&attr);
++ return result;
++ }
++
++ UA_StatusCode TestServer::addEventHistorian(const UA_NodeId &parent)
++ {
++ UA_ObjectAttributes attr = UA_ObjectAttributes_default;
++ attr.eventNotifier = UA_EVENTNOTIFIER_SUBSCRIBE_TO_EVENT | UA_EVENTNOTIFIER_HISTORY_READ;
++ attr.description = UA_LocalizedText{UA_STRING_STATIC("en"),
++ UA_STRING_STATIC("This node does not conform to part 11 of the OPC UA specification"
++ "and only supports the event history unit tests scenario")};
++
++ UA_NodeId objectId = UA_NODEID_STRING_ALLOC(2, "EventHistorian");
++ auto result = UA_Server_addObjectNode(m_server, objectId, parent, UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
++ UA_QualifiedName{2, UA_STRING_STATIC("EventHistorian")},
++ UA_NODEID_NUMERIC(0, UA_NS0ID_BASEOBJECTTYPE), attr, this, nullptr);
++
++ UA_NodeId_clear(&objectId);
++
++ if (result != UA_STATUSCODE_GOOD) {
++ qWarning() << "Failed to add event historian object:" << UA_StatusCode_name(result);
++ return result;
++ }
++
++ m_config->historyDatabase.readEvent = readHistoryEventCallback;
++
++ return UA_STATUSCODE_GOOD;
++ }
++
++ QT_END_NAMESPACE