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 | if (m_client) { |
236 | m_client->disconnect(receiver: this); |
237 | delete m_client; |
238 | } |
239 | } |
240 | |
241 | QStringList OpcUaConnection::availableBackends() const |
242 | { |
243 | return QOpcUaProvider::availableBackends(); |
244 | } |
245 | |
246 | bool OpcUaConnection::connected() const |
247 | { |
248 | return m_connected && m_client; |
249 | } |
250 | |
251 | void OpcUaConnection::setBackend(const QString &name) |
252 | { |
253 | if (name.isEmpty()) |
254 | return; |
255 | |
256 | if (!availableBackends().contains(str: name)) { |
257 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Backend '%1' is not available").arg(a: name); |
258 | qCDebug(QT_OPCUA_PLUGINS_QML) << tr(s: "Available backends:") << availableBackends().join(sep: QLatin1Char(',')); |
259 | return; |
260 | } |
261 | |
262 | if (m_client) { |
263 | if (m_client->backend() == name) |
264 | return; |
265 | |
266 | removeConnection(); |
267 | } |
268 | |
269 | QOpcUaProvider provider; |
270 | m_client = provider.createClient(backend: name); |
271 | if (m_client) { |
272 | qCDebug(QT_OPCUA_PLUGINS_QML) << "Created plugin"<< m_client->backend(); |
273 | setupConnection(); |
274 | } else { |
275 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Backend '%1' could not be created.").arg(a: name); |
276 | } |
277 | emit backendChanged(); |
278 | } |
279 | |
280 | QString OpcUaConnection::backend() const |
281 | { |
282 | if (m_client) |
283 | return m_client->backend(); |
284 | else |
285 | return QString(); |
286 | } |
287 | |
288 | OpcUaConnection *OpcUaConnection::defaultConnection() |
289 | { |
290 | return m_defaultConnection; |
291 | } |
292 | |
293 | bool OpcUaConnection::isDefaultConnection() const |
294 | { |
295 | return m_defaultConnection == this; |
296 | } |
297 | |
298 | /*! |
299 | \qmlmethod Connection::connectToEndpoint(endpointDescription) |
300 | |
301 | Connects to the endpoint specified with \a endpointDescription. |
302 | |
303 | \sa EndpointDescription |
304 | */ |
305 | |
306 | void OpcUaConnection::connectToEndpoint(const QOpcUaEndpointDescription &endpointDescription) |
307 | { |
308 | if (!m_client) |
309 | return; |
310 | |
311 | m_client->connectToEndpoint(endpoint: endpointDescription); |
312 | } |
313 | |
314 | /*! |
315 | \qmlmethod Connection::disconnectFromEndpoint() |
316 | |
317 | Disconnects an established connection. |
318 | */ |
319 | |
320 | void OpcUaConnection::disconnectFromEndpoint() |
321 | { |
322 | if (!m_client) |
323 | return; |
324 | |
325 | m_client->disconnectFromEndpoint(); |
326 | } |
327 | |
328 | void OpcUaConnection::setDefaultConnection(bool defaultConnection) |
329 | { |
330 | if (!defaultConnection && m_defaultConnection == this) |
331 | m_defaultConnection = nullptr; |
332 | |
333 | if (defaultConnection) |
334 | m_defaultConnection = this; |
335 | |
336 | emit defaultConnectionChanged(); |
337 | } |
338 | |
339 | void OpcUaConnection::clientStateHandler(QOpcUaClient::ClientState state) |
340 | { |
341 | if (m_connected) { |
342 | // don't immediately send the state; we have to wait for the namespace |
343 | // array to be updated |
344 | m_connected = (state == QOpcUaClient::ClientState::Connected); |
345 | emit connectedChanged(); |
346 | } |
347 | } |
348 | |
349 | QStringList OpcUaConnection::namespaces() const |
350 | { |
351 | if (!m_client) |
352 | return QStringList(); |
353 | |
354 | return m_client->namespaceArray(); |
355 | } |
356 | |
357 | QOpcUaEndpointDescription OpcUaConnection::currentEndpoint() const |
358 | { |
359 | if (!m_client || !m_connected) |
360 | return QOpcUaEndpointDescription(); |
361 | |
362 | return m_client->endpoint(); |
363 | } |
364 | |
365 | void OpcUaConnection::setAuthenticationInformation(const QOpcUaAuthenticationInformation &authenticationInformation) |
366 | { |
367 | if (!m_client) |
368 | return; |
369 | m_client->setAuthenticationInformation(authenticationInformation); |
370 | } |
371 | |
372 | void OpcUaConnection::setConnection(QOpcUaClient *client) |
373 | { |
374 | if (!client) |
375 | return; |
376 | removeConnection(); |
377 | m_client = client; |
378 | m_client->setParent(nullptr); |
379 | setupConnection(); |
380 | } |
381 | |
382 | QOpcUaAuthenticationInformation OpcUaConnection::authenticationInformation() const |
383 | { |
384 | if (!m_client) |
385 | return QOpcUaAuthenticationInformation(); |
386 | |
387 | return m_client->authenticationInformation(); |
388 | } |
389 | |
390 | /*! |
391 | \qmlmethod Connection::readNodeAttributes(valuesToBeRead) |
392 | |
393 | This function is used to read multiple values from a server in one go. |
394 | Returns \c true if the read request was dispatched successfully. |
395 | |
396 | The \a valuesToBeRead parameter must be a JavaScript array of \l ReadItem |
397 | entries. |
398 | |
399 | \code |
400 | // List of items to read |
401 | var readItemList = []; |
402 | // Item to be added to the list of items to be read |
403 | var readItem; |
404 | |
405 | // Prepare an item to be read |
406 | |
407 | // Create a new read item and fill properties |
408 | readItem = QtOpcUa.ReadItem.create(); |
409 | readItem.ns = "http://qt-project.org"; |
410 | readItem.nodeId = "s=Demo.Static.Scalar.Double"; |
411 | readItem.attribute = QtOpcUa.Constants.NodeAttribute.DisplayName; |
412 | |
413 | // Add the prepared item to the list of items to be read |
414 | readItemList.push(readItem); |
415 | |
416 | // Add further items |
417 | [...] |
418 | |
419 | if (!connection.readNodeAttributes(readItemList)) { |
420 | // handle error |
421 | } |
422 | \endcode |
423 | |
424 | The result of the read request are provided by the signal |
425 | \l readNodeAttributesFinished(). |
426 | |
427 | \sa readNodeAttributesFinished(), ReadItem |
428 | */ |
429 | bool OpcUaConnection::readNodeAttributes(const QJSValue &value) |
430 | { |
431 | if (!m_client || !m_connected) { |
432 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Not connected to server."); |
433 | return false; |
434 | } |
435 | if (!value.isArray()) { |
436 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "List of ReadItems it not an array."); |
437 | return false; |
438 | } |
439 | |
440 | QList<QOpcUaReadItem> readItemList; |
441 | |
442 | for (int i = 0, end = value.property(QStringLiteral("length")).toInt(); i < end; ++i){ |
443 | const auto &readItem = qjsvalue_cast<OpcUaReadItem>(value: value.property(arrayIndex: i)); |
444 | if (readItem.nodeId().isEmpty()) { |
445 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Invalid ReadItem in list of items at index %1").arg(a: i); |
446 | return false; |
447 | } |
448 | |
449 | QString finalNode; |
450 | bool ok; |
451 | int index = readItem.namespaceIdentifier().toInt(ok: &ok); |
452 | if (ok) { |
453 | QString identifier; |
454 | UniversalNode::splitNodeIdAndNamespace(nodeIdentifier: readItem.nodeId(), namespaceIndex: nullptr, identifier: &identifier); |
455 | finalNode = UniversalNode::createNodeString(namespaceIndex: index, nodeIdentifier: identifier); |
456 | } else { |
457 | finalNode = UniversalNode::resolveNamespaceToNode(nodeId: readItem.nodeId(), namespaceName: readItem.namespaceIdentifier().toString(), client: m_client); |
458 | } |
459 | |
460 | if (finalNode.isEmpty()) { |
461 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Failed to resolve node."); |
462 | return false; |
463 | } |
464 | readItemList.push_back(t: QOpcUaReadItem(finalNode, |
465 | readItem.attribute(), |
466 | readItem.indexRange()) |
467 | ); |
468 | } |
469 | |
470 | return m_client->readNodeAttributes(nodesToRead: readItemList); |
471 | } |
472 | |
473 | /*! |
474 | \qmlmethod Connection::writeNodeAttributes(valuesToBeWritten) |
475 | |
476 | This function is used to write multiple values to a server in one go. |
477 | Returns \c true if the write request was dispatched successfully. |
478 | |
479 | The \a valuesToBeWritten parameter must be a JavaScript array of |
480 | \l WriteItem entries. |
481 | |
482 | \code |
483 | // List of items to write |
484 | var writeItemList = []; |
485 | // Item to be added to the list of items to be written |
486 | var writeItem; |
487 | |
488 | // Prepare an item to be written |
489 | |
490 | // Create a new write item and fill properties |
491 | writeItem = QtOpcUa.WriteItem.create(); |
492 | writeItem.ns = "http://qt-project.org"; |
493 | writeItem.nodeId = "s=Demo.Static.Scalar.Double"; |
494 | writeItem.attribute = QtOpcUa.Constants.NodeAttribute.Value; |
495 | writeItem.value = 32.1; |
496 | writeItem.valueType = QtOpcUa.Constants.Double; |
497 | |
498 | // Add the prepared item to the list of items to be written |
499 | writeItemList.push(writeItem); |
500 | |
501 | // Add further items |
502 | [...] |
503 | |
504 | if (!connection.writeNodeAttributes(writeItemList)) { |
505 | // handle error |
506 | } |
507 | \endcode |
508 | |
509 | The result of the write request are provided by the signal |
510 | \l Connection::writeNodeAttributesFinished(). |
511 | |
512 | \sa Connection::writeNodeAttributesFinished() WriteItem |
513 | */ |
514 | bool OpcUaConnection::writeNodeAttributes(const QJSValue &value) |
515 | { |
516 | if (!m_client || !m_connected) { |
517 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Not connected to server."); |
518 | return false; |
519 | } |
520 | if (!value.isArray()) { |
521 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "List of WriteItems it not an array."); |
522 | return false; |
523 | } |
524 | |
525 | QList<QOpcUaWriteItem> writeItemList; |
526 | |
527 | for (int i = 0, end = value.property(QStringLiteral("length")).toInt(); i < end; ++i) { |
528 | const auto &writeItem = qjsvalue_cast<OpcUaWriteItem>(value: value.property(arrayIndex: i)); |
529 | if (writeItem.nodeId().isEmpty()) { |
530 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Invalid WriteItem in list of items at index %1").arg(a: i); |
531 | return false; |
532 | } |
533 | |
534 | QString finalNode; |
535 | bool ok; |
536 | int index = writeItem.namespaceIdentifier().toInt(ok: &ok); |
537 | if (ok) { |
538 | QString identifier; |
539 | UniversalNode::splitNodeIdAndNamespace(nodeIdentifier: writeItem.nodeId(), namespaceIndex: nullptr, identifier: &identifier); |
540 | finalNode = UniversalNode::createNodeString(namespaceIndex: index, nodeIdentifier: identifier); |
541 | } else { |
542 | finalNode = UniversalNode::resolveNamespaceToNode(nodeId: writeItem.nodeId(), namespaceName: writeItem.namespaceIdentifier().toString(), client: m_client); |
543 | } |
544 | |
545 | if (finalNode.isEmpty()) { |
546 | qCWarning(QT_OPCUA_PLUGINS_QML) << tr(s: "Failed to resolve node."); |
547 | return false; |
548 | } |
549 | |
550 | auto tmp = QOpcUaWriteItem(finalNode, |
551 | writeItem.attribute(), |
552 | writeItem.value(), |
553 | writeItem.valueType(), |
554 | writeItem.indexRange()); |
555 | |
556 | tmp.setSourceTimestamp(writeItem.sourceTimestamp()); |
557 | tmp.setServerTimestamp(writeItem.serverTimestamp()); |
558 | if (writeItem.hasStatusCode()) |
559 | tmp.setStatusCode(static_cast<QOpcUa::UaStatusCode>(writeItem.statusCode())); |
560 | writeItemList.push_back(t: tmp); |
561 | } |
562 | |
563 | return m_client->writeNodeAttributes(nodesToWrite: writeItemList); |
564 | } |
565 | |
566 | QStringList OpcUaConnection::supportedSecurityPolicies() const |
567 | { |
568 | if (!m_client) |
569 | return QStringList(); |
570 | return m_client->supportedSecurityPolicies(); |
571 | } |
572 | |
573 | QJSValue OpcUaConnection::supportedUserTokenTypes() const |
574 | { |
575 | if (!m_client) |
576 | return QJSValue(); |
577 | |
578 | auto engine = qjsEngine(this); |
579 | if (!engine) |
580 | return QJSValue(); |
581 | |
582 | const auto tokenTypes = m_client->supportedUserTokenTypes(); |
583 | auto returnValue = engine->newArray(length: tokenTypes.size()); |
584 | for (int i = 0; i < tokenTypes.size(); ++i) |
585 | returnValue.setProperty(arrayIndex: i, value: tokenTypes[i]); |
586 | |
587 | return returnValue; |
588 | } |
589 | |
590 | QOpcUaClient *OpcUaConnection::connection() const |
591 | { |
592 | return m_client; |
593 | } |
594 | |
595 | void OpcUaConnection::handleReadNodeAttributesFinished(const QList<QOpcUaReadResult> &results) |
596 | { |
597 | QVariantList returnValue; |
598 | |
599 | for (const auto &result : results) |
600 | returnValue.append(t: QVariant::fromValue(value: OpcUaReadResult(result, m_client))); |
601 | |
602 | emit readNodeAttributesFinished(value: QVariant::fromValue(value: returnValue)); |
603 | } |
604 | |
605 | void OpcUaConnection::handleWriteNodeAttributesFinished(const QList<QOpcUaWriteResult> &results) |
606 | { |
607 | QVariantList returnValue; |
608 | |
609 | for (const auto &result : results) |
610 | returnValue.append(t: QVariant::fromValue(value: OpcUaWriteResult(result, m_client))); |
611 | |
612 | emit writeNodeAttributesFinished(value: QVariant::fromValue(value: returnValue)); |
613 | } |
614 | |
615 | void OpcUaConnection::removeConnection() |
616 | { |
617 | if (m_client) { |
618 | m_client->disconnect(receiver: this); |
619 | m_client->disconnectFromEndpoint(); |
620 | m_client->deleteLater(); |
621 | m_client = nullptr; |
622 | } |
623 | } |
624 | |
625 | void OpcUaConnection::setupConnection() |
626 | { |
627 | connect(sender: m_client, signal: &QOpcUaClient::stateChanged, context: this, slot: &OpcUaConnection::clientStateHandler); |
628 | connect(sender: m_client, signal: &QOpcUaClient::namespaceArrayUpdated, context: this, slot: &OpcUaConnection::namespacesChanged); |
629 | connect(sender: m_client, signal: &QOpcUaClient::namespaceArrayUpdated, context: this, slot: [&]() { |
630 | if (!m_connected) { |
631 | m_connected = true; |
632 | emit connectedChanged(); |
633 | } |
634 | }); |
635 | m_client->setNamespaceAutoupdate(true); |
636 | connect(sender: m_client, signal: &QOpcUaClient::readNodeAttributesFinished, context: this, slot: &OpcUaConnection::handleReadNodeAttributesFinished); |
637 | connect(sender: m_client, signal: &QOpcUaClient::writeNodeAttributesFinished, context: this, slot: &OpcUaConnection::handleWriteNodeAttributesFinished); |
638 | m_connected = (!m_client->namespaceArray().isEmpty() && m_client->state() == QOpcUaClient::Connected); |
639 | if (m_connected) |
640 | emit connectedChanged(); |
641 | } |
642 | |
643 | QT_END_NAMESPACE |
644 |
Definitions
- m_defaultConnection
- OpcUaConnection
- ~OpcUaConnection
- availableBackends
- connected
- setBackend
- backend
- defaultConnection
- isDefaultConnection
- connectToEndpoint
- disconnectFromEndpoint
- setDefaultConnection
- clientStateHandler
- namespaces
- currentEndpoint
- setAuthenticationInformation
- setConnection
- authenticationInformation
- readNodeAttributes
- writeNodeAttributes
- supportedSecurityPolicies
- supportedUserTokenTypes
- connection
- handleReadNodeAttributesFinished
- handleWriteNodeAttributesFinished
- removeConnection
Learn Advanced QML with KDAB
Find out more