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 | |
17 | QT_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 | |
223 | Q_DECLARE_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_QML) |
224 | |
225 | OpcUaConnection* OpcUaConnection::m_defaultConnection = nullptr; |
226 | |
227 | OpcUaConnection::OpcUaConnection(QObject *parent): |
228 | QObject(parent) |
229 | { |
230 | } |
231 | |
232 | OpcUaConnection::~OpcUaConnection() |
233 | { |
234 | setDefaultConnection(false); |
235 | removeConnection(); |
236 | } |
237 | |
238 | QStringList OpcUaConnection::availableBackends() const |
239 | { |
240 | return QOpcUaProvider::availableBackends(); |
241 | } |
242 | |
243 | bool OpcUaConnection::connected() const |
244 | { |
245 | return m_connected && m_client; |
246 | } |
247 | |
248 | void 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 | |
277 | QString OpcUaConnection::backend() const |
278 | { |
279 | if (m_client) |
280 | return m_client->backend(); |
281 | else |
282 | return QString(); |
283 | } |
284 | |
285 | OpcUaConnection *OpcUaConnection::defaultConnection() |
286 | { |
287 | return m_defaultConnection; |
288 | } |
289 | |
290 | bool 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 | |
303 | void 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 | |
317 | void OpcUaConnection::disconnectFromEndpoint() |
318 | { |
319 | if (!m_client) |
320 | return; |
321 | |
322 | m_client->disconnectFromEndpoint(); |
323 | } |
324 | |
325 | void 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 | |
336 | void 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 | |
346 | QStringList OpcUaConnection::namespaces() const |
347 | { |
348 | if (!m_client) |
349 | return QStringList(); |
350 | |
351 | return m_client->namespaceArray(); |
352 | } |
353 | |
354 | QOpcUaEndpointDescription OpcUaConnection::currentEndpoint() const |
355 | { |
356 | if (!m_client || !m_connected) |
357 | return QOpcUaEndpointDescription(); |
358 | |
359 | return m_client->endpoint(); |
360 | } |
361 | |
362 | void OpcUaConnection::setAuthenticationInformation(const QOpcUaAuthenticationInformation &authenticationInformation) |
363 | { |
364 | if (!m_client) |
365 | return; |
366 | m_client->setAuthenticationInformation(authenticationInformation); |
367 | } |
368 | |
369 | void OpcUaConnection::setConnection(QOpcUaClient *client) |
370 | { |
371 | if (!client) |
372 | return; |
373 | removeConnection(); |
374 | m_client = client; |
375 | setupConnection(); |
376 | } |
377 | |
378 | QOpcUaAuthenticationInformation 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 | */ |
425 | bool 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 | */ |
510 | bool 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 | |
562 | QStringList OpcUaConnection::supportedSecurityPolicies() const |
563 | { |
564 | if (!m_client) |
565 | return QStringList(); |
566 | return m_client->supportedSecurityPolicies(); |
567 | } |
568 | |
569 | QJSValue 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 | |
586 | QOpcUaClient *OpcUaConnection::connection() const |
587 | { |
588 | return m_client; |
589 | } |
590 | |
591 | void 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 | |
601 | void 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 | |
611 | void 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 | |
623 | void 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 | |
641 | QT_END_NAMESPACE |
642 | |