1// Copyright (C) 2019 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#include <private/opcuaconnection_p.h>
5#include <private/opcuareadresult_p.h>
6#include <private/opcuawriteitem_p.h>
7#include <private/opcuawriteresult_p.h>
8#include <private/universalnode_p.h>
9
10#include <QJSEngine>
11#include <QLoggingCategory>
12#include <QOpcUaProvider>
13#include <QOpcUaReadItem>
14#include <QOpcUaReadResult>
15#include <QOpcUaWriteItem>
16
17QT_BEGIN_NAMESPACE
18
19/*!
20 \qmltype Connection
21 \inqmlmodule QtOpcUa
22 \brief Connects to a server.
23 \since QtOpcUa 5.12
24
25 The main API uses backends to make connections. You have to set the backend before
26 any connection attempt.
27
28 \code
29 import QtOpcUa as QtOpcUa
30
31 QtOpcUa.Connection {
32 backend: "open62541"
33 }
34
35 Component.onCompleted: {
36 connection.connectToEndpoint("opc.tcp://127.0.0.1:43344");
37 }
38 \endcode
39*/
40
41/*!
42 \qmlproperty stringlist Connection::availableBackends
43 \readonly
44
45 Returns the names of all available backends as a list.
46 These are used to select a backend when connecting.
47
48 \sa Connection::backend
49*/
50
51/*!
52 \qmlproperty bool Connection::connected
53 \readonly
54
55 Status of the connection.
56 \c true when there is a connection, otherwise \c false.
57*/
58
59/*!
60 \qmlproperty string Connection::backend
61
62 Set the backend to use for a connection to the server.
63 Has to be set before any connection attempt.
64
65 \sa Connection::availableBackends
66*/
67
68/*!
69 \qmlproperty bool Connection::defaultConnection
70
71 Makes this the default connection.
72 Usually each node needs to be given a connection to use. If this property
73 is set to \c true, this connection will be used in all cases where a node has no
74 connection set. Already established connections are not affected.
75 If \c defaultConnection is set to \c true on multiple connection the last one is used.
76
77 \code
78 QtOpcUa.Connection {
79 ...
80 defaultConnection: true
81 ...
82 }
83 \endcode
84
85 \sa Node
86*/
87
88/*!
89 \qmlproperty stringlist Connection::namespaces
90 \readonly
91
92 List of strings of all namespace URIs registered on the connected server.
93*/
94
95/*!
96 \qmlproperty AuthenticationInformation Connection::authenticationInformation
97
98 Set the authentication information to this connection. The authentication information has
99 to be set before calling \l connectToEndpoint. If no authentication information is set,
100 the anonymous mode will be used.
101 It has no effect on the current connection. If the client is disconnected and then reconnected,
102 the new credentials are used.
103 Reading and writing this property before a \l backend is set, writes are ignored and reads return
104 and invalid \l AuthenticationInformation.
105*/
106
107/*!
108 \qmlproperty stringlist Connection::supportedSecurityPolicies
109 \since 5.13
110
111 A list of strings containing the supported security policies
112
113 This property is currently available as a Technology Preview, and therefore the API
114 and functionality provided may be subject to change at any time without
115 prior notice.
116*/
117
118/*!
119 \qmlproperty array[tokenTypes] Connection::supportedUserTokenTypes
120 \since 5.13
121
122 An array of user token policy types of all supported user token types.
123
124 This property is currently available as a Technology Preview, and therefore the API
125 and functionality provided may be subject to change at any time without
126 prior notice.
127*/
128
129/*!
130 \qmlproperty QOpcUaEndpointDescription Connection::currentEndpoint
131 \since 5.13
132
133 An endpoint description of the server to which the connection is connected to.
134 When the connection is not established, an empty endpoint description is returned.
135*/
136
137/*!
138 \qmlsignal Connection::readNodeAttributesFinished(readResults)
139 \since 5.13
140
141 Emitted when the read request, started using \l readNodeAttributes(), is finished.
142 The \a readResults parameter is an array of \l ReadResult entries, containing the
143 values requested from the server.
144
145 \code
146 connection.onReadNodeAttributesFinished(results) {
147 for (var i = 0; results.length; i++) {
148 if (results[i].status.isGood) {
149 console.log(results[i].value);
150 } else {
151 // handle error
152 }
153 }
154 }
155 \endcode
156
157 \sa readNodeAttributes(), ReadResult
158*/
159
160/*!
161 \qmlsignal Connection::writeNodeAttributesFinished(writeResults)
162 \since 5.13
163
164 Emitted when the write request started using \l writeNodeAttributes() is
165 finished. The \a writeResults parameter is an array of \l WriteResult entries,
166 containing the values requested from the server.
167
168 \code
169 for (var i = 0; i < writeResults.length; i++) {
170 console.log(writeResults[i].nodeId);
171 console.log(writeResults[i].namespaceName);
172 console.log(writeResults[i].attribute);
173
174 if (writeResults[i].status.isBad) {
175 // value was not written
176 }
177 }
178 \endcode
179
180 \sa writeNodeAttributes(), WriteResult
181
182*/
183
184/*!
185 \qmlproperty QOpcUaClient Connection::connection
186 \since 5.13
187
188 This property is used only to inject a connection from C++. In case of complex setup of
189 a connection you can use C++ to handle all the details. After the connection is established
190 it can be handed to QML using this property. Ownership of the client is transferred to QML.
191
192 \code
193 class MyClass : public QObject {
194 Q_OBJECT
195 Q_PROPERTY(QOpcUaClient* connection READ connection NOTIFY connectionChanged)
196
197 public:
198 MyClass (QObject* parent = nullptr);
199 QOpcUaClient *connection() const;
200
201 signals:
202 void connectionChanged(QOpcUaClient *);
203 \endcode
204
205 Emitting the signal \c connectionChanged when the client setup is completed, the QML code below will
206 use the connection.
207
208 \code
209 import QtOpcUa as QtOpcUa
210
211 MyClass {
212 id: myclass
213 }
214
215 QtOpcUa.Connection {
216 connection: myclass.connection
217 }
218 \endcode
219
220*/
221
222
223Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_QML)
224
225OpcUaConnection* OpcUaConnection::m_defaultConnection = nullptr;
226
227OpcUaConnection::OpcUaConnection(QObject *parent):
228 QObject(parent)
229{
230}
231
232OpcUaConnection::~OpcUaConnection()
233{
234 setDefaultConnection(false);
235 removeConnection();
236}
237
238QStringList OpcUaConnection::availableBackends() const
239{
240 return QOpcUaProvider::availableBackends();
241}
242
243bool OpcUaConnection::connected() const
244{
245 return m_connected && m_client;
246}
247
248void OpcUaConnection::setBackend(const QString &name)
249{
250 if (name.isEmpty())
251 return;
252
253 if (!availableBackends().contains(str: name)) {
254 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Backend '%1' is not available").arg(a: name);
255 qCDebug(QT_OPCUA_PLUGINS_QML) << tr(s: "Available backends:") << availableBackends().join(sep: QLatin1Char(','));
256 return;
257 }
258
259 if (m_client) {
260 if (m_client->backend() == name)
261 return;
262
263 removeConnection();
264 }
265
266 QOpcUaProvider provider;
267 m_client = provider.createClient(backend: name);
268 if (m_client) {
269 qCDebug(QT_OPCUA_PLUGINS_QML) << "Created plugin" << m_client->backend();
270 setupConnection();
271 } else {
272 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Backend '%1' could not be created.").arg(a: name);
273 }
274 emit backendChanged();
275}
276
277QString OpcUaConnection::backend() const
278{
279 if (m_client)
280 return m_client->backend();
281 else
282 return QString();
283}
284
285OpcUaConnection *OpcUaConnection::defaultConnection()
286{
287 return m_defaultConnection;
288}
289
290bool OpcUaConnection::isDefaultConnection() const
291{
292 return m_defaultConnection == this;
293}
294
295/*!
296 \qmlmethod Connection::connectToEndpoint(endpointDescription)
297
298 Connects to the endpoint specified with \a endpointDescription.
299
300 \sa EndpointDescription
301*/
302
303void OpcUaConnection::connectToEndpoint(const QOpcUaEndpointDescription &endpointDescription)
304{
305 if (!m_client)
306 return;
307
308 m_client->connectToEndpoint(endpoint: endpointDescription);
309}
310
311/*!
312 \qmlmethod Connection::disconnectFromEndpoint()
313
314 Disconnects an established connection.
315*/
316
317void OpcUaConnection::disconnectFromEndpoint()
318{
319 if (!m_client)
320 return;
321
322 m_client->disconnectFromEndpoint();
323}
324
325void OpcUaConnection::setDefaultConnection(bool defaultConnection)
326{
327 if (!defaultConnection && m_defaultConnection == this)
328 m_defaultConnection = nullptr;
329
330 if (defaultConnection)
331 m_defaultConnection = this;
332
333 emit defaultConnectionChanged();
334}
335
336void OpcUaConnection::clientStateHandler(QOpcUaClient::ClientState state)
337{
338 if (m_connected) {
339 // don't immediately send the state; we have to wait for the namespace
340 // array to be updated
341 m_connected = (state == QOpcUaClient::ClientState::Connected);
342 emit connectedChanged();
343 }
344}
345
346QStringList OpcUaConnection::namespaces() const
347{
348 if (!m_client)
349 return QStringList();
350
351 return m_client->namespaceArray();
352}
353
354QOpcUaEndpointDescription OpcUaConnection::currentEndpoint() const
355{
356 if (!m_client || !m_connected)
357 return QOpcUaEndpointDescription();
358
359 return m_client->endpoint();
360}
361
362void OpcUaConnection::setAuthenticationInformation(const QOpcUaAuthenticationInformation &authenticationInformation)
363{
364 if (!m_client)
365 return;
366 m_client->setAuthenticationInformation(authenticationInformation);
367}
368
369void OpcUaConnection::setConnection(QOpcUaClient *client)
370{
371 if (!client)
372 return;
373 removeConnection();
374 m_client = client;
375 setupConnection();
376}
377
378QOpcUaAuthenticationInformation OpcUaConnection::authenticationInformation() const
379{
380 if (!m_client)
381 return QOpcUaAuthenticationInformation();
382
383 return m_client->authenticationInformation();
384}
385
386/*!
387 \qmlmethod Connection::readNodeAttributes(valuesToBeRead)
388
389 This function is used to read multiple values from a server in one go.
390 Returns \c true if the read request was dispatched successfully.
391
392 The \a valuesToBeRead parameter must be a JavaScript array of \l ReadItem
393 entries.
394
395 \code
396 // List of items to read
397 var readItemList = [];
398 // Item to be added to the list of items to be read
399 var readItem;
400
401 // Prepare an item to be read
402
403 // Create a new read item and fill properties
404 readItem = QtOpcUa.ReadItem.create();
405 readItem.ns = "http://qt-project.org";
406 readItem.nodeId = "s=Demo.Static.Scalar.Double";
407 readItem.attribute = QtOpcUa.Constants.NodeAttribute.DisplayName;
408
409 // Add the prepared item to the list of items to be read
410 readItemList.push(readItem);
411
412 // Add further items
413 [...]
414
415 if (!connection.readNodeAttributes(readItemList)) {
416 // handle error
417 }
418 \endcode
419
420 The result of the read request are provided by the signal
421 \l readNodeAttributesFinished().
422
423 \sa readNodeAttributesFinished(), ReadItem
424*/
425bool OpcUaConnection::readNodeAttributes(const QJSValue &value)
426{
427 if (!m_client || !m_connected) {
428 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Not connected to server.");
429 return false;
430 }
431 if (!value.isArray()) {
432 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "List of ReadItems it not an array.");
433 return false;
434 }
435
436 QList<QOpcUaReadItem> readItemList;
437
438 for (int i = 0, end = value.property(QStringLiteral("length")).toInt(); i < end; ++i){
439 const auto &readItem = qjsvalue_cast<OpcUaReadItem>(value: value.property(arrayIndex: i));
440 if (readItem.nodeId().isEmpty()) {
441 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Invalid ReadItem in list of items at index %1").arg(a: i);
442 return false;
443 }
444
445 QString finalNode;
446 bool ok;
447 int index = readItem.namespaceIdentifier().toInt(ok: &ok);
448 if (ok) {
449 QString identifier;
450 UniversalNode::splitNodeIdAndNamespace(nodeIdentifier: readItem.nodeId(), namespaceIndex: nullptr, identifier: &identifier);
451 finalNode = UniversalNode::createNodeString(namespaceIndex: index, nodeIdentifier: identifier);
452 } else {
453 finalNode = UniversalNode::resolveNamespaceToNode(nodeId: readItem.nodeId(), namespaceName: readItem.namespaceIdentifier().toString(), client: m_client);
454 }
455
456 if (finalNode.isEmpty()) {
457 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Failed to resolve node.");
458 return false;
459 }
460 readItemList.push_back(t: QOpcUaReadItem(finalNode,
461 readItem.attribute(),
462 readItem.indexRange())
463 );
464 }
465
466 return m_client->readNodeAttributes(nodesToRead: readItemList);
467}
468
469/*!
470 \qmlmethod Connection::writeNodeAttributes(valuesToBeWritten)
471
472 This function is used to write multiple values to a server in one go.
473 Returns \c true if the write request was dispatched successfully.
474
475 The \a valuesToBeWritten parameter must be a JavaScript array of
476 \l WriteItem entries.
477
478 \code
479 // List of items to write
480 var writeItemList = [];
481 // Item to be added to the list of items to be written
482 var writeItem;
483
484 // Prepare an item to be written
485
486 // Create a new write item and fill properties
487 writeItem = QtOpcUa.WriteItem.create();
488 writeItem.ns = "http://qt-project.org";
489 writeItem.nodeId = "s=Demo.Static.Scalar.Double";
490 writeItem.attribute = QtOpcUa.Constants.NodeAttribute.Value;
491 writeItem.value = 32.1;
492 writeItem.valueType = QtOpcUa.Constants.Double;
493
494 // Add the prepared item to the list of items to be written
495 writeItemList.push(writeItem);
496
497 // Add further items
498 [...]
499
500 if (!connection.writeNodeAttributes(writeItemList)) {
501 // handle error
502 }
503 \endcode
504
505 The result of the write request are provided by the signal
506 \l Connection::writeNodeAttributesFinished().
507
508 \sa Connection::writeNodeAttributesFinished() WriteItem
509*/
510bool OpcUaConnection::writeNodeAttributes(const QJSValue &value)
511{
512 if (!m_client || !m_connected) {
513 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Not connected to server.");
514 return false;
515 }
516 if (!value.isArray()) {
517 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "List of WriteItems it not an array.");
518 return false;
519 }
520
521 QList<QOpcUaWriteItem> writeItemList;
522
523 for (int i = 0, end = value.property(QStringLiteral("length")).toInt(); i < end; ++i) {
524 const auto &writeItem = qjsvalue_cast<OpcUaWriteItem>(value: value.property(arrayIndex: i));
525 if (writeItem.nodeId().isEmpty()) {
526 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Invalid WriteItem in list of items at index %1").arg(a: i);
527 return false;
528 }
529
530 QString finalNode;
531 bool ok;
532 int index = writeItem.namespaceIdentifier().toInt(ok: &ok);
533 if (ok) {
534 QString identifier;
535 UniversalNode::splitNodeIdAndNamespace(nodeIdentifier: writeItem.nodeId(), namespaceIndex: nullptr, identifier: &identifier);
536 finalNode = UniversalNode::createNodeString(namespaceIndex: index, nodeIdentifier: identifier);
537 } else {
538 finalNode = UniversalNode::resolveNamespaceToNode(nodeId: writeItem.nodeId(), namespaceName: writeItem.namespaceIdentifier().toString(), client: m_client);
539 }
540
541 if (finalNode.isEmpty()) {
542 qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Failed to resolve node.");
543 return false;
544 }
545
546 auto tmp = QOpcUaWriteItem(finalNode,
547 writeItem.attribute(),
548 writeItem.value(),
549 writeItem.valueType(),
550 writeItem.indexRange());
551
552 tmp.setSourceTimestamp(writeItem.sourceTimestamp());
553 tmp.setServerTimestamp(writeItem.serverTimestamp());
554 if (writeItem.hasStatusCode())
555 tmp.setStatusCode(static_cast<QOpcUa::UaStatusCode>(writeItem.statusCode()));
556 writeItemList.push_back(t: tmp);
557 }
558
559 return m_client->writeNodeAttributes(nodesToWrite: writeItemList);
560}
561
562QStringList OpcUaConnection::supportedSecurityPolicies() const
563{
564 if (!m_client)
565 return QStringList();
566 return m_client->supportedSecurityPolicies();
567}
568
569QJSValue OpcUaConnection::supportedUserTokenTypes() const
570{
571 if (!m_client)
572 return QJSValue();
573
574 auto engine = qjsEngine(this);
575 if (!engine)
576 return QJSValue();
577
578 const auto tokenTypes = m_client->supportedUserTokenTypes();
579 auto returnValue = engine->newArray(length: tokenTypes.size());
580 for (int i = 0; i < tokenTypes.size(); ++i)
581 returnValue.setProperty(arrayIndex: i, value: tokenTypes[i]);
582
583 return returnValue;
584}
585
586QOpcUaClient *OpcUaConnection::connection() const
587{
588 return m_client;
589}
590
591void OpcUaConnection::handleReadNodeAttributesFinished(const QList<QOpcUaReadResult> &results)
592{
593 QVariantList returnValue;
594
595 for (const auto &result : results)
596 returnValue.append(t: QVariant::fromValue(value: OpcUaReadResult(result, m_client)));
597
598 emit readNodeAttributesFinished(value: QVariant::fromValue(value: returnValue));
599}
600
601void OpcUaConnection::handleWriteNodeAttributesFinished(const QList<QOpcUaWriteResult> &results)
602{
603 QVariantList returnValue;
604
605 for (const auto &result : results)
606 returnValue.append(t: QVariant::fromValue(value: OpcUaWriteResult(result, m_client)));
607
608 emit writeNodeAttributesFinished(value: QVariant::fromValue(value: returnValue));
609}
610
611void OpcUaConnection::removeConnection()
612{
613 if (m_client) {
614 m_client->disconnect(receiver: this);
615 m_client->disconnectFromEndpoint();
616 if (!m_client->parent()) {
617 m_client->deleteLater();
618 }
619 m_client = nullptr;
620 }
621}
622
623void OpcUaConnection::setupConnection()
624{
625 connect(sender: m_client, signal: &QOpcUaClient::stateChanged, context: this, slot: &OpcUaConnection::clientStateHandler);
626 connect(sender: m_client, signal: &QOpcUaClient::namespaceArrayUpdated, context: this, slot: &OpcUaConnection::namespacesChanged);
627 connect(sender: m_client, signal: &QOpcUaClient::namespaceArrayUpdated, context: this, slot: [&]() {
628 if (!m_connected) {
629 m_connected = true;
630 emit connectedChanged();
631 }
632 });
633 m_client->setNamespaceAutoupdate(true);
634 connect(sender: m_client, signal: &QOpcUaClient::readNodeAttributesFinished, context: this, slot: &OpcUaConnection::handleReadNodeAttributesFinished);
635 connect(sender: m_client, signal: &QOpcUaClient::writeNodeAttributesFinished, context: this, slot: &OpcUaConnection::handleWriteNodeAttributesFinished);
636 m_connected = (!m_client->namespaceArray().isEmpty() && m_client->state() == QOpcUaClient::Connected);
637 if (m_connected)
638 emit connectedChanged();
639}
640
641QT_END_NAMESPACE
642

source code of qtopcua/src/declarative_opcua/opcuaconnection.cpp