1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#ifdef USE_SYSTEM_OPEN62541
5#include <open62541/client_config_default.h>
6#include <open62541/plugin/securitypolicy_default.h>
7#include <open62541/plugin/pki_default.h>
8#endif
9
10#include "qopen62541backend.h"
11#include "qopen62541node.h"
12#include "qopen62541utils.h"
13#include "qopen62541valueconverter.h"
14#include <private/qopcuaclient_p.h>
15#include <private/qopcuasecuritypolicyuris_p.h>
16
17#include "qopcuaauthenticationinformation.h"
18#include <qopcuaerrorstate.h>
19
20#include <QtCore/QDir>
21#include <QtCore/QFile>
22#include <QtCore/qloggingcategory.h>
23#include <QtCore/qstringlist.h>
24#include <QtCore/qurl.h>
25#include <QtCore/private/qnumeric_p.h> // for qt_saturate
26
27#include <algorithm>
28#include <limits>
29
30QT_BEGIN_NAMESPACE
31
32Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_OPEN62541)
33
34Open62541AsyncBackend::Open62541AsyncBackend(QOpen62541Client *parent)
35 : QOpcUaBackend()
36 , m_uaclient(nullptr)
37 , m_clientImpl(parent)
38 , m_clientIterateInterval(50)
39 , m_asyncRequestTimeout(15000)
40 , m_clientIterateTimer(this)
41 , m_clientIterateOnDemandTimer(this)
42 , m_minPublishingInterval(0)
43{
44 QObject::connect(sender: &m_clientIterateTimer, signal: &QTimer::timeout,
45 context: this, slot: &Open62541AsyncBackend::iterateClient);
46
47 m_clientIterateOnDemandTimer.setSingleShot(true);
48 QObject::connect(sender: &m_clientIterateOnDemandTimer, signal: &QTimer::timeout,
49 context: this, slot: &Open62541AsyncBackend::iterateClient);
50
51 QObject::connect(sender: parent, signal: &QOpcUaClientImpl::connectionSettingsChanged,
52 context: this, slot: &Open62541AsyncBackend::handleConnectionSettingsChanged);
53}
54
55Open62541AsyncBackend::~Open62541AsyncBackend()
56{
57 cleanupSubscriptions();
58 if (m_uaclient)
59 UA_Client_delete(client: m_uaclient);
60}
61
62void Open62541AsyncBackend::readAttributes(quint64 handle, UA_NodeId id, QOpcUa::NodeAttributes attr, QString indexRange)
63{
64 UaDeleter<UA_NodeId> nodeIdDeleter(&id, UA_NodeId_clear);
65
66 if (!m_uaclient) {
67 QList<QOpcUaReadResult> resultMetadata;
68
69 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
70 QOpcUaReadResult temp;
71 temp.setAttribute(attribute);
72 resultMetadata.push_back(t: temp);
73 });
74 emit attributesRead(handle, attributes: resultMetadata, serviceResult: QOpcUa::UaStatusCode::BadDisconnect);
75 return;
76 }
77
78 UA_ReadRequest req;
79 UA_ReadRequest_init(p: &req);
80 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
81 UaDeleter<UA_ReadRequest> requestDeleter(&req, UA_ReadRequest_clear);
82 req.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
83
84 qt_forEachAttribute(attributes: attr, f: [&req](QOpcUa::NodeAttribute attr) {
85 Q_UNUSED(attr);
86 ++req.nodesToReadSize;
87 });
88
89 if (req.nodesToReadSize)
90 req.nodesToRead = static_cast<UA_ReadValueId *>(UA_Array_new(size: req.nodesToReadSize, type: &UA_TYPES[UA_TYPES_READVALUEID]));
91
92 QList<QOpcUaReadResult> resultMetadata;
93 size_t index = 0;
94 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
95 auto &current = req.nodesToRead[index++];
96
97 current.attributeId = QOpen62541ValueConverter::toUaAttributeId(attr: attribute);
98 UA_NodeId_copy(src: &id, dst: &current.nodeId);
99 if (indexRange.size())
100 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: indexRange, ptr: &current.indexRange);
101
102 QOpcUaReadResult temp;
103 temp.setAttribute(attribute);
104 resultMetadata.push_back(t: temp);
105 });
106
107 quint32 requestId = 0;
108 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_READREQUEST],
109 callback: &asyncReadCallback, responseType: &UA_TYPES[UA_TYPES_READRESPONSE], userdata: this,
110 requestId: &requestId);
111
112 if (result != UA_STATUSCODE_GOOD) {
113 const auto statusCode = static_cast<QOpcUa::UaStatusCode>(result);
114 for (auto &entry : resultMetadata) {
115 entry.setStatusCode(statusCode);
116 }
117 emit attributesRead(handle, attributes: resultMetadata, serviceResult: statusCode);
118 return;
119 }
120
121 m_asyncReadContext[requestId] = { .handle: handle, .results: resultMetadata };
122
123 triggerIterateClient();
124}
125
126void Open62541AsyncBackend::writeAttribute(quint64 handle, UA_NodeId id, QOpcUa::NodeAttribute attrId, QVariant value, QOpcUa::Types type, QString indexRange)
127{
128 if (!m_uaclient) {
129 UA_NodeId_clear(p: &id);
130 emit attributeWritten(handle, attribute: attrId, value, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
131 return;
132 }
133
134 if (type == QOpcUa::Types::Undefined && attrId != QOpcUa::NodeAttribute::Value)
135 type = attributeIdToTypeId(attr: attrId);
136
137 UA_WriteRequest req;
138 UA_WriteRequest_init(p: &req);
139 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
140 UaDeleter<UA_WriteRequest> requestDeleter(&req, UA_WriteRequest_clear);
141 req.nodesToWriteSize = 1;
142 req.nodesToWrite = UA_WriteValue_new();
143
144 UA_WriteValue_init(p: req.nodesToWrite);
145 req.nodesToWrite->attributeId = QOpen62541ValueConverter::toUaAttributeId(attr: attrId);
146 req.nodesToWrite->nodeId = id;
147 req.nodesToWrite->value.value = QOpen62541ValueConverter::toOpen62541Variant(value, type);
148 req.nodesToWrite->value.hasValue = true;
149 if (indexRange.size())
150 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: indexRange, ptr: &req.nodesToWrite->indexRange);
151
152 quint32 requestId = 0;
153 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_WRITEREQUEST],
154 callback: &asyncWriteAttributesCallback, responseType: &UA_TYPES[UA_TYPES_WRITERESPONSE], userdata: this,
155 requestId: &requestId);
156
157 if (result != UA_STATUSCODE_GOOD) {
158 emit attributeWritten(handle, attribute: attrId, value, statusCode: static_cast<QOpcUa::UaStatusCode>(result));
159 return;
160 }
161
162 m_asyncWriteAttributesContext[requestId] = { .handle: handle, .toWrite: {{attrId, value}} };
163 triggerIterateClient();
164}
165
166void Open62541AsyncBackend::writeAttributes(quint64 handle, UA_NodeId id, QOpcUaNode::AttributeMap toWrite, QOpcUa::Types valueAttributeType)
167{
168 UaDeleter<UA_NodeId> nodeIdDeleter(&id, UA_NodeId_clear);
169
170 if (!m_uaclient) {
171 emit attributeWritten(handle, attribute: QOpcUa::NodeAttribute::None, value: QVariant(), statusCode: QOpcUa::UaStatusCode::BadDisconnect);
172 return;
173 }
174
175 if (toWrite.size() == 0) {
176 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No values to be written";
177 emit attributeWritten(handle, attribute: QOpcUa::NodeAttribute::None, value: QVariant(), statusCode: QOpcUa::UaStatusCode::BadNothingToDo);
178 return;
179 }
180
181 UA_WriteRequest req;
182 UA_WriteRequest_init(p: &req);
183 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
184 UaDeleter<UA_WriteRequest> requestDeleter(&req, UA_WriteRequest_clear);
185 req.nodesToWriteSize = toWrite.size();
186 req.nodesToWrite = static_cast<UA_WriteValue *>(UA_Array_new(size: req.nodesToWriteSize, type: &UA_TYPES[UA_TYPES_WRITEVALUE]));
187 size_t index = 0;
188 for (auto it = toWrite.begin(); it != toWrite.end(); ++it, ++index) {
189 UA_WriteValue_init(p: &(req.nodesToWrite[index]));
190 req.nodesToWrite[index].attributeId = QOpen62541ValueConverter::toUaAttributeId(attr: it.key());
191 UA_NodeId_copy(src: &id, dst: &(req.nodesToWrite[index].nodeId));
192 QOpcUa::Types type = it.key() == QOpcUa::NodeAttribute::Value ? valueAttributeType : attributeIdToTypeId(attr: it.key());
193 req.nodesToWrite[index].value.value = QOpen62541ValueConverter::toOpen62541Variant(it.value(), type);
194 }
195
196 quint32 requestId = 0;
197 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_WRITEREQUEST],
198 callback: &asyncWriteAttributesCallback, responseType: &UA_TYPES[UA_TYPES_WRITERESPONSE], userdata: this,
199 requestId: &requestId);
200
201 if (result != UA_STATUSCODE_GOOD) {
202 for (auto it = toWrite.begin(); it != toWrite.end(); ++it) {
203 emit attributeWritten(handle, attribute: it.key(), value: it.value(), statusCode: static_cast<QOpcUa::UaStatusCode>(result));
204 }
205 return;
206 }
207
208 m_asyncWriteAttributesContext[requestId] = { .handle: handle, .toWrite: toWrite };
209 triggerIterateClient();
210}
211
212void Open62541AsyncBackend::enableMonitoring(quint64 handle, UA_NodeId id, QOpcUa::NodeAttributes attr, const QOpcUaMonitoringParameters &settings)
213{
214 UaDeleter<UA_NodeId> nodeIdDeleter(&id, UA_NodeId_clear);
215
216 if (!m_uaclient) {
217 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
218 QOpcUaMonitoringParameters s;
219 s.setStatusCode(QOpcUa::UaStatusCode::BadDisconnect);
220 emit monitoringEnableDisable(handle, attr: attribute, subscribe: true, status: s);
221 });
222 return;
223 }
224
225 QOpen62541Subscription *usedSubscription = nullptr;
226
227 // Create a new subscription if necessary
228 if (settings.subscriptionId()) {
229 auto sub = m_subscriptions.find(key: settings.subscriptionId());
230 if (sub == m_subscriptions.end()) {
231 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "There is no subscription with id" << settings.subscriptionId();
232
233 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
234 QOpcUaMonitoringParameters s;
235 s.setStatusCode(QOpcUa::UaStatusCode::BadSubscriptionIdInvalid);
236 emit monitoringEnableDisable(handle, attr: attribute, subscribe: true, status: s);
237 });
238 return;
239 }
240 usedSubscription = sub.value(); // Ignore interval != subscription.interval
241 } else {
242 usedSubscription = getSubscription(settings);
243 }
244
245 if (!usedSubscription) {
246 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not create subscription with interval" << settings.publishingInterval();
247 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
248 QOpcUaMonitoringParameters s;
249 s.setStatusCode(QOpcUa::UaStatusCode::BadSubscriptionIdInvalid);
250 emit monitoringEnableDisable(handle, attr: attribute, subscribe: true, status: s);
251 });
252 return;
253 }
254
255 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
256 if (getSubscriptionForItem(handle, attr: attribute)) {
257 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Monitored item for" << attribute << "has already been created";
258 QOpcUaMonitoringParameters s;
259 s.setStatusCode(QOpcUa::UaStatusCode::BadEntryExists);
260 emit monitoringEnableDisable(handle, attr: attribute, subscribe: true, status: s);
261 } else {
262 bool success = usedSubscription->addAttributeMonitoredItem(handle, attr: attribute, id, settings);
263 if (success)
264 m_attributeMapping[handle][attribute] = usedSubscription;
265 }
266 });
267
268 if (usedSubscription->monitoredItemsCount() == 0)
269 removeSubscription(subscriptionId: usedSubscription->subscriptionId()); // No items were added
270}
271
272void Open62541AsyncBackend::disableMonitoring(quint64 handle, QOpcUa::NodeAttributes attr)
273{
274 if (!m_uaclient) {
275 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
276 QOpcUaMonitoringParameters s;
277 s.setStatusCode(QOpcUa::UaStatusCode::BadDisconnect);
278 emit monitoringEnableDisable(handle, attr: attribute, subscribe: false, status: s);
279 });
280 return;
281 }
282
283 qt_forEachAttribute(attributes: attr, f: [&](QOpcUa::NodeAttribute attribute){
284 QOpen62541Subscription *sub = getSubscriptionForItem(handle, attr: attribute);
285 if (sub) {
286 sub->removeAttributeMonitoredItem(handle, attr: attribute);
287 m_attributeMapping[handle].remove(key: attribute);
288 if (sub->monitoredItemsCount() == 0)
289 removeSubscription(subscriptionId: sub->subscriptionId());
290 } else {
291 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "There is no monitored item for this attribute";
292 QOpcUaMonitoringParameters s;
293 s.setStatusCode(QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid);
294 emit monitoringEnableDisable(handle, attr: attribute, subscribe: false, status: s);
295 }
296 });
297}
298
299void Open62541AsyncBackend::modifyMonitoring(quint64 handle, QOpcUa::NodeAttribute attr, QOpcUaMonitoringParameters::Parameter item, QVariant value)
300{
301 if (!m_uaclient) {
302 QOpcUaMonitoringParameters p;
303 p.setStatusCode(QOpcUa::UaStatusCode::BadDisconnect);
304 emit monitoringStatusChanged(handle, attr, items: item, param: p);
305 return;
306 }
307
308 QOpen62541Subscription *subscription = getSubscriptionForItem(handle, attr);
309 if (!subscription) {
310 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Could not modify" << item << ", the monitored item does not exist";
311 QOpcUaMonitoringParameters p;
312 p.setStatusCode(QOpcUa::UaStatusCode::BadMonitoredItemIdInvalid);
313 emit monitoringStatusChanged(handle, attr, items: item, param: p);
314 return;
315 }
316
317 subscription->modifyMonitoring(handle, attr, item, value);
318}
319
320QOpen62541Subscription *Open62541AsyncBackend::getSubscription(const QOpcUaMonitoringParameters &settings)
321{
322 if (settings.subscriptionType() == QOpcUaMonitoringParameters::SubscriptionType::Shared) {
323 // Requesting multiple subscriptions with publishing interval < minimum publishing interval breaks subscription sharing
324 double interval = revisePublishingInterval(requestedValue: settings.publishingInterval(), minimumValue: m_minPublishingInterval);
325 for (auto entry : std::as_const(t&: m_subscriptions)) {
326 if (qFuzzyCompare(p1: entry->interval(), p2: interval) && entry->shared() == QOpcUaMonitoringParameters::SubscriptionType::Shared)
327 return entry;
328 }
329 }
330
331 QOpen62541Subscription *sub = new QOpen62541Subscription(this, settings);
332 UA_UInt32 id = sub->createOnServer();
333 if (!id) {
334 delete sub;
335 return nullptr;
336 }
337 m_subscriptions[id] = sub;
338 if (sub->interval() > settings.publishingInterval()) // The publishing interval has been revised by the server.
339 m_minPublishingInterval = sub->interval();
340 // This must be a queued connection to prevent the slot from being called while the client is inside UA_Client_run_iterate().
341 QObject::connect(sender: sub, signal: &QOpen62541Subscription::timeout, context: this, slot: &Open62541AsyncBackend::handleSubscriptionTimeout, type: Qt::QueuedConnection);
342 return sub;
343}
344
345bool Open62541AsyncBackend::removeSubscription(UA_UInt32 subscriptionId)
346{
347 auto sub = m_subscriptions.find(key: subscriptionId);
348 if (sub != m_subscriptions.end()) {
349 sub.value()->removeOnServer();
350 delete sub.value();
351 m_subscriptions.remove(key: subscriptionId);
352 return true;
353 }
354 return false;
355}
356
357void Open62541AsyncBackend::callMethod(quint64 handle, UA_NodeId objectId, UA_NodeId methodId, QList<QOpcUa::TypedVariant> args)
358{
359 if (!m_uaclient) {
360 emit methodCallFinished(handle, methodNodeId: Open62541Utils::nodeIdToQString(id: methodId), result: QVariant(), statusCode: QOpcUa::UaStatusCode::BadDisconnect);
361 UA_NodeId_clear(p: &objectId);
362 UA_NodeId_clear(p: &methodId);
363 return;
364 }
365
366 UA_Variant *inputArgs = nullptr;
367
368 if (args.size()) {
369 inputArgs = static_cast<UA_Variant *>(UA_Array_new(size: args.size(), type: &UA_TYPES[UA_TYPES_VARIANT]));
370 for (qsizetype i = 0; i < args.size(); ++i)
371 inputArgs[i] = QOpen62541ValueConverter::toOpen62541Variant(args[i].first, args[i].second);
372 }
373
374 quint32 requestId = 0;
375
376 UA_CallRequest request;
377 UA_CallRequest_init(p: &request);
378 request.requestHeader.timeoutHint = m_asyncRequestTimeout;
379 UaDeleter<UA_CallRequest> requestDeleter(&request, UA_CallRequest_clear);
380
381 request.methodsToCallSize = 1;
382 request.methodsToCall = UA_CallMethodRequest_new();
383 request.methodsToCall->objectId = objectId;
384 request.methodsToCall->methodId = methodId;
385 request.methodsToCall->inputArguments = inputArgs;
386 request.methodsToCall->inputArgumentsSize = args.size();
387
388 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &request, requestType: &UA_TYPES[UA_TYPES_CALLREQUEST],
389 callback: &asyncMethodCallback,
390 responseType: &UA_TYPES[UA_TYPES_CALLRESPONSE],
391 userdata: this, requestId: &requestId);
392 if (result != UA_STATUSCODE_GOOD)
393 emit methodCallFinished(handle, methodNodeId: Open62541Utils::nodeIdToQString(id: methodId), result: QVariant(),
394 statusCode: static_cast<QOpcUa::UaStatusCode>(result));
395
396 m_asyncCallContext[requestId] = { .handle: handle, .methodNodeId: Open62541Utils::nodeIdToQString(id: methodId) };
397 triggerIterateClient();
398}
399
400void Open62541AsyncBackend::resolveBrowsePath(quint64 handle, UA_NodeId startNode, const QList<QOpcUaRelativePathElement> &path)
401{
402 if (!m_uaclient) {
403 UA_NodeId_clear(p: &startNode);
404 emit resolveBrowsePathFinished(handle, targets: {}, path, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
405 return;
406 }
407
408 UA_TranslateBrowsePathsToNodeIdsRequest req;
409 UA_TranslateBrowsePathsToNodeIdsRequest_init(p: &req);
410 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
411 UaDeleter<UA_TranslateBrowsePathsToNodeIdsRequest> requestDeleter(
412 &req,UA_TranslateBrowsePathsToNodeIdsRequest_clear);
413
414 req.browsePathsSize = 1;
415 req.browsePaths = UA_BrowsePath_new();
416 UA_BrowsePath_init(p: req.browsePaths);
417 req.browsePaths->startingNode = startNode;
418 req.browsePaths->relativePath.elementsSize = path.size();
419 req.browsePaths->relativePath.elements = static_cast<UA_RelativePathElement *>(UA_Array_new(size: path.size(), type: &UA_TYPES[UA_TYPES_RELATIVEPATHELEMENT]));
420
421 for (qsizetype i = 0 ; i < path.size(); ++i) {
422 req.browsePaths->relativePath.elements[i].includeSubtypes = path[i].includeSubtypes();
423 req.browsePaths->relativePath.elements[i].isInverse = path[i].isInverse();
424 req.browsePaths->relativePath.elements[i].referenceTypeId = Open62541Utils::nodeIdFromQString(name: path[i].referenceTypeId());
425 req.browsePaths->relativePath.elements[i].targetName = UA_QUALIFIEDNAME_ALLOC(nsIndex: path[i].targetName().namespaceIndex(),
426 chars: path[i].targetName().name().toUtf8().constData());
427 }
428
429 quint32 requestId = 0;
430 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_TRANSLATEBROWSEPATHSTONODEIDSREQUEST],
431 callback: &asyncTranslateBrowsePathCallback,
432 responseType: &UA_TYPES[UA_TYPES_TRANSLATEBROWSEPATHSTONODEIDSRESPONSE],
433 userdata: this, requestId: &requestId);
434
435 if (result != UA_STATUSCODE_GOOD) {
436 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Translate browse path failed:" << UA_StatusCode_name(code: result);
437 emit resolveBrowsePathFinished(handle, targets: QList<QOpcUaBrowsePathTarget>(), path,
438 statusCode: static_cast<QOpcUa::UaStatusCode>(result));
439 return;
440 }
441
442 m_asyncTranslateContext[requestId] = { .handle: handle, .path: path };
443 triggerIterateClient();
444}
445
446void Open62541AsyncBackend::open62541LogHandler (void *logContext, UA_LogLevel level, UA_LogCategory category,
447 const char *msg, va_list args) {
448
449 Q_UNUSED(logContext)
450
451 Q_STATIC_ASSERT(UA_LOGCATEGORY_NETWORK == 0);
452 Q_STATIC_ASSERT(UA_LOGCATEGORY_SECURECHANNEL == 1);
453 Q_STATIC_ASSERT(UA_LOGCATEGORY_SESSION == 2);
454 Q_STATIC_ASSERT(UA_LOGCATEGORY_SERVER == 3);
455 Q_STATIC_ASSERT(UA_LOGCATEGORY_CLIENT == 4);
456 Q_STATIC_ASSERT(UA_LOGCATEGORY_USERLAND == 5);
457 Q_STATIC_ASSERT(UA_LOGCATEGORY_SECURITYPOLICY == 6);
458 Q_STATIC_ASSERT(UA_LOGCATEGORY_EVENTLOOP == 7);
459 Q_STATIC_ASSERT(UA_LOGCATEGORY_PUBSUB == 8);
460 Q_STATIC_ASSERT(UA_LOGCATEGORY_DISCOVERY == 9);
461
462 Q_ASSERT(category <= UA_LOGCATEGORY_DISCOVERY);
463
464 const auto logMessage = QString::vasprintf(format: msg, ap: args);
465
466 static const QLoggingCategory loggingCategories[] {
467 QLoggingCategory("qt.opcua.plugins.open62541.sdk.network"),
468 QLoggingCategory("qt.opcua.plugins.open62541.sdk.securechannel"),
469 QLoggingCategory("qt.opcua.plugins.open62541.sdk.session"),
470 QLoggingCategory("qt.opcua.plugins.open62541.sdk.server"),
471 QLoggingCategory("qt.opcua.plugins.open62541.sdk.client"),
472 QLoggingCategory("qt.opcua.plugins.open62541.sdk.userland"),
473 QLoggingCategory("qt.opcua.plugins.open62541.sdk.securitypolicy"),
474 QLoggingCategory("qt.opcua.plugins.open62541.sdk.eventloop"),
475 QLoggingCategory("qt.opcua.plugins.open62541.sdk.pubsub"),
476 QLoggingCategory("qt.opcua.plugins.open62541.sdk.discovery")
477 };
478
479 switch (level) {
480 case UA_LOGLEVEL_TRACE:
481 case UA_LOGLEVEL_DEBUG:
482 qCDebug(loggingCategories[category]).noquote() << logMessage;
483 break;
484 case UA_LOGLEVEL_INFO:
485 qCInfo(loggingCategories[category]).noquote() << logMessage;
486 break;
487 case UA_LOGLEVEL_WARNING:
488 qCWarning(loggingCategories[category]).noquote() << logMessage;
489 break;
490 case UA_LOGLEVEL_ERROR:
491 case UA_LOGLEVEL_FATAL:
492 qCCritical(loggingCategories[category]).noquote() << logMessage;
493 break;
494 default:
495 qCCritical(loggingCategories[category]).noquote() << "Unknown UA_LOGLEVEL" << logMessage;
496 break;
497 }
498}
499
500void Open62541AsyncBackend::findServers(const QUrl &url, const QStringList &localeIds, const QStringList &serverUris)
501{
502 UA_ClientConfig initialConfig {};
503 initialConfig.logging = &m_open62541Logger;
504 UA_ClientConfig_setDefault(config: &initialConfig);
505
506 UA_Client *tmpClient = UA_Client_newWithConfig(config: &initialConfig);
507
508 UaDeleter<UA_Client> clientDeleter(tmpClient, UA_Client_delete);
509
510 UA_String *uaServerUris = nullptr;
511 if (!serverUris.isEmpty()) {
512 uaServerUris = static_cast<UA_String *>(UA_Array_new(size: serverUris.size(), type: &UA_TYPES[UA_TYPES_STRING]));
513 for (qsizetype i = 0; i < serverUris.size(); ++i)
514 QOpen62541ValueConverter::scalarFromQt(var: serverUris.at(i), ptr: &uaServerUris[i]);
515 }
516 UaArrayDeleter<UA_TYPES_STRING> serverUrisDeleter(uaServerUris, serverUris.size());
517
518 UA_String *uaLocaleIds = nullptr;
519 if (!localeIds.isEmpty()) {
520 uaLocaleIds = static_cast<UA_String *>(UA_Array_new(size: localeIds.size(), type: &UA_TYPES[UA_TYPES_STRING]));
521 for (qsizetype i = 0; i < localeIds.size(); ++i)
522 QOpen62541ValueConverter::scalarFromQt(var: localeIds.at(i), ptr: &uaLocaleIds[i]);
523 }
524 UaArrayDeleter<UA_TYPES_STRING> localeIdsDeleter(uaLocaleIds, localeIds.size());
525
526 size_t serversSize = 0;
527 UA_ApplicationDescription *servers = nullptr;
528
529 UA_StatusCode result = UA_Client_findServers(client: tmpClient, serverUrl: url.toString(options: QUrl::RemoveUserInfo).toUtf8().constData(),
530 serverUrisSize: serverUris.size(), serverUris: uaServerUris, localeIdsSize: localeIds.size(), localeIds: uaLocaleIds,
531 registeredServersSize: &serversSize, registeredServers: &servers);
532
533 UaArrayDeleter<UA_TYPES_APPLICATIONDESCRIPTION> serversDeleter(servers, serversSize);
534
535 QList<QOpcUaApplicationDescription> ret;
536
537 for (size_t i = 0; i < serversSize; ++i)
538 ret.append(t: convertApplicationDescription(desc&: servers[i]));
539
540 if (result != UA_STATUSCODE_GOOD) {
541 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to get servers:" << static_cast<QOpcUa::UaStatusCode>(result);
542 }
543
544 emit findServersFinished(servers: ret, statusCode: static_cast<QOpcUa::UaStatusCode>(result), requestUrl: url);
545}
546
547void Open62541AsyncBackend::readNodeAttributes(const QList<QOpcUaReadItem> &nodesToRead)
548{
549 if (!m_uaclient) {
550 emit readNodeAttributesFinished(results: {}, serviceResult: QOpcUa::UaStatusCode::BadDisconnect);
551 return;
552 }
553
554 if (nodesToRead.size() == 0) {
555 emit readNodeAttributesFinished(results: QList<QOpcUaReadResult>(), serviceResult: QOpcUa::UaStatusCode::BadNothingToDo);
556 return;
557 }
558
559 UA_ReadRequest req;
560 UA_ReadRequest_init(p: &req);
561 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
562 UaDeleter<UA_ReadRequest> requestDeleter(&req, UA_ReadRequest_clear);
563
564 req.nodesToReadSize = nodesToRead.size();
565 req.nodesToRead = static_cast<UA_ReadValueId *>(UA_Array_new(size: nodesToRead.size(), type: &UA_TYPES[UA_TYPES_READVALUEID]));
566 req.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
567
568 for (qsizetype i = 0; i < nodesToRead.size(); ++i) {
569 UA_ReadValueId_init(p: &req.nodesToRead[i]);
570 req.nodesToRead[i].attributeId = QOpen62541ValueConverter::toUaAttributeId(attr: nodesToRead.at(i).attribute());
571 req.nodesToRead[i].nodeId = Open62541Utils::nodeIdFromQString(name: nodesToRead.at(i).nodeId());
572 if (!nodesToRead[i].indexRange().isEmpty())
573 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: nodesToRead.at(i).indexRange(),
574 ptr: &req.nodesToRead[i].indexRange);
575 }
576
577 quint32 requestId = 0;
578 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_READREQUEST], callback: &asyncBatchReadCallback,
579 responseType: &UA_TYPES[UA_TYPES_READRESPONSE], userdata: this, requestId: &requestId);
580
581 if (result != UA_STATUSCODE_GOOD) {
582 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Batch read failed:" << result;
583 emit readNodeAttributesFinished(results: QList<QOpcUaReadResult>(), serviceResult: static_cast<QOpcUa::UaStatusCode>(result));
584 return;
585 }
586
587 m_asyncBatchReadContext[requestId] = { .nodesToRead: nodesToRead };
588 triggerIterateClient();
589}
590
591void Open62541AsyncBackend::writeNodeAttributes(const QList<QOpcUaWriteItem> &nodesToWrite)
592{
593 if (!m_uaclient) {
594 emit writeNodeAttributesFinished(results: {}, serviceResult: QOpcUa::UaStatusCode::BadDisconnect);
595 return;
596 }
597
598 if (nodesToWrite.isEmpty()) {
599 emit writeNodeAttributesFinished(results: QList<QOpcUaWriteResult>(), serviceResult: QOpcUa::UaStatusCode::BadNothingToDo);
600 return;
601 }
602
603 UA_WriteRequest req;
604 UA_WriteRequest_init(p: &req);
605 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
606 UaDeleter<UA_WriteRequest> requestDeleter(&req, UA_WriteRequest_clear);
607
608 req.nodesToWriteSize = nodesToWrite.size();
609 req.nodesToWrite = static_cast<UA_WriteValue *>(UA_Array_new(size: nodesToWrite.size(), type: &UA_TYPES[UA_TYPES_WRITEVALUE]));
610
611 for (qsizetype i = 0; i < nodesToWrite.size(); ++i) {
612 const auto &currentItem = nodesToWrite.at(i);
613 auto &currentUaItem = req.nodesToWrite[i];
614 currentUaItem.attributeId = QOpen62541ValueConverter::toUaAttributeId(attr: currentItem.attribute());
615 currentUaItem.nodeId = Open62541Utils::nodeIdFromQString(name: currentItem.nodeId());
616 if (currentItem.hasStatusCode()) {
617 currentUaItem.value.status = currentItem.statusCode();
618 currentUaItem.value.hasStatus = UA_TRUE;
619 }
620 if (!currentItem.indexRange().isEmpty())
621 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: currentItem.indexRange(), ptr: &currentUaItem.indexRange);
622 if (!currentItem.value().isNull()) {
623 currentUaItem.value.hasValue = true;
624 currentUaItem.value.value = QOpen62541ValueConverter::toOpen62541Variant(currentItem.value(), currentItem.type());
625 }
626 if (currentItem.sourceTimestamp().isValid()) {
627 QOpen62541ValueConverter::scalarFromQt<UA_DateTime, QDateTime>(var: currentItem.sourceTimestamp(),
628 ptr: &currentUaItem.value.sourceTimestamp);
629 currentUaItem.value.hasSourceTimestamp = UA_TRUE;
630 }
631 if (currentItem.serverTimestamp().isValid()) {
632 QOpen62541ValueConverter::scalarFromQt<UA_DateTime, QDateTime>(var: currentItem.serverTimestamp(),
633 ptr: &currentUaItem.value.serverTimestamp);
634 currentUaItem.value.hasServerTimestamp = UA_TRUE;
635 }
636 }
637
638 quint32 requestId = 0;
639 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_WRITEREQUEST], callback: &asyncBatchWriteCallback,
640 responseType: &UA_TYPES[UA_TYPES_WRITERESPONSE], userdata: this, requestId: &requestId);
641
642 if (result != UA_STATUSCODE_GOOD) {
643 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Batch write failed:" << result;
644 emit writeNodeAttributesFinished(results: QList<QOpcUaWriteResult>(), serviceResult: static_cast<QOpcUa::UaStatusCode>(result));
645 return;
646 }
647
648 m_asyncBatchWriteContext[requestId] = { .nodesToWrite: nodesToWrite };
649 triggerIterateClient();
650}
651
652void Open62541AsyncBackend::readHistoryRaw(QOpcUaHistoryReadRawRequest request, QList<QByteArray> continuationPoints, bool releaseContinuationPoints, quint64 handle)
653{
654 if (!m_uaclient) {
655 emit historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode::BadDisconnect, handle);
656 return;
657 }
658
659 if (!continuationPoints.empty() && continuationPoints.size() != request.nodesToRead().size()) {
660 emit historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode::BadInternalError, handle);
661 return;
662 }
663
664 UA_HistoryReadRequest uarequest;
665 UA_HistoryReadRequest_init(p: &uarequest);
666 uarequest.requestHeader.timeoutHint = m_asyncRequestTimeout;
667 uarequest.nodesToReadSize = request.nodesToRead().size();
668 uarequest.nodesToRead = static_cast<UA_HistoryReadValueId*>(UA_Array_new(size: uarequest.nodesToReadSize, type: &UA_TYPES[UA_TYPES_HISTORYREADVALUEID]));
669 for (size_t i = 0; i < uarequest.nodesToReadSize; ++i) {
670 uarequest.nodesToRead[i].nodeId = Open62541Utils::nodeIdFromQString(name: request.nodesToRead().at(i).nodeId());
671 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: request.nodesToRead().at(i).indexRange(), ptr: &uarequest.nodesToRead[i].indexRange);
672 uarequest.nodesToRead[i].dataEncoding = UA_QUALIFIEDNAME_ALLOC(nsIndex: 0, chars: "Default Binary");
673 if (!continuationPoints.isEmpty())
674 QOpen62541ValueConverter::scalarFromQt<UA_ByteString, QByteArray>(var: continuationPoints.at(i), ptr: &uarequest.nodesToRead[i].continuationPoint);
675 }
676 uarequest.timestampsToReturn = static_cast<UA_TimestampsToReturn>(request.timestampsToReturn());
677
678 if (releaseContinuationPoints)
679 uarequest.releaseContinuationPoints = releaseContinuationPoints;
680
681 uarequest.historyReadDetails.encoding = UA_EXTENSIONOBJECT_DECODED;
682 uarequest.historyReadDetails.content.decoded.type = &UA_TYPES[UA_TYPES_READRAWMODIFIEDDETAILS];
683 UA_ReadRawModifiedDetails *details = UA_ReadRawModifiedDetails_new();
684 uarequest.historyReadDetails.content.decoded.data = details;
685 QOpen62541ValueConverter::scalarFromQt<UA_DateTime, QDateTime>(var: request.startTimestamp(), ptr: &details->startTime);
686 QOpen62541ValueConverter::scalarFromQt<UA_DateTime, QDateTime>(var: request.endTimestamp(), ptr: &details->endTime);
687 details->isReadModified = UA_FALSE;
688 details->returnBounds = request.returnBounds();
689 details->numValuesPerNode = request.numValuesPerNode();
690
691 quint32 requestId = 0;
692 UA_StatusCode resultCode = __UA_Client_AsyncService(client: m_uaclient, request: &uarequest, requestType: &UA_TYPES[UA_TYPES_HISTORYREADREQUEST], callback: &asyncReadHistoryDataCallBack,
693 responseType: &UA_TYPES[UA_TYPES_HISTORYREADRESPONSE], userdata: this, requestId: &requestId);
694
695 UA_HistoryReadRequest_clear(p: &uarequest);
696
697 if (resultCode != UA_STATUSCODE_GOOD) {
698 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Read history data failed:" << resultCode;
699 emit historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode(resultCode), handle);
700 return;
701 }
702
703 m_asyncReadHistoryDataContext[requestId] = {.handle: handle, .historyReadRawRequest: request};
704 triggerIterateClient();
705}
706
707void Open62541AsyncBackend::readHistoryEvents(const QOpcUaHistoryReadEventRequest &request, const QList<QByteArray> &continuationPoints,
708 bool releaseContinuationPoints, quint64 handle)
709{
710 if (!m_uaclient) {
711 emit historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode::BadDisconnect, handle);
712 return;
713 }
714
715 if (!continuationPoints.empty() && continuationPoints.size() != request.nodesToRead().size()) {
716 emit historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode::BadInternalError, handle);
717 return;
718 }
719
720 UA_HistoryReadRequest uarequest;
721 UA_HistoryReadRequest_init(p: &uarequest);
722 uarequest.requestHeader.timeoutHint = m_asyncRequestTimeout;
723 uarequest.nodesToReadSize = request.nodesToRead().size();
724 uarequest.nodesToRead = static_cast<UA_HistoryReadValueId*>(UA_Array_new(size: uarequest.nodesToReadSize, type: &UA_TYPES[UA_TYPES_HISTORYREADVALUEID]));
725
726 for (size_t i = 0; i < uarequest.nodesToReadSize; ++i) {
727 uarequest.nodesToRead[i].nodeId = Open62541Utils::nodeIdFromQString(name: request.nodesToRead().at(i).nodeId());
728 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: request.nodesToRead().at(i).indexRange(), ptr: &uarequest.nodesToRead[i].indexRange);
729 uarequest.nodesToRead[i].dataEncoding = UA_QUALIFIEDNAME_ALLOC(nsIndex: 0, chars: "Default Binary");
730 if (!continuationPoints.isEmpty())
731 QOpen62541ValueConverter::scalarFromQt<UA_ByteString, QByteArray>(var: continuationPoints.at(i), ptr: &uarequest.nodesToRead[i].continuationPoint);
732 }
733
734 uarequest.timestampsToReturn = UA_TIMESTAMPSTORETURN_BOTH;
735
736 if (releaseContinuationPoints)
737 uarequest.releaseContinuationPoints = releaseContinuationPoints;
738
739 uarequest.historyReadDetails.encoding = UA_EXTENSIONOBJECT_DECODED;
740 uarequest.historyReadDetails.content.decoded.type = &UA_TYPES[UA_TYPES_READEVENTDETAILS];
741 UA_ReadEventDetails *details = UA_ReadEventDetails_new();
742 uarequest.historyReadDetails.content.decoded.data = details;
743 QOpen62541ValueConverter::scalarFromQt<UA_DateTime, QDateTime>(var: request.startTimestamp(), ptr: &details->startTime);
744 QOpen62541ValueConverter::scalarFromQt<UA_DateTime, QDateTime>(var: request.endTimestamp(), ptr: &details->endTime);
745 details->numValuesPerNode = request.numValuesPerNode();
746
747 QOpen62541ValueConverter::scalarFromQt<UA_EventFilter, QOpcUaMonitoringParameters::EventFilter>(var: request.filter(), ptr: &details->filter);
748
749 quint32 requestId = 0;
750 UA_StatusCode resultCode = __UA_Client_AsyncService(client: m_uaclient, request: &uarequest, requestType: &UA_TYPES[UA_TYPES_HISTORYREADREQUEST], callback: &asyncReadHistoryEventsCallback,
751 responseType: &UA_TYPES[UA_TYPES_HISTORYREADRESPONSE], userdata: this, requestId: &requestId);
752
753 UA_HistoryReadRequest_clear(p: &uarequest);
754
755 if (resultCode != UA_STATUSCODE_GOOD) {
756 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Read history data failed:" << resultCode;
757 emit historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode(resultCode), handle);
758 return;
759 }
760
761 m_asyncReadHistoryEventsContext[requestId] = {.handle: handle, .historyReadEventRequest: request};
762 triggerIterateClient();
763}
764
765void Open62541AsyncBackend::addNode(const QOpcUaAddNodeItem &nodeToAdd)
766{
767 if (!m_uaclient) {
768 emit addNodeFinished(requestedNodeId: nodeToAdd.requestedNewNodeId(), assignedNodeId: {}, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
769 return;
770 }
771
772 UA_AddNodesRequest req;
773 UA_AddNodesRequest_init(p: &req);
774 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
775 UaDeleter<UA_AddNodesRequest> requestDeleter(&req, UA_AddNodesRequest_clear);
776 req.nodesToAddSize = 1;
777 req.nodesToAdd = UA_AddNodesItem_new();
778 UA_AddNodesItem_init(p: req.nodesToAdd);
779
780 QOpen62541ValueConverter::scalarFromQt<UA_ExpandedNodeId, QOpcUaExpandedNodeId>(
781 var: nodeToAdd.parentNodeId(), ptr: &req.nodesToAdd->parentNodeId);
782
783 req.nodesToAdd->referenceTypeId = Open62541Utils::nodeIdFromQString(name: nodeToAdd.referenceTypeId());
784
785 QOpen62541ValueConverter::scalarFromQt<UA_ExpandedNodeId, QOpcUaExpandedNodeId>(
786 var: nodeToAdd.requestedNewNodeId(), ptr: &req.nodesToAdd->requestedNewNodeId);
787
788 QOpen62541ValueConverter::scalarFromQt<UA_QualifiedName, QOpcUaQualifiedName>(
789 var: nodeToAdd.browseName(), ptr: &req.nodesToAdd->browseName);
790
791 req.nodesToAdd->nodeClass = static_cast<UA_NodeClass>(nodeToAdd.nodeClass());
792
793 req.nodesToAdd->nodeAttributes = assembleNodeAttributes(nodeAttributes: nodeToAdd.nodeAttributes(),
794 nodeClass: nodeToAdd.nodeClass());
795
796 if (!nodeToAdd.typeDefinition().nodeId().isEmpty())
797 QOpen62541ValueConverter::scalarFromQt<UA_ExpandedNodeId, QOpcUaExpandedNodeId>(
798 var: nodeToAdd.typeDefinition(), ptr: &req.nodesToAdd->typeDefinition);
799
800 quint32 requestId = 0;
801 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_ADDNODESREQUEST],
802 callback: &asyncAddNodeCallback,
803 responseType: &UA_TYPES[UA_TYPES_ADDNODESRESPONSE],
804 userdata: this, requestId: &requestId);
805
806 if (result != UA_STATUSCODE_GOOD) {
807 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add node:" << result;
808 emit addNodeFinished(requestedNodeId: nodeToAdd.requestedNewNodeId(), assignedNodeId: QString(), statusCode: static_cast<QOpcUa::UaStatusCode>(result));
809 return;
810 }
811
812 m_asyncAddNodeContext[requestId] = { .requestedNodeId: nodeToAdd.requestedNewNodeId() };
813 triggerIterateClient();
814}
815
816void Open62541AsyncBackend::deleteNode(const QString &nodeId, bool deleteTargetReferences)
817{
818 if (!m_uaclient) {
819 emit deleteNodeFinished(nodeId, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
820 return;
821 }
822
823 UA_DeleteNodesRequest request;
824 UA_DeleteNodesRequest_init(p: &request);
825 request.requestHeader.timeoutHint = m_asyncRequestTimeout;
826 UaDeleter<UA_DeleteNodesRequest> requestDeleter(&request, UA_DeleteNodesRequest_clear);
827
828 request.nodesToDeleteSize = 1;
829 request.nodesToDelete = UA_DeleteNodesItem_new();
830
831 request.nodesToDelete->nodeId = Open62541Utils::nodeIdFromQString(name: nodeId);
832 request.nodesToDelete->deleteTargetReferences = deleteTargetReferences;
833
834 quint32 requestId = 0;
835 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &request, requestType: &UA_TYPES[UA_TYPES_DELETENODESREQUEST],
836 callback: &asyncDeleteNodeCallback,
837 responseType: &UA_TYPES[UA_TYPES_DELETENODESRESPONSE],
838 userdata: this, requestId: &requestId);
839
840 QOpcUa::UaStatusCode resultStatus = static_cast<QOpcUa::UaStatusCode>(result);
841
842 if (result != QOpcUa::UaStatusCode::Good) {
843 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to delete node" << nodeId << "with status code" << resultStatus;
844 emit deleteNodeFinished(nodeId, statusCode: resultStatus);
845 return;
846 }
847
848 m_asyncDeleteNodeContext[requestId] = { .nodeId: nodeId };
849 triggerIterateClient();
850}
851
852void Open62541AsyncBackend::addReference(const QOpcUaAddReferenceItem &referenceToAdd)
853{
854 if (!m_uaclient) {
855 emit addReferenceFinished(sourceNodeId: referenceToAdd.sourceNodeId(), referenceTypeId: referenceToAdd.referenceTypeId(),
856 targetNodeId: referenceToAdd.targetNodeId(), isForwardReference: referenceToAdd.isForwardReference(),
857 statusCode: QOpcUa::UaStatusCode::BadDisconnect);
858 return;
859 }
860
861 UA_AddReferencesRequest request;
862 UA_AddReferencesRequest_init(p: &request);
863 request.requestHeader.timeoutHint = m_asyncRequestTimeout;
864 UaDeleter<UA_AddReferencesRequest> requestDeleter(&request, UA_AddReferencesRequest_clear);
865
866 request.referencesToAddSize = 1;
867 request.referencesToAdd = UA_AddReferencesItem_new();
868
869 request.referencesToAdd->isForward = referenceToAdd.isForwardReference();
870 QOpen62541ValueConverter::scalarFromQt<UA_NodeId, QString>(var: referenceToAdd.sourceNodeId(),
871 ptr: &request.referencesToAdd->sourceNodeId);
872 QOpen62541ValueConverter::scalarFromQt<UA_ExpandedNodeId, QOpcUaExpandedNodeId>(var: referenceToAdd.targetNodeId(),
873 ptr: &request.referencesToAdd->targetNodeId);
874 QOpen62541ValueConverter::scalarFromQt<UA_NodeId, QString>(var: referenceToAdd.referenceTypeId(),
875 ptr: &request.referencesToAdd->referenceTypeId);
876 request.referencesToAdd->targetNodeClass = static_cast<UA_NodeClass>(referenceToAdd.targetNodeClass());
877 QOpen62541ValueConverter::scalarFromQt<UA_String, QString>(var: referenceToAdd.targetServerUri(),
878 ptr: &request.referencesToAdd->targetServerUri);
879
880 quint32 requestId = 0;
881 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &request, requestType: &UA_TYPES[UA_TYPES_ADDREFERENCESREQUEST],
882 callback: &asyncAddReferenceCallback,
883 responseType: &UA_TYPES[UA_TYPES_ADDREFERENCESRESPONSE],
884 userdata: this, requestId: &requestId);
885
886 QOpcUa::UaStatusCode statusCode = static_cast<QOpcUa::UaStatusCode>(result);
887 if (result != UA_STATUSCODE_GOOD) {
888 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add reference from" << referenceToAdd.sourceNodeId() << "to"
889 << referenceToAdd.targetNodeId().nodeId() << ":" << statusCode;
890 emit addReferenceFinished(sourceNodeId: referenceToAdd.sourceNodeId(), referenceTypeId: referenceToAdd.referenceTypeId(),
891 targetNodeId: referenceToAdd.targetNodeId(), isForwardReference: referenceToAdd.isForwardReference(), statusCode);
892 return;
893 }
894
895 m_asyncAddReferenceContext[requestId] = { .sourceNodeId: referenceToAdd.sourceNodeId(), .referenceTypeId: referenceToAdd.referenceTypeId(),
896 .targetNodeId: referenceToAdd.targetNodeId(), .isForwardReference: referenceToAdd.isForwardReference() };
897 triggerIterateClient();
898}
899
900void Open62541AsyncBackend::deleteReference(const QOpcUaDeleteReferenceItem &referenceToDelete)
901{
902 if (!m_uaclient) {
903 emit deleteReferenceFinished(sourceNodeId: referenceToDelete.sourceNodeId(), referenceTypeId: referenceToDelete.referenceTypeId(),
904 targetNodeId: referenceToDelete.targetNodeId(), isForwardReference: referenceToDelete.isForwardReference(),
905 statusCode: QOpcUa::UaStatusCode::BadDisconnect);
906 return;
907 }
908
909 UA_DeleteReferencesRequest request;
910 UA_DeleteReferencesRequest_init(p: &request);
911 request.requestHeader.timeoutHint = m_asyncRequestTimeout;
912 UaDeleter<UA_DeleteReferencesRequest> requestDeleter(&request, UA_DeleteReferencesRequest_clear);
913
914 request.referencesToDeleteSize = 1;
915 request.referencesToDelete = UA_DeleteReferencesItem_new();
916 request.referencesToDelete->isForward = referenceToDelete.isForwardReference();
917 QOpen62541ValueConverter::scalarFromQt<UA_NodeId, QString>(var: referenceToDelete.sourceNodeId(),
918 ptr: &request.referencesToDelete->sourceNodeId);
919 QOpen62541ValueConverter::scalarFromQt<UA_ExpandedNodeId, QOpcUaExpandedNodeId>(var: referenceToDelete.targetNodeId(),
920 ptr: &request.referencesToDelete->targetNodeId);
921 QOpen62541ValueConverter::scalarFromQt<UA_NodeId, QString>(var: referenceToDelete.referenceTypeId(),
922 ptr: &request.referencesToDelete->referenceTypeId);
923 request.referencesToDelete->deleteBidirectional = referenceToDelete.deleteBidirectional();
924
925 quint32 requestId = 0;
926 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &request, requestType: &UA_TYPES[UA_TYPES_DELETEREFERENCESREQUEST],
927 callback: &asyncDeleteReferenceCallback,
928 responseType: &UA_TYPES[UA_TYPES_DELETEREFERENCESRESPONSE],
929 userdata: this, requestId: &requestId);
930
931 QOpcUa::UaStatusCode statusCode = static_cast<QOpcUa::UaStatusCode>(result);
932 if (result != UA_STATUSCODE_GOOD) {
933 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to delete reference from" << referenceToDelete.sourceNodeId() << "to"
934 << referenceToDelete.targetNodeId().nodeId() << ":" << statusCode;
935
936 emit deleteReferenceFinished(sourceNodeId: referenceToDelete.sourceNodeId(), referenceTypeId: referenceToDelete.referenceTypeId(),
937 targetNodeId: referenceToDelete.targetNodeId(),
938 isForwardReference: referenceToDelete.isForwardReference(), statusCode);
939 return;
940 }
941
942 m_asyncDeleteReferenceContext[requestId] = { .sourceNodeId: referenceToDelete.sourceNodeId(), .referenceTypeId: referenceToDelete.referenceTypeId(),
943 .targetNodeId: referenceToDelete.targetNodeId(), .isForwardReference: referenceToDelete.isForwardReference()};
944 triggerIterateClient();
945}
946
947static void convertBrowseResult(UA_BrowseResult *src, size_t referencesSize, QList<QOpcUaReferenceDescription> &dst)
948{
949 if (!src)
950 return;
951
952 for (size_t i = 0; i < referencesSize; ++i) {
953 QOpcUaReferenceDescription temp;
954 temp.setTargetNodeId(QOpen62541ValueConverter::scalarToQt<QOpcUaExpandedNodeId>(data: &src->references[i].nodeId));
955 temp.setTypeDefinition(QOpen62541ValueConverter::scalarToQt<QOpcUaExpandedNodeId>(data: &src->references[i].typeDefinition));
956 temp.setRefTypeId(Open62541Utils::nodeIdToQString(id: src->references[i].referenceTypeId));
957 temp.setNodeClass(static_cast<QOpcUa::NodeClass>(src->references[i].nodeClass));
958 temp.setBrowseName(QOpen62541ValueConverter::scalarToQt<QOpcUaQualifiedName, UA_QualifiedName>(data: &src->references[i].browseName));
959 temp.setDisplayName(QOpen62541ValueConverter::scalarToQt<QOpcUaLocalizedText, UA_LocalizedText>(data: &src->references[i].displayName));
960 temp.setIsForwardReference(src->references[i].isForward);
961 dst.push_back(t: temp);
962 }
963}
964
965void Open62541AsyncBackend::browse(quint64 handle, UA_NodeId id, const QOpcUaBrowseRequest &request)
966{
967 if (!m_uaclient) {
968 emit browseFinished(handle, children: {}, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
969 return;
970 }
971
972 UA_BrowseRequest uaRequest;
973 UA_BrowseRequest_init(p: &uaRequest);
974 uaRequest.requestHeader.timeoutHint = m_asyncRequestTimeout;
975 UaDeleter<UA_BrowseRequest> requestDeleter(&uaRequest, UA_BrowseRequest_clear);
976
977 uaRequest.nodesToBrowse = UA_BrowseDescription_new();
978 uaRequest.nodesToBrowseSize = 1;
979 uaRequest.nodesToBrowse->browseDirection = static_cast<UA_BrowseDirection>(request.browseDirection());
980 uaRequest.nodesToBrowse->includeSubtypes = request.includeSubtypes();
981 uaRequest.nodesToBrowse->nodeClassMask = static_cast<quint32>(request.nodeClassMask());
982 uaRequest.nodesToBrowse->nodeId = id;
983 uaRequest.nodesToBrowse->resultMask = UA_BROWSERESULTMASK_ALL;
984 uaRequest.nodesToBrowse->referenceTypeId = Open62541Utils::nodeIdFromQString(name: request.referenceTypeId());
985 uaRequest.requestedMaxReferencesPerNode = 0; // Let the server choose a maximum value
986
987 quint32 requestId = 0;
988 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &uaRequest, requestType: &UA_TYPES[UA_TYPES_BROWSEREQUEST], callback: &asyncBrowseCallback,
989 responseType: &UA_TYPES[UA_TYPES_BROWSERESPONSE], userdata: this, requestId: &requestId);
990
991 if (result != UA_STATUSCODE_GOOD) {
992 emit browseFinished(handle, children: QList<QOpcUaReferenceDescription>(), statusCode: static_cast<QOpcUa::UaStatusCode>(result));
993 return;
994 }
995
996 m_asyncBrowseContext[requestId] = { .handle: handle, .isBrowseNext: false, .results: QList<QOpcUaReferenceDescription>() };
997 triggerIterateClient();
998}
999
1000void Open62541AsyncBackend::clientStateCallback(UA_Client *client,
1001 UA_SecureChannelState channelState,
1002 UA_SessionState sessionState,
1003 UA_StatusCode connectStatus)
1004{
1005 Q_UNUSED(channelState)
1006 Q_UNUSED(sessionState)
1007 Q_UNUSED(connectStatus)
1008
1009 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(UA_Client_getContext(client));
1010 if (!backend)
1011 return;
1012
1013 // The connection is gone, no need to keep iterating
1014 backend->m_clientIterateTimer.stop();
1015 backend->m_clientIterateOnDemandTimer.stop();
1016
1017 // UA_Client_disconnect() must be called from outside this callback
1018 QMetaObject::invokeMethod(object: backend, function: [backend]() {
1019 backend->disconnectInternal(error: QOpcUaClient::ConnectionError);
1020 }, type: Qt::QueuedConnection);
1021}
1022
1023void Open62541AsyncBackend::inactivityCallback(UA_Client *client)
1024{
1025 // The client state callback is not called if the background check encounters a timeout.
1026 // This call triggers the disconnect and the appropriate state change
1027 clientStateCallback(client, channelState: UA_SECURECHANNELSTATE_CLOSED, sessionState: UA_SESSIONSTATE_CLOSED, UA_STATUSCODE_BADTIMEOUT);
1028}
1029
1030void Open62541AsyncBackend::connectToEndpoint(const QOpcUaEndpointDescription &endpoint)
1031{
1032 disconnectInternal();
1033
1034 emit stateAndOrErrorChanged(state: QOpcUaClient::Connecting, error: QOpcUaClient::NoError);
1035
1036 QString errorMessage;
1037 const auto endpointError = verifyEndpointDescription(endpoint, message: &errorMessage);
1038 if (endpointError != QOpcUaClient::ClientError::NoError) {
1039 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << errorMessage;
1040 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: endpointError);
1041 return;
1042 }
1043
1044#ifdef UA_ENABLE_ENCRYPTION
1045 const auto pkiConfig = m_clientImpl->m_client->pkiConfiguration();
1046#endif
1047
1048 if (!m_clientImpl->supportedSecurityPolicies().contains(str: endpoint.securityPolicy())
1049#ifdef UA_ENABLE_ENCRYPTION
1050 || (!pkiConfig.isKeyAndCertificateFileSet() && endpoint.securityPolicy() != QOpcUa::NonePolicy)
1051#endif
1052 ) {
1053#ifndef UA_ENABLE_ENCRYPTION
1054 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "The open62541 plugin has been built without encryption support, only the"
1055 << QOpcUa::NonePolicy << "security policy is supported";
1056#endif
1057 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unsupported security policy:" << endpoint.securityPolicy();
1058
1059#ifdef UA_ENABLE_ENCRYPTION
1060 if (!pkiConfig.isKeyAndCertificateFileSet()) {
1061 qCWarning(QT_OPCUA_PLUGINS_OPEN62541)
1062 << "No certificate and private key is set, only the" << QOpcUa::NonePolicy << "security policy is supported";
1063 }
1064#endif
1065 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::ClientError::UnsupportedSecurityPolicy);
1066 return;
1067 }
1068
1069 const auto identity = m_clientImpl->m_client->applicationIdentity();
1070 const auto authInfo = m_clientImpl->m_client->authenticationInformation();
1071
1072 if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Certificate) {
1073#ifndef UA_ENABLE_ENCRYPTION
1074 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "The open62541 plugin has been built without encryption support, "
1075 "certificate auth is not supported";
1076 emit stateAndOrErrorChanged(QOpcUaClient::Disconnected,
1077 QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
1078 return;
1079#else
1080 if (!authInfo.authenticationData().isValid() && !pkiConfig.isKeyAndCertificateFileSet()) {
1081 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to do certificate auth when no certificate is set";
1082 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected,
1083 error: QOpcUaClient::ClientError::InvalidAuthenticationInformation);
1084 return;
1085 }
1086#endif
1087 }
1088
1089 // Possible early out if no token matching the configuration and the client's capabilities is available
1090 // If the token's security policy URI is not None, at least a certificate and a private key must be set
1091 bool suitableTokenFound = false;
1092 const auto userIdentityTokens = endpoint.userIdentityTokens();
1093 for (const auto &token : userIdentityTokens) {
1094 // An empty security policy URI means that the endpoint's policy is to be used
1095 const auto tokenPolicy = token.securityPolicy().isEmpty() ? endpoint.securityPolicy() : token.securityPolicy();
1096
1097 if (token.tokenType() == authInfo.authenticationType() &&
1098 m_clientImpl->supportedSecurityPolicies().contains(str: tokenPolicy) &&
1099#ifdef UA_ENABLE_ENCRYPTION
1100 (tokenPolicy == QOpcUa::NonePolicy || pkiConfig.isKeyAndCertificateFileSet())
1101#else
1102 tokenPolicy == QOpcUa::NonePolicy
1103#endif
1104 ) {
1105 suitableTokenFound = true;
1106 break;
1107 }
1108 }
1109
1110 if (!suitableTokenFound) {
1111 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No suitable identity token found in the endpoint";
1112 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Availabe tokens in the endpoint:";
1113 for (const auto &token : userIdentityTokens) {
1114 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << " Type:" << token.tokenType()
1115 << "SecurityPolicyUri:" << token.securityPolicy();
1116 }
1117
1118 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected,
1119 error: QOpcUaClient::ClientError::NoMatchingUserIdentityTokenFound);
1120 return;
1121 }
1122
1123 UA_ClientConfig initialConfig {};
1124 initialConfig.logging = &m_open62541Logger;
1125 m_uaclient = UA_Client_newWithConfig(config: &initialConfig);
1126
1127 auto conf = UA_Client_getConfig(client: m_uaclient);
1128 m_currentConnectionSettings = m_clientImpl->m_client->connectionSettings();
1129
1130#ifdef UA_ENABLE_ENCRYPTION
1131 if (!setupClientConfigSecurity(authInfo, pkiConfig, endpoint))
1132 return;
1133#else
1134 const auto setDefaultResult = UA_ClientConfig_setDefault(conf);
1135 if (setDefaultResult != UA_STATUSCODE_GOOD) {
1136 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set client config defaults:" << static_cast<QOpcUa::UaStatusCode>(setDefaultResult);
1137 emit stateAndOrErrorChanged(QOpcUaClient::Disconnected, QOpcUaClient::UnknownError);
1138 UA_Client_delete(m_uaclient);
1139 m_uaclient = nullptr;
1140 return;
1141 }
1142#endif
1143
1144 using Timeout_t = decltype(conf->timeout);
1145 conf->timeout = qt_saturate<Timeout_t>(x: m_currentConnectionSettings.connectTimeout().count());
1146 conf->secureChannelLifeTime = qt_saturate<Timeout_t>(x: m_currentConnectionSettings.secureChannelLifeTime().count());
1147 conf->requestedSessionTimeout = qt_saturate<Timeout_t>(x: m_currentConnectionSettings.sessionTimeout().count());
1148
1149 const auto sessionLocaleIds = m_currentConnectionSettings.sessionLocaleIds();
1150 if (!sessionLocaleIds.isEmpty()) {
1151 conf->sessionLocaleIds = static_cast<UA_String *>(UA_Array_new(size: sessionLocaleIds.size(), type: &UA_TYPES[UA_TYPES_STRING]));
1152 for (qsizetype i = 0; i < sessionLocaleIds.size(); ++i)
1153 conf->sessionLocaleIds[i] = UA_STRING_ALLOC(sessionLocaleIds[i].toUtf8().constData());
1154 conf->sessionLocaleIdsSize = sessionLocaleIds.size();
1155 }
1156
1157 UA_LocalizedText_clear(p: &conf->clientDescription.applicationName);
1158 UA_String_clear(p: &conf->clientDescription.applicationUri);
1159 UA_String_clear(p: &conf->clientDescription.productUri);
1160
1161 conf->clientContext = this;
1162
1163 // Send periodic read requests as keepalive
1164 conf->connectivityCheckInterval = 60000;
1165 conf->inactivityCallback = inactivityCallback;
1166
1167 conf->clientDescription.applicationName = UA_LOCALIZEDTEXT_ALLOC(locale: "", text: identity.applicationName().toUtf8().constData());
1168 conf->clientDescription.applicationUri = UA_STRING_ALLOC(identity.applicationUri().toUtf8().constData());
1169 conf->clientDescription.productUri = UA_STRING_ALLOC(identity.productUri().toUtf8().constData());
1170 conf->clientDescription.applicationType = UA_APPLICATIONTYPE_CLIENT;
1171
1172 conf->securityPolicyUri = UA_STRING_ALLOC(endpoint.securityPolicy().toUtf8().constData());
1173 conf->securityMode = static_cast<UA_MessageSecurityMode>(endpoint.securityMode());
1174
1175 establishConnectionInternal(authInfo,
1176#ifdef UA_ENABLE_ENCRYPTION
1177 pkiConfig,
1178#endif
1179 endpoint);
1180}
1181
1182void Open62541AsyncBackend::disconnectFromEndpoint()
1183{
1184 disconnectInternal();
1185}
1186
1187void Open62541AsyncBackend::requestEndpoints(const QUrl &url)
1188{
1189 UA_ClientConfig initialConfig {};
1190 initialConfig.logging = &m_open62541Logger;
1191 UA_ClientConfig_setDefault(config: &initialConfig);
1192
1193 UA_Client *tmpClient = UA_Client_newWithConfig(config: &initialConfig);
1194
1195 size_t numEndpoints = 0;
1196 UA_EndpointDescription *endpoints = nullptr;
1197 UA_StatusCode res = UA_Client_getEndpoints(client: tmpClient, serverUrl: url.toString(options: QUrl::RemoveUserInfo).toUtf8().constData(), endpointDescriptionsSize: &numEndpoints, endpointDescriptions: &endpoints);
1198 UaArrayDeleter<UA_TYPES_ENDPOINTDESCRIPTION> endpointDescriptionDeleter(endpoints, numEndpoints);
1199 QList<QOpcUaEndpointDescription> ret;
1200
1201 namespace vc = QOpen62541ValueConverter;
1202 using namespace QOpcUa;
1203 if (res == UA_STATUSCODE_GOOD && numEndpoints) {
1204 for (size_t i = 0; i < numEndpoints ; ++i) {
1205 QOpcUaEndpointDescription epd;
1206 QOpcUaApplicationDescription &apd = epd.serverRef();
1207
1208 apd.setApplicationUri(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].server.applicationUri));
1209 apd.setProductUri(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].server.productUri));
1210 apd.setApplicationName(vc::scalarToQt<QOpcUaLocalizedText, UA_LocalizedText>(data: &endpoints[i].server.applicationName));
1211 apd.setApplicationType(static_cast<QOpcUaApplicationDescription::ApplicationType>(endpoints[i].server.applicationType));
1212 apd.setGatewayServerUri(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].server.gatewayServerUri));
1213 apd.setDiscoveryProfileUri(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].server.discoveryProfileUri));
1214 for (size_t j = 0; j < endpoints[i].server.discoveryUrlsSize; ++j)
1215 apd.discoveryUrlsRef().append(t: vc::scalarToQt<QString, UA_String>(data: &endpoints[i].server.discoveryUrls[j]));
1216
1217 epd.setEndpointUrl(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].endpointUrl));
1218 epd.setServerCertificate(vc::scalarToQt<QByteArray, UA_ByteString>(data: &endpoints[i].serverCertificate));
1219 epd.setSecurityMode(static_cast<QOpcUaEndpointDescription::MessageSecurityMode>(endpoints[i].securityMode));
1220 epd.setSecurityPolicy(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].securityPolicyUri));
1221 for (size_t j = 0; j < endpoints[i].userIdentityTokensSize; ++j) {
1222 QOpcUaUserTokenPolicy policy;
1223 UA_UserTokenPolicy *policySrc = &endpoints[i].userIdentityTokens[j];
1224 policy.setPolicyId(vc::scalarToQt<QString, UA_String>(data: &policySrc->policyId));
1225 policy.setTokenType(static_cast<QOpcUaUserTokenPolicy::TokenType>(endpoints[i].userIdentityTokens[j].tokenType));
1226 policy.setIssuedTokenType(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].userIdentityTokens[j].issuedTokenType));
1227 policy.setIssuerEndpointUrl(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].userIdentityTokens[j].issuerEndpointUrl));
1228 policy.setSecurityPolicy(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].userIdentityTokens[j].securityPolicyUri));
1229 epd.userIdentityTokensRef().append(t: policy);
1230 }
1231
1232 epd.setTransportProfileUri(vc::scalarToQt<QString, UA_String>(data: &endpoints[i].transportProfileUri));
1233 epd.setSecurityLevel(endpoints[i].securityLevel);
1234 ret.append(t: epd);
1235 }
1236 } else {
1237 if (res == UA_STATUSCODE_GOOD)
1238 qWarning() << "Server returned an empty endpoint list";
1239 else
1240 qWarning() << "Failed to retrieve endpoints from " << url.toString(options: QUrl::RemoveUserInfo).toUtf8().constData()
1241 << "with status" << UA_StatusCode_name(code: res);
1242 }
1243
1244 emit endpointsRequestFinished(endpoints: ret, statusCode: static_cast<QOpcUa::UaStatusCode>(res), requestUrl: url);
1245
1246 UA_Client_delete(client: tmpClient);
1247}
1248
1249void Open62541AsyncBackend::handleConnectionSettingsChanged(const QOpcUaConnectionSettings &settings)
1250{
1251 // If there is no client, the settings will be applied during the next connectToEndpoint() call
1252 if (m_uaclient) {
1253 if (m_currentConnectionSettings.requestTimeout() != settings.requestTimeout()) {
1254 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Changing the request timeout for an established connection is not supported."
1255 << "The change will be active after the next connectToEndpoint() call.";
1256 }
1257
1258 if (m_currentConnectionSettings.secureChannelLifeTime() != settings.secureChannelLifeTime()) {
1259 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Changing the secure channel lifetime for an established connection is not supported."
1260 << "The change will be active after the next connectToEndpoint() call.";
1261 }
1262
1263 if (m_currentConnectionSettings.sessionTimeout() != settings.sessionTimeout()) {
1264 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Changing the session timeout for an established connection is not supported."
1265 << "The change will be active after the next connectToEndpoint() call.";
1266 }
1267
1268 if (m_currentConnectionSettings.connectTimeout() != settings.connectTimeout()) {
1269 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Changing the connect timeout for an established connection is not supported."
1270 << "The change will be active after the next connectToEndpoint() call.";
1271 }
1272
1273 if (m_currentConnectionSettings.sessionLocaleIds() != settings.sessionLocaleIds()) {
1274 const auto conf = UA_Client_getConfig(client: m_uaclient);
1275
1276 if (conf->sessionLocaleIdsSize) {
1277 UA_Array_delete(p: conf->sessionLocaleIds, size: conf->sessionLocaleIdsSize, type: &UA_TYPES[UA_TYPES_LOCALEID]);
1278 conf->sessionLocaleIdsSize = 0;
1279 }
1280
1281 if (!settings.sessionLocaleIds().isEmpty()) {
1282 const auto sessionLocaleIds = settings.sessionLocaleIds();
1283 conf->sessionLocaleIds = static_cast<UA_String *>(UA_Array_new(size: sessionLocaleIds.size(), type: &UA_TYPES[UA_TYPES_STRING]));
1284 for (qsizetype i = 0; i < sessionLocaleIds.size(); ++i)
1285 conf->sessionLocaleIds[i] = UA_STRING_ALLOC(sessionLocaleIds[i].toUtf8().constData());
1286 conf->sessionLocaleIdsSize = sessionLocaleIds.size();
1287 }
1288
1289 const auto result = UA_Client_activateCurrentSession(client: m_uaclient);
1290 if (result != UA_STATUSCODE_GOOD) {
1291 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Changing the session locale Ids failed with" << UA_StatusCode_name(code: result);
1292 } else {
1293 qCInfo(QT_OPCUA_PLUGINS_OPEN62541) << "The session locale ids were updated to" << settings.sessionLocaleIds().join(sep: QChar::fromLatin1(c: ' '));
1294 }
1295 }
1296
1297 m_currentConnectionSettings = settings;
1298 }
1299}
1300
1301void Open62541AsyncBackend::iterateClient()
1302{
1303 if (!m_uaclient)
1304 return;
1305
1306 // If BADSERVERNOTCONNECTED is returned, the subscriptions are gone and local information can be deleted.
1307 if (UA_Client_run_iterate(client: m_uaclient,
1308 timeout: std::max<quint32>(a: 1, b: m_clientIterateInterval / 2)) == UA_STATUSCODE_BADSERVERNOTCONNECTED) {
1309 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to send publish request";
1310 cleanupSubscriptions();
1311 }
1312}
1313
1314void Open62541AsyncBackend::triggerIterateClient()
1315{
1316 // This is 5 seconds faster over all client tests compared to just
1317 // calling QTimer::singleShot() every time
1318 if (!m_clientIterateOnDemandTimer.isActive()) {
1319 // Restart the normal iterate timer, no need to have more invocations
1320 if (m_clientIterateTimer.isActive())
1321 m_clientIterateTimer.start(msec: m_clientIterateInterval);
1322 m_clientIterateOnDemandTimer.start(msec: 0);
1323 }
1324}
1325
1326void Open62541AsyncBackend::handleSubscriptionTimeout(QOpen62541Subscription *sub, QList<QPair<quint64, QOpcUa::NodeAttribute>> items)
1327{
1328 for (auto it : std::as_const(t&: items)) {
1329 auto item = m_attributeMapping.find(key: it.first);
1330 if (item == m_attributeMapping.end())
1331 continue;
1332 item->remove(key: it.second);
1333 }
1334 m_subscriptions.remove(key: sub->subscriptionId());
1335 delete sub;
1336}
1337
1338QOpen62541Subscription *Open62541AsyncBackend::getSubscriptionForItem(quint64 handle, QOpcUa::NodeAttribute attr)
1339{
1340 auto nodeEntry = m_attributeMapping.find(key: handle);
1341 if (nodeEntry == m_attributeMapping.end())
1342 return nullptr;
1343
1344 auto subscription = nodeEntry->find(key: attr);
1345 if (subscription == nodeEntry->end()) {
1346 return nullptr;
1347 }
1348
1349 return subscription.value();
1350}
1351
1352QOpcUaApplicationDescription Open62541AsyncBackend::convertApplicationDescription(UA_ApplicationDescription &desc)
1353{
1354 QOpcUaApplicationDescription temp;
1355
1356 temp.setApplicationUri(QOpen62541ValueConverter::scalarToQt<QString, UA_String>(data: &desc.applicationUri));
1357 temp.setProductUri(QOpen62541ValueConverter::scalarToQt<QString, UA_String>(data: &desc.productUri));
1358 temp.setApplicationName(QOpen62541ValueConverter::scalarToQt<QOpcUaLocalizedText, UA_LocalizedText>(data: &desc.applicationName));
1359 temp.setApplicationType(static_cast<QOpcUaApplicationDescription::ApplicationType>(desc.applicationType));
1360 temp.setGatewayServerUri(QOpen62541ValueConverter::scalarToQt<QString, UA_String>(data: &desc.gatewayServerUri));
1361 temp.setDiscoveryProfileUri(QOpen62541ValueConverter::scalarToQt<QString, UA_String>(data: &desc.discoveryProfileUri));
1362
1363
1364 for (size_t i = 0; i < desc.discoveryUrlsSize; ++i)
1365 temp.discoveryUrlsRef().append(t: QOpen62541ValueConverter::scalarToQt<QString, UA_String>(data: &desc.discoveryUrls[i]));
1366
1367 return temp;
1368}
1369
1370void Open62541AsyncBackend::cleanupSubscriptions()
1371{
1372 qDeleteAll(c: m_subscriptions);
1373 m_subscriptions.clear();
1374 m_attributeMapping.clear();
1375 m_minPublishingInterval = 0;
1376}
1377
1378void Open62541AsyncBackend::registerNodes(const QStringList &nodesToRegister)
1379{
1380 if (!m_uaclient) {
1381 emit registerNodesFinished(nodesToRegister, registeredNodeIds: {}, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
1382 return;
1383 }
1384
1385 UA_RegisterNodesRequest req;
1386 UA_RegisterNodesRequest_init(p: &req);
1387 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
1388
1389 req.nodesToRegisterSize = nodesToRegister.size();
1390 req.nodesToRegister = static_cast<UA_NodeId *>(UA_Array_new(size: nodesToRegister.size(), type: &UA_TYPES[UA_TYPES_NODEID]));
1391
1392 for (int i = 0; i < nodesToRegister.size(); ++i)
1393 QOpen62541ValueConverter::scalarFromQt<UA_NodeId, QString>(var: nodesToRegister.at(i), ptr: &req.nodesToRegister[i]);
1394
1395 quint32 requestId = 0;
1396 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_REGISTERNODESREQUEST],
1397 callback: &asyncRegisterNodesCallback,
1398 responseType: &UA_TYPES[UA_TYPES_REGISTERNODESRESPONSE],
1399 userdata: this, requestId: &requestId);
1400
1401 UA_RegisterNodesRequest_clear(p: &req);
1402
1403 if (result != UA_STATUSCODE_GOOD) {
1404 emit registerNodesFinished(nodesToRegister, registeredNodeIds: {}, statusCode: QOpcUa::UaStatusCode(result));
1405 } else {
1406 m_asyncRegisterUnregisterNodesContext[requestId] = { .nodeIds: nodesToRegister };
1407 triggerIterateClient();
1408 }
1409}
1410
1411void Open62541AsyncBackend::unregisterNodes(const QStringList &nodesToUnregister)
1412{
1413 if (!m_uaclient) {
1414 emit unregisterNodesFinished(nodesToUnregister, statusCode: QOpcUa::UaStatusCode::BadDisconnect);
1415 return;
1416 }
1417
1418 UA_UnregisterNodesRequest req;
1419 UA_UnregisterNodesRequest_init(p: &req);
1420 req.requestHeader.timeoutHint = m_asyncRequestTimeout;
1421
1422 req.nodesToUnregisterSize = nodesToUnregister.size();
1423 req.nodesToUnregister = static_cast<UA_NodeId *>(UA_Array_new(size: nodesToUnregister.size(), type: &UA_TYPES[UA_TYPES_NODEID]));
1424
1425 for (int i = 0; i < nodesToUnregister.size(); ++i)
1426 QOpen62541ValueConverter::scalarFromQt<UA_NodeId, QString>(var: nodesToUnregister.at(i), ptr: &req.nodesToUnregister[i]);
1427
1428 quint32 requestId = 0;
1429 UA_StatusCode result = __UA_Client_AsyncService(client: m_uaclient, request: &req, requestType: &UA_TYPES[UA_TYPES_UNREGISTERNODESREQUEST],
1430 callback: &asyncUnregisterNodesCallback,
1431 responseType: &UA_TYPES[UA_TYPES_UNREGISTERNODESRESPONSE],
1432 userdata: this, requestId: &requestId);
1433
1434 UA_UnregisterNodesRequest_clear(p: &req);
1435
1436 if (result != UA_STATUSCODE_GOOD) {
1437 emit unregisterNodesFinished(nodesToUnregister, statusCode: QOpcUa::UaStatusCode(result));
1438 } else {
1439 m_asyncRegisterUnregisterNodesContext[requestId] = { .nodeIds: nodesToUnregister };
1440 triggerIterateClient();
1441 }
1442}
1443
1444void Open62541AsyncBackend::asyncMethodCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1445{
1446 Q_UNUSED(client)
1447
1448 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1449 const auto context = backend->m_asyncCallContext.take(key: requestId);
1450
1451 QVariant result;
1452
1453 const auto cr = static_cast<UA_CallResponse *>(response);
1454
1455 if (cr->resultsSize && cr->results->outputArgumentsSize > 1 && cr->results->statusCode == UA_STATUSCODE_GOOD) {
1456 QVariantList temp;
1457 for (size_t i = 0; i < cr->results->outputArgumentsSize; ++i)
1458 temp.append(t: QOpen62541ValueConverter::toQVariant(cr->results->outputArguments[i]));
1459
1460 result = temp;
1461 } else if (cr->resultsSize && cr->results->outputArgumentsSize == 1 && cr->results->statusCode == UA_STATUSCODE_GOOD) {
1462 result = QOpen62541ValueConverter::toQVariant(cr->results->outputArguments[0]);
1463 }
1464
1465 emit backend->methodCallFinished(handle: context.handle, methodNodeId: context.methodNodeId, result,
1466 statusCode: static_cast<QOpcUa::UaStatusCode>(cr->resultsSize ?
1467 cr->results->statusCode :
1468 cr->responseHeader.serviceResult));
1469}
1470
1471void Open62541AsyncBackend::asyncTranslateBrowsePathCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1472{
1473 Q_UNUSED(client)
1474
1475 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1476 const auto context = backend->m_asyncTranslateContext.take(key: requestId);
1477
1478 const auto res = static_cast<UA_TranslateBrowsePathsToNodeIdsResponse *>(response);
1479
1480 if (res->responseHeader.serviceResult != UA_STATUSCODE_GOOD || res->resultsSize != 1) {
1481 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Translate browse path failed:" << UA_StatusCode_name(code: res->responseHeader.serviceResult);
1482 emit backend->resolveBrowsePathFinished(handle: context.handle, targets: QList<QOpcUaBrowsePathTarget>(), path: context.path,
1483 statusCode: static_cast<QOpcUa::UaStatusCode>(res->responseHeader.serviceResult));
1484 return;
1485 }
1486
1487 QList<QOpcUaBrowsePathTarget> ret;
1488 for (size_t i = 0; i < res->results->targetsSize ; ++i) {
1489 QOpcUaBrowsePathTarget temp;
1490 temp.setRemainingPathIndex(res->results->targets[i].remainingPathIndex);
1491 temp.targetIdRef().setNamespaceUri(QString::fromUtf8(utf8: reinterpret_cast<char *>(res->results->targets[i].targetId.namespaceUri.data)));
1492 temp.targetIdRef().setServerIndex(res->results->targets[i].targetId.serverIndex);
1493 temp.targetIdRef().setNodeId(Open62541Utils::nodeIdToQString(id: res->results->targets[i].targetId.nodeId));
1494 ret.append(t: temp);
1495 }
1496
1497 emit backend->resolveBrowsePathFinished(handle: context.handle, targets: ret, path: context.path, statusCode: static_cast<QOpcUa::UaStatusCode>(res->results->statusCode));
1498}
1499
1500void Open62541AsyncBackend::asyncAddNodeCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1501{
1502 Q_UNUSED(client)
1503
1504 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1505 const auto context = backend->m_asyncAddNodeContext.take(key: requestId);
1506
1507 const auto res = static_cast<UA_AddNodesResponse *>(response);
1508
1509 QOpcUa::UaStatusCode status = QOpcUa::UaStatusCode::Good;
1510 QString resultId;
1511 if (res->responseHeader.serviceResult == UA_STATUSCODE_GOOD) {
1512 if (res->results->statusCode == UA_STATUSCODE_GOOD)
1513 resultId = Open62541Utils::nodeIdToQString(id: res->results->addedNodeId);
1514 else {
1515 status = static_cast<QOpcUa::UaStatusCode>(res->results->statusCode);
1516 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add node:" << status;
1517 }
1518 } else {
1519 status = static_cast<QOpcUa::UaStatusCode>(res->responseHeader.serviceResult);
1520 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to add node:" << status;
1521 }
1522
1523 emit backend->addNodeFinished(requestedNodeId: context.requestedNodeId, assignedNodeId: resultId, statusCode: status);
1524}
1525
1526void Open62541AsyncBackend::asyncDeleteNodeCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1527{
1528 Q_UNUSED(client)
1529
1530 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1531 const auto context = backend->m_asyncDeleteNodeContext.take(key: requestId);
1532
1533 const auto res = static_cast<UA_DeleteNodesResponse *>(response);
1534
1535 emit backend->deleteNodeFinished(nodeId: context.nodeId,
1536 statusCode: static_cast<QOpcUa::UaStatusCode>(res->resultsSize ?
1537 res->results[0] : res->responseHeader.serviceResult));
1538}
1539
1540void Open62541AsyncBackend::asyncAddReferenceCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1541{
1542 Q_UNUSED(client)
1543
1544 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1545 const auto context = backend->m_asyncAddReferenceContext.take(key: requestId);
1546
1547 const auto res = static_cast<UA_AddReferencesResponse *>(response);
1548
1549 emit backend->addReferenceFinished(sourceNodeId: context.sourceNodeId, referenceTypeId: context.referenceTypeId, targetNodeId: context.targetNodeId,
1550 isForwardReference: context.isForwardReference,
1551 statusCode: static_cast<QOpcUa::UaStatusCode>(res->resultsSize ?
1552 res->results[0] : res->responseHeader.serviceResult));
1553}
1554
1555void Open62541AsyncBackend::asyncDeleteReferenceCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1556{
1557 Q_UNUSED(client)
1558
1559 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1560 const auto context = backend->m_asyncDeleteReferenceContext.take(key: requestId);
1561
1562 const auto res = static_cast<UA_DeleteReferencesResponse *>(response);
1563
1564 emit backend->deleteReferenceFinished(sourceNodeId: context.sourceNodeId, referenceTypeId: context.referenceTypeId,
1565 targetNodeId: context.targetNodeId, isForwardReference: context.isForwardReference,
1566 statusCode: static_cast<QOpcUa::UaStatusCode>(res->resultsSize ?
1567 res->results[0] :
1568 res->responseHeader.serviceResult));
1569}
1570
1571void Open62541AsyncBackend::asyncReadCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1572{
1573 Q_UNUSED(client)
1574
1575 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1576 auto context = backend->m_asyncReadContext.take(key: requestId);
1577
1578 const auto res = static_cast<UA_ReadResponse *>(response);
1579
1580 for (qsizetype i = 0; i < context.results.size(); ++i) {
1581 // Use the service result as status code if there is no specific result for the current value.
1582 // This ensures a result for each attribute when UA_Client_Service_read is called for a disconnected client.
1583 if (static_cast<size_t>(i) >= res->resultsSize) {
1584 context.results[i].setStatusCode(static_cast<QOpcUa::UaStatusCode>(res->responseHeader.serviceResult));
1585 continue;
1586 }
1587 if (res->results[i].hasStatus)
1588 context.results[i].setStatusCode(static_cast<QOpcUa::UaStatusCode>(res->results[i].status));
1589 else
1590 context.results[i].setStatusCode(QOpcUa::UaStatusCode::Good);
1591 if (res->results[i].hasValue && res->results[i].value.data)
1592 context.results[i].setValue(QOpen62541ValueConverter::toQVariant(res->results[i].value));
1593 if (res->results[i].hasSourceTimestamp)
1594 context.results[i].setSourceTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime, UA_DateTime>(data: &res->results[i].sourceTimestamp));
1595 if (res->results[i].hasServerTimestamp)
1596 context.results[i].setServerTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime, UA_DateTime>(data: &res->results[i].serverTimestamp));
1597 }
1598
1599 emit backend->attributesRead(handle: context.handle, attributes: context.results,
1600 serviceResult: static_cast<QOpcUa::UaStatusCode>(res->responseHeader.serviceResult));
1601}
1602
1603void Open62541AsyncBackend::asyncWriteAttributesCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1604{
1605 Q_UNUSED(client)
1606
1607 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1608 auto context = backend->m_asyncWriteAttributesContext.take(key: requestId);
1609
1610 const auto res = static_cast<UA_WriteResponse *>(response);
1611
1612 size_t index = 0;
1613 for (auto it = context.toWrite.begin(); it != context.toWrite.end(); ++it, ++index) {
1614 QOpcUa::UaStatusCode status = index < res->resultsSize ?
1615 static_cast<QOpcUa::UaStatusCode>(res->results[index]) :
1616 static_cast<QOpcUa::UaStatusCode>(res->responseHeader.serviceResult);
1617 emit backend->attributeWritten(handle: context.handle, attribute: it.key(), value: it.value(), statusCode: status);
1618 }
1619}
1620
1621void Open62541AsyncBackend::asyncBrowseCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1622{
1623 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1624 auto context = backend->m_asyncBrowseContext.take(key: requestId);
1625
1626 UA_StatusCode statusCode = UA_STATUSCODE_GOOD;
1627 size_t referencesSize = 0;
1628 UA_BrowseResult *references = nullptr;
1629 UA_ByteString *continuationPoint = nullptr;
1630
1631 if (context.isBrowseNext) {
1632 const auto res = static_cast<UA_BrowseNextResponse *>(response);
1633 referencesSize = res->resultsSize ? res->results->referencesSize : 0;
1634 references = res->results;
1635 statusCode = referencesSize ? references->statusCode : res->responseHeader.serviceResult;
1636 continuationPoint = res->resultsSize ? &res->results->continuationPoint : nullptr;
1637 } else {
1638 const auto res = static_cast<UA_BrowseResponse *>(response);
1639 referencesSize = res->resultsSize ? res->results->referencesSize : 0;
1640 references = res->results;
1641 statusCode = referencesSize ? references->statusCode : res->responseHeader.serviceResult;
1642 continuationPoint = res->resultsSize ? &res->results->continuationPoint : nullptr;
1643 }
1644
1645 convertBrowseResult(src: references, referencesSize, dst&: context.results);
1646
1647 if (statusCode == UA_STATUSCODE_GOOD && continuationPoint && continuationPoint->length) {
1648 UA_BrowseNextRequest request;
1649 UA_BrowseNextRequest_init(p: &request);
1650 request.requestHeader.timeoutHint = backend->m_asyncRequestTimeout;
1651 UaDeleter<UA_BrowseNextRequest> requestDeleter(&request, UA_BrowseNextRequest_clear);
1652
1653 request.continuationPointsSize = 1;
1654 request.continuationPoints = UA_ByteString_new();
1655 UA_ByteString_copy(src: continuationPoint, dst: request.continuationPoints);
1656
1657 quint32 requestId = 0;
1658 statusCode =__UA_Client_AsyncService(client, request: &request, requestType: &UA_TYPES[UA_TYPES_BROWSENEXTREQUEST], callback: &asyncBrowseCallback,
1659 responseType: &UA_TYPES[UA_TYPES_BROWSENEXTRESPONSE], userdata: backend, requestId: &requestId);
1660
1661 if (statusCode == UA_STATUSCODE_GOOD) {
1662 context.isBrowseNext = true;
1663 backend->m_asyncBrowseContext[requestId] = context;
1664 backend->triggerIterateClient();
1665 return;
1666 }
1667 } else if (statusCode != UA_STATUSCODE_GOOD) {
1668 emit backend->browseFinished(handle: context.handle, children: QList<QOpcUaReferenceDescription>(),
1669 statusCode: static_cast<QOpcUa::UaStatusCode>(statusCode));
1670 return;
1671 }
1672
1673 emit backend->browseFinished(handle: context.handle, children: context.results, statusCode: static_cast<QOpcUa::UaStatusCode>(statusCode));
1674}
1675
1676void Open62541AsyncBackend::asyncBatchReadCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1677{
1678 Q_UNUSED(client)
1679
1680 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1681 auto context = backend->m_asyncBatchReadContext.take(key: requestId);
1682
1683 const auto res = static_cast<UA_ReadResponse *>(response);
1684
1685 QOpcUa::UaStatusCode serviceResult = static_cast<QOpcUa::UaStatusCode>(res->responseHeader.serviceResult);
1686
1687 if (serviceResult != QOpcUa::UaStatusCode::Good) {
1688 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Batch read failed:" << serviceResult;
1689 emit backend->readNodeAttributesFinished(results: QList<QOpcUaReadResult>(), serviceResult);
1690 } else {
1691 QList<QOpcUaReadResult> ret;
1692
1693 for (qsizetype i = 0; i < context.nodesToRead.size(); ++i) {
1694 QOpcUaReadResult item;
1695 item.setAttribute(context.nodesToRead.at(i).attribute());
1696 item.setNodeId(context.nodesToRead.at(i).nodeId());
1697 item.setIndexRange(context.nodesToRead.at(i).indexRange());
1698 if (static_cast<size_t>(i) < res->resultsSize) {
1699 if (res->results[i].hasServerTimestamp)
1700 item.setServerTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime>(data: &res->results[i].serverTimestamp));
1701 if (res->results[i].hasSourceTimestamp)
1702 item.setSourceTimestamp(QOpen62541ValueConverter::scalarToQt<QDateTime>(data: &res->results[i].sourceTimestamp));
1703 if (res->results[i].hasValue)
1704 item.setValue(QOpen62541ValueConverter::toQVariant(res->results[i].value));
1705 if (res->results[i].hasStatus)
1706 item.setStatusCode(static_cast<QOpcUa::UaStatusCode>(res->results[i].status));
1707 else
1708 item.setStatusCode(serviceResult);
1709 } else {
1710 item.setStatusCode(serviceResult);
1711 }
1712 ret.push_back(t: item);
1713 }
1714 emit backend->readNodeAttributesFinished(results: ret, serviceResult);
1715 }
1716}
1717
1718void Open62541AsyncBackend::asyncBatchWriteCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1719{
1720 Q_UNUSED(client)
1721
1722 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1723 const auto context = backend->m_asyncBatchWriteContext.take(key: requestId);
1724
1725 const auto res = static_cast<UA_WriteResponse *>(response);
1726
1727 QOpcUa::UaStatusCode serviceResult = QOpcUa::UaStatusCode(res->responseHeader.serviceResult);
1728
1729 if (serviceResult != QOpcUa::UaStatusCode::Good) {
1730 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Batch write failed:" << serviceResult;
1731 emit backend->writeNodeAttributesFinished(results: QList<QOpcUaWriteResult>(), serviceResult);
1732 } else {
1733 QList<QOpcUaWriteResult> ret;
1734
1735 for (qsizetype i = 0; i < context.nodesToWrite.size(); ++i) {
1736 QOpcUaWriteResult item;
1737 item.setAttribute(context.nodesToWrite.at(i).attribute());
1738 item.setNodeId(context.nodesToWrite.at(i).nodeId());
1739 item.setIndexRange(context.nodesToWrite.at(i).indexRange());
1740 if (static_cast<size_t>(i) < res->resultsSize)
1741 item.setStatusCode(QOpcUa::UaStatusCode(res->results[i]));
1742 else
1743 item.setStatusCode(serviceResult);
1744 ret.push_back(t: item);
1745 }
1746 emit backend->writeNodeAttributesFinished(results: ret, serviceResult);
1747 }
1748}
1749
1750void Open62541AsyncBackend::asyncReadHistoryDataCallBack(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1751{
1752 Q_UNUSED(client);
1753
1754 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1755 AsyncReadHistoryDataContext context = backend->m_asyncReadHistoryDataContext.take(key: requestId);
1756
1757 UA_HistoryReadResponse* res = static_cast<UA_HistoryReadResponse*>(response);
1758
1759 QList<QByteArray> continuationPoints;
1760
1761 QList<QOpcUaHistoryData> historyData;
1762
1763 for (size_t i = 0; i < res->resultsSize; ++i) {
1764 if (res->results[i].historyData.encoding != UA_EXTENSIONOBJECT_DECODED) {
1765 emit backend->historyDataAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode(res->responseHeader.serviceResult), handle: context.handle);
1766 return;
1767 }
1768
1769 historyData.push_back(t: QOpcUaHistoryData(context.historyReadRawRequest.nodesToRead().at(i).nodeId()));
1770
1771 historyData[i].setStatusCode(QOpcUa::UaStatusCode(res->results[i].statusCode));
1772
1773 if (res->results[i].statusCode != UA_STATUSCODE_GOOD)
1774 continue;
1775
1776 if (res->results[i].historyData.content.decoded.type != &UA_TYPES[UA_TYPES_HISTORYDATA]) {
1777 historyData[i].setStatusCode(QOpcUa::UaStatusCode::BadInternalError);
1778 continue;
1779 }
1780
1781 UA_HistoryData *data = static_cast<UA_HistoryData *>(res->results[i].historyData.content.decoded.data);
1782 for (size_t j = 0; j < data->dataValuesSize; ++j) {
1783 const QOpcUaDataValue value = QOpen62541ValueConverter::scalarToQt<QOpcUaDataValue, UA_DataValue>(data: &data->dataValues[j]);
1784 historyData[i].addValue(value);
1785 }
1786
1787 continuationPoints.push_back(t: QOpen62541ValueConverter::scalarToQt<QByteArray, UA_ByteString>(data: &res->results[i].continuationPoint));
1788 }
1789
1790 emit backend->historyDataAvailable(data: historyData, continuationPoints, serviceResult: QOpcUa::UaStatusCode(res->responseHeader.serviceResult), handle: context.handle);
1791}
1792
1793void Open62541AsyncBackend::asyncRegisterNodesCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1794{
1795 Q_UNUSED(client)
1796
1797 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1798 const auto context = backend->m_asyncRegisterUnregisterNodesContext.take(key: requestId);
1799
1800 const auto res = static_cast<UA_RegisterNodesResponse *>(response);
1801
1802 const auto serviceResult = QOpcUa::UaStatusCode(res->responseHeader.serviceResult);
1803
1804 if (serviceResult != QOpcUa::UaStatusCode::Good) {
1805 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Register nodes failed:" << serviceResult;
1806 emit backend->registerNodesFinished(nodesToRegister: context.nodeIds, registeredNodeIds: {}, statusCode: serviceResult);
1807 } else {
1808 QStringList resultIds;
1809 for (size_t i = 0; i < res->registeredNodeIdsSize; ++i)
1810 resultIds.push_back(t: QOpen62541ValueConverter::scalarToQt<QString, UA_NodeId>(data: &res->registeredNodeIds[i]));
1811
1812 emit backend->registerNodesFinished(nodesToRegister: context.nodeIds, registeredNodeIds: resultIds, statusCode: serviceResult);
1813 }
1814}
1815
1816void Open62541AsyncBackend::asyncUnregisterNodesCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1817{
1818 Q_UNUSED(client)
1819
1820 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1821 const auto context = backend->m_asyncRegisterUnregisterNodesContext.take(key: requestId);
1822
1823 const auto res = static_cast<UA_UnregisterNodesResponse *>(response);
1824
1825 const auto serviceResult = QOpcUa::UaStatusCode(res->responseHeader.serviceResult);
1826
1827 if (serviceResult != QOpcUa::UaStatusCode::Good)
1828 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unregister nodes failed:" << serviceResult;
1829
1830 emit backend->unregisterNodesFinished(nodesToUnregister: context.nodeIds, statusCode: serviceResult);
1831}
1832
1833void Open62541AsyncBackend::asyncReadHistoryEventsCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, void *response)
1834{
1835 Q_UNUSED(client);
1836
1837 Open62541AsyncBackend *backend = static_cast<Open62541AsyncBackend *>(userdata);
1838 auto context = backend->m_asyncReadHistoryEventsContext.take(key: requestId);
1839
1840 auto res = static_cast<UA_HistoryReadResponse*>(response);
1841
1842 QList<QByteArray> continuationPoints;
1843
1844 QList<QOpcUaHistoryEvent> historyEvents;
1845
1846 for (size_t i = 0; i < res->resultsSize; ++i) {
1847 if (res->results[i].historyData.encoding != UA_EXTENSIONOBJECT_DECODED) {
1848 emit backend->historyEventsAvailable(data: {}, continuationPoints: {}, serviceResult: QOpcUa::UaStatusCode(res->responseHeader.serviceResult), handle: context.handle);
1849 return;
1850 }
1851
1852 historyEvents.push_back(t: QOpcUaHistoryEvent(context.historyReadEventRequest.nodesToRead().at(i).nodeId()));
1853
1854 historyEvents[i].setStatusCode(QOpcUa::UaStatusCode(res->results[i].statusCode));
1855
1856 if (res->results[i].statusCode != UA_STATUSCODE_GOOD)
1857 continue;
1858
1859 if (res->results[i].historyData.content.decoded.type != &UA_TYPES[UA_TYPES_HISTORYEVENT]) {
1860 historyEvents[i].setStatusCode(QOpcUa::UaStatusCode::BadInternalError);
1861 continue;
1862 }
1863
1864 auto events = static_cast<UA_HistoryEvent *>(res->results[i].historyData.content.decoded.data);
1865 for (size_t j = 0; j < events->eventsSize; ++j) {
1866 QVariantList eventFields;
1867 for (size_t k = 0; k < events->events[j].eventFieldsSize; ++k)
1868 eventFields.push_back(t: QOpen62541ValueConverter::toQVariant(events->events[j].eventFields[k]));
1869 historyEvents.back().addEvent(value: eventFields);
1870 }
1871
1872 continuationPoints.push_back(t: QOpen62541ValueConverter::scalarToQt<QByteArray, UA_ByteString>(data: &res->results[i].continuationPoint));
1873 }
1874
1875 emit backend->historyEventsAvailable(data: historyEvents, continuationPoints, serviceResult: QOpcUa::UaStatusCode(res->responseHeader.serviceResult), handle: context.handle);
1876}
1877
1878bool Open62541AsyncBackend::loadFileToByteString(const QString &location, UA_ByteString *target) const
1879{
1880 if (location.isEmpty()) {
1881 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to read from empty file path";
1882 return false;
1883 }
1884
1885 if (!target) {
1886 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No target ByteString given";
1887 return false;
1888 }
1889
1890 UA_ByteString_init(p: target);
1891
1892 QFile file(location);
1893
1894 if (!file.open(flags: QFile::ReadOnly)) {
1895 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to open file" << location << file.errorString();
1896 return false;
1897 }
1898
1899 QByteArray data = file.readAll();
1900
1901 UA_ByteString temp;
1902 temp.length = data.size();
1903 if (data.isEmpty())
1904 temp.data = nullptr;
1905 else {
1906 if (data.startsWith(c: '-')) { // PEM file
1907 // mbedTLS expects PEM encoded data to be null terminated
1908 data = data.append(c: '\0');
1909 temp.length = data.size();
1910 }
1911 temp.data = reinterpret_cast<unsigned char *>(data.data());
1912 }
1913
1914 return UA_ByteString_copy(src: &temp, dst: target) == UA_STATUSCODE_GOOD;
1915}
1916
1917bool Open62541AsyncBackend::loadAllFilesInDirectory(const QString &location, UA_ByteString **target, qsizetype *size) const
1918{
1919 if (location.isEmpty()) {
1920 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Unable to read from empty file path";
1921 return false;
1922 }
1923
1924 if (!target) {
1925 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "No target ByteString given";
1926 return false;
1927 }
1928
1929 *target = nullptr;
1930 *size = 0;
1931
1932 QDir dir(location);
1933
1934 auto entries = dir.entryList(filters: QDir::Files);
1935
1936 if (entries.isEmpty()) {
1937 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Directory is empty";
1938 return true;
1939 }
1940
1941 const qsizetype tempSize = entries.size();
1942 UA_ByteString *list = static_cast<UA_ByteString *>(UA_Array_new(size: tempSize, type: &UA_TYPES[UA_TYPES_BYTESTRING]));
1943
1944 if (!list) {
1945 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to allocate memory for loading files in" << location;
1946 return false;
1947 }
1948
1949 for (qsizetype i = 0; i < entries.size(); ++i) {
1950 if (!loadFileToByteString(location: dir.filePath(fileName: entries.at(i)), target: &list[i])) {
1951 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to open file" << entries.at(i);
1952 UA_Array_delete(p: list, size: tempSize, type: &UA_TYPES[UA_TYPES_BYTESTRING]);
1953 size = 0;
1954 *target = nullptr;
1955 return false;
1956 }
1957 }
1958
1959 *target = list;
1960 *size = tempSize;
1961
1962 return true;
1963}
1964
1965void Open62541AsyncBackend::disconnectInternal(QOpcUaClient::ClientError error)
1966{
1967 m_clientIterateTimer.stop();
1968 m_clientIterateOnDemandTimer.stop();
1969
1970 if (m_uaclient) {
1971 // Disable the state callback, we will emit stateAndOrErrorChanged() here
1972 UA_Client_getConfig(client: m_uaclient)->stateCallback = nullptr;
1973 UA_Client_getConfig(client: m_uaclient)->inactivityCallback = nullptr;
1974 }
1975
1976 cleanupSubscriptions();
1977
1978 if (m_uaclient) {
1979 UA_Client_disconnect(client: m_uaclient);
1980 UA_Client_delete(client: m_uaclient);
1981 m_uaclient = nullptr;
1982 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error);
1983 }
1984}
1985
1986#ifdef UA_ENABLE_ENCRYPTION
1987bool Open62541AsyncBackend::setupClientConfigSecurity(const QOpcUaAuthenticationInformation &authInfo,
1988 const QOpcUaPkiConfiguration &pkiConfig,
1989 const QOpcUaEndpointDescription &endpoint)
1990{
1991 const auto conf = UA_Client_getConfig(client: m_uaclient);
1992 if (!conf)
1993 return false;
1994
1995 UA_ByteString privateKey = UA_BYTESTRING_NULL;
1996 UaDeleter<UA_ByteString> privateKeyDeleter(&privateKey, &UA_ByteString_clear);
1997
1998 if (pkiConfig.isKeyAndCertificateFileSet()) {
1999 UA_ByteString localCertificate;
2000 UA_ByteString *trustList = nullptr;
2001 qsizetype trustListSize = 0;
2002 UA_ByteString *revocationList = nullptr;
2003 qsizetype revocationListSize = 0;
2004
2005 bool success = loadFileToByteString(location: pkiConfig.clientCertificateFile(), target: &localCertificate);
2006
2007 if (!success) {
2008 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load client certificate";
2009 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2010 UA_Client_delete(client: m_uaclient);
2011 m_uaclient = nullptr;
2012 return false;
2013 }
2014
2015 UaDeleter<UA_ByteString> clientCertDeleter(&localCertificate, &UA_ByteString_clear);
2016
2017 success = loadPrivateKeyWithPotentialPassword(privateKeyPath: pkiConfig.privateKeyFile(), privateKey);
2018
2019 if (!success) {
2020 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load private key";
2021 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2022 UA_Client_delete(client: m_uaclient);
2023 m_uaclient = nullptr;
2024 return false;
2025 }
2026
2027 if (!pkiConfig.trustListDirectory().isEmpty()) {
2028 success = loadAllFilesInDirectory(location: pkiConfig.trustListDirectory(), target: &trustList, size: &trustListSize);
2029
2030 if (!success) {
2031 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load trust list";
2032 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2033 UA_Client_delete(client: m_uaclient);
2034 m_uaclient = nullptr;
2035 return false;
2036 }
2037 }
2038
2039 UaArrayDeleter<UA_TYPES_BYTESTRING> trustListDeleter(trustList, trustListSize);
2040
2041 if (!pkiConfig.revocationListDirectory().isEmpty()) {
2042 success = loadAllFilesInDirectory(location: pkiConfig.revocationListDirectory(), target: &revocationList, size: &revocationListSize);
2043
2044 if (!success) {
2045 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load revocation list";
2046 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2047 UA_Client_delete(client: m_uaclient);
2048 m_uaclient = nullptr;
2049 return false;
2050 }
2051 }
2052
2053 UaArrayDeleter<UA_TYPES_BYTESTRING> revocationListDeleter(revocationList, revocationListSize);
2054
2055 // UA_ClientConfig_setDefaultEncryption() no longer adds Basic128Rsa15 and Basic256 to the security policies in v1.4.7
2056 // This must be done manually (for now).
2057 UA_StatusCode result = UA_ClientConfig_setDefault(config: conf);
2058
2059 if (result != UA_STATUSCODE_GOOD) {
2060 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set client config defaults:" << static_cast<QOpcUa::UaStatusCode>(result);
2061 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::UnknownError);
2062 UA_Client_delete(client: m_uaclient);
2063 m_uaclient = nullptr;
2064 return false;
2065 }
2066
2067 if (conf->certificateVerification.clear)
2068 conf->certificateVerification.clear(&conf->certificateVerification);
2069 result = UA_CertificateVerification_Trustlist(cv: &conf->certificateVerification,
2070 certificateTrustList: trustList, certificateTrustListSize: trustListSize,
2071 certificateIssuerList: nullptr, certificateIssuerListSize: 0,
2072 certificateRevocationList: revocationList, certificateRevocationListSize: revocationListSize);
2073
2074 if (result != UA_STATUSCODE_GOOD) {
2075 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set up certificate verification:" << static_cast<QOpcUa::UaStatusCode>(result);
2076 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2077 UA_Client_delete(client: m_uaclient);
2078 m_uaclient = nullptr;
2079 return false;
2080 }
2081
2082 result = setAuthSecurityPolicyInClientConfig(conf, cert: localCertificate, key: privateKey, desc: endpoint, tokenType: authInfo.authenticationType());
2083
2084 if (result != UA_STATUSCODE_GOOD) {
2085 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set up auth security policies:" << static_cast<QOpcUa::UaStatusCode>(result);
2086 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2087 UA_Client_delete(client: m_uaclient);
2088 m_uaclient = nullptr;
2089 return false;
2090 }
2091
2092 result = setSecurityPolicyInClientConfig(conf, cert: localCertificate, key: privateKey, desc: endpoint);
2093
2094 if (result != UA_STATUSCODE_GOOD) {
2095 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to set up security policies:" << static_cast<QOpcUa::UaStatusCode>(result);
2096 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::InvalidPki);
2097 UA_Client_delete(client: m_uaclient);
2098 m_uaclient = nullptr;
2099 return false;
2100 }
2101 } else {
2102 return UA_ClientConfig_setDefault(config: conf) == UA_STATUSCODE_GOOD;
2103 }
2104
2105 return true;
2106}
2107#endif
2108
2109void Open62541AsyncBackend::establishConnectionInternal(const QOpcUaAuthenticationInformation &authInfo,
2110#ifdef UA_ENABLE_ENCRYPTION
2111 const QOpcUaPkiConfiguration &pkiConfig,
2112#endif
2113 const QOpcUaEndpointDescription &endpoint)
2114{
2115 const auto conf = UA_Client_getConfig(client: m_uaclient);
2116 if (!conf)
2117 return;
2118
2119 UA_StatusCode ret = UA_STATUSCODE_BADINTERNALERROR;
2120 bool retry = false;
2121
2122 do {
2123 retry = false;
2124
2125 if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Anonymous) {
2126 // Nothing to do, Anonymous is default
2127 } else if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Username) {
2128 const auto credentials = authInfo.authenticationData().value<QPair<QString, QString>>();
2129 UA_ClientConfig_setAuthenticationUsername(config: conf, username: credentials.first.toUtf8().constData(),
2130 password: credentials.second.toUtf8().constData());
2131 } else if (authInfo.authenticationType() == QOpcUaUserTokenPolicy::TokenType::Certificate) {
2132#ifdef UA_ENABLE_ENCRYPTION
2133 QString certPath;
2134 QString keyPath;
2135
2136 if (authInfo.authenticationData().canConvert<QPair<QString, QString>>()) {
2137 const auto authPaths = authInfo.authenticationData().value<QPair<QString, QString>>();
2138
2139 if (authPaths.first.isEmpty() || authPaths.second.isEmpty()) {
2140 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Certificate and private key path must be set for certificate auth";
2141 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::ClientError::InvalidAuthenticationInformation);
2142 UA_Client_delete(client: m_uaclient);
2143 m_uaclient = nullptr;
2144 return;
2145 }
2146
2147 certPath = authPaths.first;
2148 keyPath = authPaths.second;
2149 } else {
2150 certPath = pkiConfig.clientCertificateFile();
2151 keyPath = pkiConfig.privateKeyFile();
2152 }
2153
2154 UA_ByteString cert = UA_BYTESTRING_NULL;
2155 UA_ByteString key = UA_BYTESTRING_NULL;
2156
2157 if (!loadFileToByteString(location: certPath, target: &cert)) {
2158 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load certificate for certificate auth";
2159 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::ClientError::InvalidAuthenticationInformation);
2160 UA_Client_delete(client: m_uaclient);
2161 m_uaclient = nullptr;
2162 return;
2163 }
2164
2165 UaDeleter<UA_ByteString> certDeleter(&cert, &UA_ByteString_clear);
2166
2167 if (!loadPrivateKeyWithPotentialPassword(privateKeyPath: keyPath, privateKey&: key)) {
2168 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to load private key for certificate auth";
2169 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::ClientError::InvalidAuthenticationInformation);
2170 UA_Client_delete(client: m_uaclient);
2171 m_uaclient = nullptr;
2172 return;
2173 }
2174
2175 UaDeleter<UA_ByteString> keyDeleter(&key, &UA_ByteString_clear);
2176
2177 // UA_ClientConfig_setAuthenticationCert() overwrites the auth security policies,
2178 // so we must call our custom implementation
2179
2180 UA_X509IdentityToken *token = UA_X509IdentityToken_new();
2181 UA_StatusCode result = UA_ByteString_copy(src: &cert, dst: &token->certificateData);
2182 UA_ExtensionObject_clear(p: &conf->userIdentityToken);
2183 conf->userIdentityToken.encoding = UA_EXTENSIONOBJECT_DECODED;
2184 conf->userIdentityToken.content.decoded.type = &UA_TYPES[UA_TYPES_X509IDENTITYTOKEN];
2185 conf->userIdentityToken.content.decoded.data = token;
2186
2187 if (result != UA_STATUSCODE_GOOD) {
2188 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to initialize certificate auth:" << UA_StatusCode_name(code: result);
2189 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::ClientError::UnsupportedAuthenticationInformation);
2190 UA_Client_delete(client: m_uaclient);
2191 m_uaclient = nullptr;
2192 return;
2193 }
2194#endif
2195 } else {
2196 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error: QOpcUaClient::UnsupportedAuthenticationInformation);
2197 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Failed to connect: Selected authentication type"
2198 << authInfo.authenticationType() << "is not supported.";
2199 UA_Client_delete(client: m_uaclient);
2200 m_uaclient = nullptr;
2201 return;
2202 }
2203
2204 ret = UA_Client_connect(client: m_uaclient, endpointUrl: endpoint.endpointUrl().toUtf8().constData());
2205
2206#ifdef UA_ENABLE_ENCRYPTION
2207 if (ret == UA_STATUSCODE_BADCERTIFICATEUNTRUSTED) {
2208 QOpcUaErrorState errorState;
2209 errorState.setClientSideError(true);
2210 errorState.setConnectionStep(QOpcUaErrorState::ConnectionStep::CertificateValidation);
2211 errorState.setErrorCode(QOpcUa::UaStatusCode::BadCertificateUntrusted);
2212
2213 emit QOpcUaBackend::connectError(errorState: &errorState);
2214
2215 if (errorState.ignoreError()) {
2216 // Use the AcceptAll certificate verification
2217 UA_CertificateVerification_AcceptAll(cv: &conf->certificateVerification);
2218 retry = true;
2219 }
2220 }
2221#endif
2222 } while (retry);
2223
2224 if (ret != UA_STATUSCODE_GOOD) {
2225 UA_Client_delete(client: m_uaclient);
2226 m_uaclient = nullptr;
2227 QOpcUaClient::ClientError error = QOpcUaClient::UnknownError;
2228 switch (ret) {
2229 case UA_STATUSCODE_BADUSERACCESSDENIED:
2230 case UA_STATUSCODE_BADIDENTITYTOKENINVALID: // Intentional fall through
2231 error = QOpcUaClient::AccessDenied;
2232 break;
2233 case UA_STATUSCODE_BADCERTIFICATEUNTRUSTED:
2234 error = QOpcUaClient::CertificateUntrusted;
2235 break;
2236 default:
2237 break;
2238 }
2239
2240 QOpcUaErrorState errorState;
2241 errorState.setConnectionStep(QOpcUaErrorState::ConnectionStep::Unknown);
2242 errorState.setErrorCode(static_cast<QOpcUa::UaStatusCode>(ret));
2243 errorState.setClientSideError(false);
2244 errorState.setIgnoreError(false);
2245
2246 // This signal is connected using Qt::BlockingQueuedConnection. It will place a metacall to a different thread and waits
2247 // until this metacall is fully handled before returning.
2248 emit QOpcUaBackend::connectError(errorState: &errorState);
2249
2250 emit stateAndOrErrorChanged(state: QOpcUaClient::Disconnected, error);
2251 qCWarning(QT_OPCUA_PLUGINS_OPEN62541) << "Open62541: Failed to connect";
2252 return;
2253 }
2254
2255 using Timeout_t = decltype(conf->timeout);
2256 conf->timeout = qt_saturate<Timeout_t>(x: m_currentConnectionSettings.requestTimeout().count());
2257
2258 // Attach the client state callback after the successful connect
2259 conf->stateCallback = clientStateCallback;
2260 conf->noReconnect = true;
2261
2262 m_clientIterateTimer.start(msec: m_clientIterateInterval);
2263 emit stateAndOrErrorChanged(state: QOpcUaClient::Connected, error: QOpcUaClient::NoError);
2264}
2265
2266#ifdef UA_ENABLE_ENCRYPTION
2267bool Open62541AsyncBackend::loadPrivateKeyWithPotentialPassword(const QString &privateKeyPath, UA_ByteString &privateKey)
2268{
2269 UA_ByteString privateKeyData = UA_BYTESTRING_NULL;
2270 const auto guard = qScopeGuard(f: [&privateKeyData]() {
2271 UA_ByteString_clear(p: &privateKeyData);
2272 });
2273
2274 const auto success = loadFileToByteString(location: privateKeyPath, target: &privateKeyData);
2275
2276 if (!success)
2277 return false;
2278
2279 UA_StatusCode decryptResult = UA_STATUSCODE_BADINTERNALERROR;
2280 QString password;
2281 bool previousTryWasInvalid = false;
2282 do {
2283 auto uaPassword = UA_STRING_ALLOC(password.toUtf8().constData());
2284 decryptResult = UA_PKI_decryptPrivateKey(privateKey: privateKeyData, password: uaPassword, outDerKey: &privateKey);
2285 UA_String_clear(p: &uaPassword);
2286
2287 // The key was already DER or had no password
2288 if (decryptResult == UA_STATUSCODE_GOOD)
2289 return true;
2290
2291 // Decoding failed, a password may be required or the given password was invalid
2292 if (decryptResult == UA_STATUSCODE_BADSECURITYCHECKSFAILED) {
2293 emit passwordForPrivateKeyRequired(keyFilePath: privateKeyPath, password: &password, previousTryWasInvalid);
2294
2295 // No further attempt requested
2296 if (password.isEmpty())
2297 return false;
2298
2299 previousTryWasInvalid = true;
2300 }
2301 } while (decryptResult == UA_STATUSCODE_BADSECURITYCHECKSFAILED);
2302
2303 return false;
2304}
2305
2306// Only add the security policy the requested endpoint requires
2307UA_StatusCode Open62541AsyncBackend::setSecurityPolicyInClientConfig(UA_ClientConfig *conf, const UA_ByteString &cert, const UA_ByteString &key,
2308 const QOpcUaEndpointDescription &desc)
2309{
2310 if (desc.securityPolicy() == QOpcUa::NonePolicy)
2311 return UA_STATUSCODE_GOOD;
2312
2313 const size_t numPolicies = conf->securityPoliciesSize + 1;
2314 conf->securityPolicies = static_cast<UA_SecurityPolicy *>(UA_realloc(ptr: conf->securityPolicies, size: sizeof(UA_SecurityPolicy) * numPolicies));
2315
2316 UA_StatusCode result = UA_STATUSCODE_GOOD;
2317
2318 if (desc.securityPolicy() == QOpcUa::Basic128Rsa15Policy)
2319 result = UA_SecurityPolicy_Basic128Rsa15(policy: &conf->securityPolicies[1],
2320 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2321 else if (desc.securityPolicy() == QOpcUa::Basic256Policy)
2322 result = UA_SecurityPolicy_Basic256(policy: &conf->securityPolicies[1],
2323 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2324 else if (desc.securityPolicy() == QOpcUa::Aes256Sha256RsaPssPolicy)
2325 result = UA_SecurityPolicy_Aes256Sha256RsaPss(policy: &conf->securityPolicies[1],
2326 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2327 else if (desc.securityPolicy() == QOpcUa::Basic256Sha256Policy)
2328 result = UA_SecurityPolicy_Basic256Sha256(policy: &conf->securityPolicies[1],
2329 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2330 else if (desc.securityPolicy() == QOpcUa::Aes128Sha256RsaOaepPolicy)
2331 result = UA_SecurityPolicy_Aes128Sha256RsaOaep(policy: &conf->securityPolicies[1],
2332 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2333 else
2334 result = UA_STATUSCODE_BADINVALIDARGUMENT;
2335
2336 if (result != UA_STATUSCODE_GOOD) {
2337 // UA_ClientConfig_clear() doesn't check for a valid clear() pointer on the policy
2338 // Don't increment securityPoliciesSize in case of an error
2339 return result;
2340 } else {
2341 conf->securityPoliciesSize = numPolicies;
2342 }
2343
2344 return result;
2345}
2346
2347// Find the most secure security policy a token of the required type supports
2348UA_StatusCode Open62541AsyncBackend::setAuthSecurityPolicyInClientConfig(UA_ClientConfig *conf, const UA_ByteString &cert,
2349 const UA_ByteString &key,
2350 const QOpcUaEndpointDescription &desc,
2351 QOpcUaUserTokenPolicy::TokenType tokenType)
2352{
2353 // No None policy for auth, but all encrypting policies
2354 const size_t numPolicies = 1;
2355
2356 for (size_t i = 0; i < conf->authSecurityPoliciesSize; i++) {
2357 conf->authSecurityPolicies[i].clear(&conf->authSecurityPolicies[i]);
2358 }
2359 UA_free(ptr: conf->authSecurityPolicies);
2360 conf->authSecurityPolicies = nullptr;
2361 conf->authSecurityPoliciesSize = 0;
2362
2363 UA_StatusCode result = UA_STATUSCODE_BADINVALIDARGUMENT;
2364
2365 QString selectedPolicy;
2366 int lastIndex = -1;
2367 const auto tempTokens = desc.userIdentityTokens();
2368 for (const auto &token : tempTokens) {
2369 if (token.tokenType() != tokenType)
2370 continue;
2371
2372 auto currentPolicy = token.securityPolicy();
2373 if (currentPolicy.isEmpty())
2374 currentPolicy = desc.securityPolicy();
2375
2376 // Assumes that supportedSecurityPolicies() is sorted by strength
2377 // The supportedSecurityPolicies() has been modified accordingly.
2378 const auto index = m_clientImpl->supportedSecurityPolicies().indexOf(str: currentPolicy);
2379 if (index > lastIndex) {
2380 lastIndex = index;
2381 selectedPolicy = currentPolicy;
2382 }
2383 }
2384
2385 if (!selectedPolicy.isEmpty()) {
2386 if (selectedPolicy == QOpcUa::NonePolicy)
2387 return UA_STATUSCODE_GOOD;
2388
2389 conf->authSecurityPolicies = static_cast<UA_SecurityPolicy*>(UA_realloc(ptr: conf->authSecurityPolicies,
2390 size: sizeof(UA_SecurityPolicy) * numPolicies));
2391 conf->authSecurityPoliciesSize = numPolicies;
2392
2393 if (selectedPolicy == QOpcUa::Basic128Rsa15Policy)
2394 result = UA_SecurityPolicy_Basic128Rsa15(policy: &conf->authSecurityPolicies[0],
2395 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2396 else if (selectedPolicy == QOpcUa::Basic256Policy)
2397 result = UA_SecurityPolicy_Basic256(policy: &conf->authSecurityPolicies[0],
2398 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2399 else if (selectedPolicy == QOpcUa::Aes256Sha256RsaPssPolicy)
2400 result = UA_SecurityPolicy_Aes256Sha256RsaPss(policy: &conf->authSecurityPolicies[0],
2401 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2402 else if (selectedPolicy == QOpcUa::Basic256Sha256Policy)
2403 result = UA_SecurityPolicy_Basic256Sha256(policy: &conf->authSecurityPolicies[0],
2404 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2405 else if (selectedPolicy == QOpcUa::Aes128Sha256RsaOaepPolicy)
2406 result = UA_SecurityPolicy_Aes128Sha256RsaOaep(policy: &conf->authSecurityPolicies[0],
2407 localCertificate: cert, localPrivateKey: key, logger: conf->logging);
2408
2409 if (result != UA_STATUSCODE_GOOD) {
2410 // UA_ClientConfig_clear() doesn't check for a valid clear() pointer on the policy
2411 conf->authSecurityPoliciesSize = 0;
2412 UA_free(ptr: conf->authSecurityPolicies);
2413 conf->authSecurityPolicies = nullptr;
2414 }
2415 }
2416
2417 return result;
2418}
2419#endif
2420
2421UA_ExtensionObject Open62541AsyncBackend::assembleNodeAttributes(const QOpcUaNodeCreationAttributes &nodeAttributes,
2422 QOpcUa::NodeClass nodeClass)
2423{
2424 UA_ExtensionObject obj;
2425 UA_ExtensionObject_init(p: &obj);
2426 obj.encoding = UA_EXTENSIONOBJECT_DECODED;
2427
2428 switch (nodeClass) {
2429 case QOpcUa::NodeClass::Object: {
2430 UA_ObjectAttributes *attr = UA_ObjectAttributes_new();
2431 *attr = UA_ObjectAttributes_default;
2432 obj.content.decoded.data = attr;
2433 obj.content.decoded.type = &UA_TYPES[UA_TYPES_OBJECTATTRIBUTES];
2434
2435 if (nodeAttributes.hasEventNotifier()) {
2436 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_EVENTNOTIFIER;
2437 attr->eventNotifier = nodeAttributes.eventNotifier();
2438 }
2439 break;
2440 }
2441 case QOpcUa::NodeClass::Variable: {
2442 UA_VariableAttributes *attr = UA_VariableAttributes_new();
2443 *attr = UA_VariableAttributes_default;
2444 obj.content.decoded.data = attr;
2445 obj.content.decoded.type = &UA_TYPES[UA_TYPES_VARIABLEATTRIBUTES];
2446
2447 if (nodeAttributes.hasValue()) {
2448 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_VALUE;
2449 attr->value = QOpen62541ValueConverter::toOpen62541Variant(nodeAttributes.value(),
2450 nodeAttributes.valueType());
2451 }
2452 if (nodeAttributes.hasDataTypeId()) {
2453 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_DATATYPE;
2454 attr->dataType = Open62541Utils::nodeIdFromQString(name: nodeAttributes.dataTypeId());
2455 }
2456 if (nodeAttributes.hasValueRank()) {
2457 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_VALUERANK;
2458 attr->valueRank = nodeAttributes.valueRank();
2459 }
2460 if (nodeAttributes.hasArrayDimensions()) {
2461 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ARRAYDIMENSIONS;
2462 attr->arrayDimensions = copyArrayDimensions(arrayDimensions: nodeAttributes.arrayDimensions(), outputSize: &attr->arrayDimensionsSize);
2463 }
2464 if (nodeAttributes.hasAccessLevel()) {
2465 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ACCESSLEVEL;
2466 attr->accessLevel = nodeAttributes.accessLevel();
2467 }
2468 if (nodeAttributes.hasUserAccessLevel()) {
2469 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_USERACCESSLEVEL;
2470 attr->userAccessLevel = nodeAttributes.userAccessLevel();
2471 }
2472 if (nodeAttributes.hasMinimumSamplingInterval()) {
2473 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_MINIMUMSAMPLINGINTERVAL;
2474 attr->minimumSamplingInterval = nodeAttributes.minimumSamplingInterval();
2475 }
2476 if (nodeAttributes.hasHistorizing()) {
2477 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_HISTORIZING;
2478 attr->historizing = nodeAttributes.historizing();
2479 }
2480 break;
2481 }
2482 case QOpcUa::NodeClass::Method: {
2483 UA_MethodAttributes *attr = UA_MethodAttributes_new();
2484 *attr = UA_MethodAttributes_default;
2485 obj.content.decoded.data = attr;
2486 obj.content.decoded.type = &UA_TYPES[UA_TYPES_METHODATTRIBUTES];
2487
2488 if (nodeAttributes.hasExecutable()) {
2489 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_EXECUTABLE;
2490 attr->executable = nodeAttributes.executable();
2491 }
2492 if (nodeAttributes.hasUserExecutable()) {
2493 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_USEREXECUTABLE;
2494 attr->userExecutable = nodeAttributes.userExecutable();
2495 }
2496 break;
2497 }
2498 case QOpcUa::NodeClass::ObjectType: {
2499 UA_ObjectTypeAttributes *attr = UA_ObjectTypeAttributes_new();
2500 *attr = UA_ObjectTypeAttributes_default;
2501 obj.content.decoded.data = attr;
2502 obj.content.decoded.type = &UA_TYPES[UA_TYPES_OBJECTTYPEATTRIBUTES];
2503
2504 if (nodeAttributes.hasIsAbstract()) {
2505 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ISABSTRACT;
2506 attr->isAbstract = nodeAttributes.isAbstract();
2507 }
2508 break;
2509 }
2510 case QOpcUa::NodeClass::VariableType: {
2511 UA_VariableTypeAttributes *attr = UA_VariableTypeAttributes_new();
2512 *attr = UA_VariableTypeAttributes_default;
2513 obj.content.decoded.data = attr;
2514 obj.content.decoded.type = &UA_TYPES[UA_TYPES_VARIABLETYPEATTRIBUTES];
2515
2516 if (nodeAttributes.hasValue()) {
2517 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_VALUE;
2518 attr->value = QOpen62541ValueConverter::toOpen62541Variant(nodeAttributes.value(),
2519 nodeAttributes.valueType());
2520 }
2521 if (nodeAttributes.hasDataTypeId()) {
2522 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_DATATYPE;
2523 attr->dataType = Open62541Utils::nodeIdFromQString(name: nodeAttributes.dataTypeId());
2524 }
2525 if (nodeAttributes.hasValueRank()) {
2526 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_VALUERANK;
2527 attr->valueRank = nodeAttributes.valueRank();
2528 }
2529 if (nodeAttributes.hasArrayDimensions()) {
2530 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ARRAYDIMENSIONS;
2531 attr->arrayDimensions = copyArrayDimensions(arrayDimensions: nodeAttributes.arrayDimensions(), outputSize: &attr->arrayDimensionsSize);
2532 }
2533 if (nodeAttributes.hasIsAbstract()) {
2534 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ISABSTRACT;
2535 attr->isAbstract = nodeAttributes.isAbstract();
2536 }
2537 break;
2538 }
2539 case QOpcUa::NodeClass::ReferenceType: {
2540 UA_ReferenceTypeAttributes *attr = UA_ReferenceTypeAttributes_new();
2541 *attr = UA_ReferenceTypeAttributes_default;
2542 obj.content.decoded.data = attr;
2543 obj.content.decoded.type = &UA_TYPES[UA_TYPES_REFERENCETYPEATTRIBUTES];
2544
2545 if (nodeAttributes.hasIsAbstract()) {
2546 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ISABSTRACT;
2547 attr->isAbstract = nodeAttributes.isAbstract();
2548 }
2549 if (nodeAttributes.hasSymmetric()) {
2550 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_SYMMETRIC;
2551 attr->symmetric = nodeAttributes.symmetric();
2552 }
2553 if (nodeAttributes.hasInverseName()) {
2554 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_INVERSENAME;
2555 QOpen62541ValueConverter::scalarFromQt<UA_LocalizedText, QOpcUaLocalizedText>(
2556 var: nodeAttributes.inverseName(), ptr: &attr->inverseName);
2557 }
2558 break;
2559 }
2560 case QOpcUa::NodeClass::DataType: {
2561 UA_DataTypeAttributes *attr = UA_DataTypeAttributes_new();
2562 *attr = UA_DataTypeAttributes_default;
2563 obj.content.decoded.data = attr;
2564 obj.content.decoded.type = &UA_TYPES[UA_TYPES_DATATYPEATTRIBUTES];
2565
2566 if (nodeAttributes.hasIsAbstract()) {
2567 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_ISABSTRACT;
2568 attr->isAbstract = nodeAttributes.isAbstract();
2569 }
2570 break;
2571 }
2572 case QOpcUa::NodeClass::View: {
2573 UA_ViewAttributes *attr = UA_ViewAttributes_new();
2574 *attr = UA_ViewAttributes_default;
2575 obj.content.decoded.data = attr;
2576 obj.content.decoded.type = &UA_TYPES[UA_TYPES_VIEWATTRIBUTES];
2577
2578 if (nodeAttributes.hasContainsNoLoops()) {
2579 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_CONTAINSNOLOOPS;
2580 attr->containsNoLoops = nodeAttributes.containsNoLoops();
2581 }
2582 if (nodeAttributes.hasEventNotifier()) {
2583 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_EVENTNOTIFIER;
2584 attr->eventNotifier = nodeAttributes.eventNotifier();
2585 }
2586 break;
2587 }
2588 default:
2589 qCDebug(QT_OPCUA_PLUGINS_OPEN62541) << "Could not convert node attributes, unknown node class";
2590 UA_ExtensionObject_init(p: &obj);
2591 return obj;
2592 }
2593
2594 UA_ObjectAttributes *attr = reinterpret_cast<UA_ObjectAttributes *>(obj.content.decoded.data);
2595 if (nodeAttributes.hasDisplayName()) {
2596 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_DISPLAYNAME;
2597 QOpen62541ValueConverter::scalarFromQt<UA_LocalizedText, QOpcUaLocalizedText>(
2598 var: nodeAttributes.displayName(), ptr: &attr->displayName);
2599 }
2600 if (nodeAttributes.hasDescription()) {
2601 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_DESCRIPTION;
2602 QOpen62541ValueConverter::scalarFromQt<UA_LocalizedText, QOpcUaLocalizedText>(
2603 var: nodeAttributes.description(), ptr: &attr->description);
2604 }
2605 if (nodeAttributes.hasWriteMask()) {
2606 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_WRITEMASK;
2607 attr->writeMask = nodeAttributes.writeMask();
2608 }
2609 if (nodeAttributes.hasUserWriteMask()) {
2610 attr->specifiedAttributes |= UA_NODEATTRIBUTESMASK_USERWRITEMASK;
2611 attr->userWriteMask = nodeAttributes.userWriteMask();
2612 }
2613
2614 return obj;
2615}
2616
2617UA_UInt32 *Open62541AsyncBackend::copyArrayDimensions(const QList<quint32> &arrayDimensions, size_t *outputSize)
2618{
2619 if (outputSize)
2620 *outputSize = arrayDimensions.size();
2621
2622 if (!outputSize)
2623 return nullptr;
2624
2625 UA_UInt32 *data = nullptr;
2626 UA_StatusCode res = UA_Array_copy(src: arrayDimensions.constData(), size: arrayDimensions.size(),
2627 dst: reinterpret_cast<void **>(&data), type: &UA_TYPES[UA_TYPES_UINT32]);
2628 return res == UA_STATUSCODE_GOOD ? data : nullptr;
2629}
2630
2631QT_END_NAMESPACE
2632

source code of qtopcua/src/plugins/opcua/open62541/qopen62541backend.cpp