1 | // Copyright (C) 2018 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/opcuanode_p.h> |
5 | #include <private/opcuarelativenodepath_p.h> |
6 | #include <private/opcuarelativenodeid_p.h> |
7 | #include <private/opcuapathresolver_p.h> |
8 | #include <private/opcuaattributevalue_p.h> |
9 | |
10 | #include <qopcuatype.h> |
11 | #include <QOpcUaNode> |
12 | #include <QOpcUaClient> |
13 | #include <QLoggingCategory> |
14 | |
15 | QT_BEGIN_NAMESPACE |
16 | |
17 | /*! |
18 | \qmltype Node |
19 | \inqmlmodule QtOpcUa |
20 | \brief Represents a node on a server. |
21 | \since QtOpcUa 5.12 |
22 | |
23 | \code |
24 | import QtOpcUa as QtOpcUa |
25 | |
26 | QtOpcUa.Node { |
27 | nodeId : QtOpcUa.NodeId { |
28 | identifier: "s=Example.Method" |
29 | ns: "Example Namespace" |
30 | } |
31 | connection: myConnection |
32 | } |
33 | \endcode |
34 | */ |
35 | |
36 | /*! |
37 | \qmlproperty NodeId Node::nodeId |
38 | |
39 | ID of the node on the server to be used. |
40 | This can be an absolute node ID or a relative node ID. |
41 | |
42 | \sa NodeId, RelativeNodeId |
43 | */ |
44 | |
45 | /*! |
46 | \qmlproperty Connection Node::connection |
47 | |
48 | The connection to be used for node instances. |
49 | The node will automatically be accessible when the associated connection |
50 | has established a connection to a server. |
51 | |
52 | If this property is not set, the default connection will be used, if any. |
53 | |
54 | \sa Connection, Connection::defaultConnection |
55 | */ |
56 | |
57 | /*! |
58 | \qmlproperty bool Node::readyToUse |
59 | \readonly |
60 | |
61 | This property returns whether the node is ready to use. |
62 | This happens once after a successful connection to a server was established |
63 | and the node was successfully set up. |
64 | */ |
65 | |
66 | /*! |
67 | \qmlsignal Connection::nodeChanged() |
68 | |
69 | Emitted when the underlying node has changed. |
70 | This happens when the namespace or identifier of the \l NodeId changed. |
71 | */ |
72 | |
73 | /*! |
74 | \qmlproperty QOpcUa::NodeClass Node::nodeClass |
75 | \readonly |
76 | |
77 | The node class of the node. In case the information is not available |
78 | \c QtOpcUa.Constants.NodeClass.Undefined is returned. |
79 | */ |
80 | |
81 | /*! |
82 | \qmlproperty string Node::browseName |
83 | |
84 | The browse name of the node. In case the information is not available |
85 | an empty string is returned. |
86 | */ |
87 | |
88 | /*! |
89 | \qmlproperty LocalizedText Node::displayName |
90 | |
91 | The localized text of the node. In case the information is not available |
92 | a default constructed \l LocalizedText is returned. |
93 | */ |
94 | |
95 | /*! |
96 | \qmlproperty LocalizedText Node::description |
97 | |
98 | The description of the node. In case the information is not available |
99 | a default constructed \l LocalizedText is returned. |
100 | */ |
101 | |
102 | /*! |
103 | \qmlproperty Status Node::status |
104 | \readonly |
105 | |
106 | Current status of the node. The node is only usable when the status is \c Valid. |
107 | In any other case it indicates an error. |
108 | |
109 | \sa errorMessage, Status |
110 | */ |
111 | |
112 | /*! |
113 | \qmlproperty string Node::errorMessage |
114 | \readonly |
115 | |
116 | A string representation of the current status code. |
117 | |
118 | \sa status, Status |
119 | */ |
120 | |
121 | /*! |
122 | \qmlproperty enumeration Node::Status |
123 | |
124 | Status of a node. |
125 | |
126 | \value Node.Status.Valid Node is ready for use |
127 | \value Node.Status.InvalidNodeId Node id is invalid |
128 | \value Node.Status.NoConnection Not connected to a server |
129 | \value Node.Status.InvalidNodeType Node type on the server does not match the QML type |
130 | \value Node.Status.InvalidClient Invalid connection client |
131 | \value Node.Status.FailedToResolveNode Failed to resolve node |
132 | \value Node.Status.InvalidObjectNode The object node is invalid or its type does not match the expected type |
133 | \value Node.Status.FailedToReadAttributes Failed to read attributes |
134 | \value Node.Status.FailedToSetupMonitoring Failed to setup monitoring |
135 | |
136 | \sa status, errorMessage |
137 | */ |
138 | |
139 | /*! |
140 | \qmlproperty EventFilter Node::eventFilter |
141 | \since 5.13 |
142 | |
143 | An event filter allows monitoring events on the server for certain conditions. |
144 | |
145 | \sa EventFilter |
146 | */ |
147 | |
148 | Q_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_QML, "qt.opcua.plugins.qml" ) |
149 | |
150 | OpcUaNode::OpcUaNode(QObject *parent): |
151 | QObject(parent), |
152 | m_nodeId(new OpcUaNodeIdType(this)), |
153 | m_attributesToRead(QOpcUaNode::mandatoryBaseAttributes()), |
154 | m_status(Status::InvalidNodeId) |
155 | { |
156 | m_attributesToRead |= QOpcUa::NodeAttribute::Description; |
157 | connect(sender: &m_resolvedNode, signal: &UniversalNode::nodeChanged, context: this, slot: &OpcUaNode::nodeChanged); |
158 | connect(sender: m_attributeCache.attribute(attribute: QOpcUa::NodeAttribute::BrowseName), signal: &OpcUaAttributeValue::changed, context: this, slot: &OpcUaNode::browseNameChanged); |
159 | connect(sender: m_attributeCache.attribute(attribute: QOpcUa::NodeAttribute::NodeClass), signal: &OpcUaAttributeValue::changed, context: this, slot: &OpcUaNode::nodeClassChanged); |
160 | connect(sender: m_attributeCache.attribute(attribute: QOpcUa::NodeAttribute::DisplayName), signal: &OpcUaAttributeValue::changed, context: this, slot: &OpcUaNode::displayNameChanged); |
161 | connect(sender: m_attributeCache.attribute(attribute: QOpcUa::NodeAttribute::Description), signal: &OpcUaAttributeValue::changed, context: this, slot: &OpcUaNode::descriptionChanged); |
162 | } |
163 | |
164 | OpcUaNode::~OpcUaNode() |
165 | { |
166 | delete m_node; |
167 | } |
168 | |
169 | OpcUaNodeIdType *OpcUaNode::nodeId() const |
170 | { |
171 | return m_nodeId; |
172 | } |
173 | |
174 | OpcUaConnection *OpcUaNode::connection() |
175 | { |
176 | if (!m_connection) |
177 | setConnection(OpcUaConnection::defaultConnection()); |
178 | |
179 | return m_connection; |
180 | } |
181 | |
182 | bool OpcUaNode::readyToUse() const |
183 | { |
184 | return m_readyToUse; |
185 | } |
186 | |
187 | void OpcUaNode::setBrowseName(const QString &value) |
188 | { |
189 | if (!m_connection || !m_node) |
190 | return; |
191 | if (!m_resolvedNode.isNamespaceIndexValid()) |
192 | return; |
193 | |
194 | m_node->writeAttribute(attribute: QOpcUa::NodeAttribute::BrowseName, value: QOpcUaQualifiedName(m_resolvedNode.namespaceIndex(), value)); |
195 | } |
196 | |
197 | QString OpcUaNode::browseName() |
198 | { |
199 | return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>().name(); |
200 | } |
201 | |
202 | QOpcUa::NodeClass OpcUaNode::nodeClass() |
203 | { |
204 | return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>(); |
205 | } |
206 | |
207 | void OpcUaNode::setDisplayName(const QOpcUaLocalizedText &value) |
208 | { |
209 | if (!m_connection || !m_node) |
210 | return; |
211 | m_node->writeAttribute(attribute: QOpcUa::NodeAttribute::DisplayName, value); |
212 | } |
213 | |
214 | QOpcUaLocalizedText OpcUaNode::displayName() |
215 | { |
216 | return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>(); |
217 | } |
218 | |
219 | void OpcUaNode::setDescription(const QOpcUaLocalizedText &value) |
220 | { |
221 | if (!m_connection || !m_node) |
222 | return; |
223 | m_node->writeAttribute(attribute: QOpcUa::NodeAttribute::Description, value); |
224 | } |
225 | |
226 | QOpcUaLocalizedText OpcUaNode::description() |
227 | { |
228 | return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>(); |
229 | } |
230 | |
231 | OpcUaNode::Status OpcUaNode::status() const |
232 | { |
233 | return m_status; |
234 | } |
235 | |
236 | const QString &OpcUaNode::errorMessage() const |
237 | { |
238 | return m_errorMessage; |
239 | } |
240 | |
241 | /*! |
242 | \qmlmethod Date Node::getSourceTimestamp(QOpcUa::NodeAttribute attribute) |
243 | |
244 | Returns the source timestamp of the given \a attribute. |
245 | */ |
246 | QDateTime OpcUaNode::getSourceTimestamp(QOpcUa::NodeAttribute attribute) const |
247 | { |
248 | if (!m_connection || !m_node) |
249 | return QDateTime(); |
250 | return m_node->sourceTimestamp(attribute); |
251 | } |
252 | |
253 | /*! |
254 | \qmlmethod Date Node::getServerTimestamp(Constants::NodeAttribute attribute) |
255 | |
256 | Returns the server timestamp of the given \a attribute. |
257 | */ |
258 | QDateTime OpcUaNode::getServerTimestamp(QOpcUa::NodeAttribute attribute) const |
259 | { |
260 | if (!m_connection || !m_node) |
261 | return QDateTime(); |
262 | return m_node->serverTimestamp(attribute); |
263 | } |
264 | |
265 | void OpcUaNode::setNodeId(OpcUaNodeIdType *nodeId) |
266 | { |
267 | if (m_nodeId == nodeId) |
268 | return; |
269 | |
270 | // This deletes the initial dummy object that was created in the |
271 | // constructor in case a "real" nodeId is set. |
272 | if (m_nodeId->parent() == this) |
273 | m_nodeId->deleteLater(); |
274 | |
275 | // Disconnect signals from old node |
276 | m_nodeId->disconnect(receiver: this); |
277 | m_nodeId = nodeId; |
278 | connect(sender: m_nodeId, signal: &OpcUaNodeIdType::nodeChanged, context: this, slot: &OpcUaNode::updateNode); |
279 | connect(sender: m_nodeId, signal: &OpcUaNodeIdType::destroyed, context: this, slot: [&]() { m_nodeId = nullptr; }); |
280 | |
281 | updateNode(); |
282 | } |
283 | |
284 | void OpcUaNode::setConnection(OpcUaConnection *connection) |
285 | { |
286 | if (connection == m_connection) |
287 | return; |
288 | |
289 | m_connection = connection; |
290 | connect(sender: connection, SIGNAL(connectedChanged()), receiver: this, SLOT(updateNode())); |
291 | |
292 | updateNode(); |
293 | emit connectionChanged(connection); |
294 | } |
295 | |
296 | void OpcUaNode::setupNode(const QString &absoluteNodePath) |
297 | { |
298 | m_attributeCache.invalidate(); |
299 | m_absoluteNodePath = absoluteNodePath; |
300 | |
301 | if (m_node) { |
302 | m_node->deleteLater(); |
303 | |
304 | // Prevents a race condition where an update from the old node appears after the cache has been invalidate |
305 | QObject::disconnect(m_attributeUpdatedConnection); |
306 | QObject::disconnect(m_attributeReadConnection); |
307 | QObject::disconnect(m_enableMonitoringFinishedConnection); |
308 | QObject::disconnect(m_disableMonitoringFinishedConnection); |
309 | QObject::disconnect(m_monitoringStatusChangedConnection); |
310 | QObject::disconnect(m_eventOccurredConnection); |
311 | |
312 | m_node = nullptr; |
313 | } |
314 | |
315 | if (m_absoluteNodePath.isEmpty()) |
316 | return; |
317 | |
318 | auto conn = connection(); |
319 | if (!conn || !m_nodeId || !conn->m_client) |
320 | return; |
321 | |
322 | if (!conn->connected()) |
323 | return; |
324 | |
325 | m_node = conn->m_client->node(nodeId: m_absoluteNodePath); |
326 | if (!m_node) { |
327 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Invalid node:" << m_absoluteNodePath; |
328 | return; |
329 | } |
330 | |
331 | m_attributeUpdatedConnection = connect(sender: m_node, signal: &QOpcUaNode::attributeUpdated, |
332 | context: &m_attributeCache, slot: &OpcUaAttributeCache::setAttributeValue); |
333 | |
334 | m_attributeReadConnection = connect(sender: m_node, signal: &QOpcUaNode::attributeRead, context: this, slot: [this](){ |
335 | setReadyToUse(true); |
336 | }); |
337 | |
338 | m_enableMonitoringFinishedConnection = connect(sender: m_node, signal: &QOpcUaNode::enableMonitoringFinished, context: this, |
339 | slot: [this](QOpcUa::NodeAttribute attr, QOpcUa::UaStatusCode statusCode){ |
340 | if (attr != QOpcUa::NodeAttribute::EventNotifier) |
341 | return; |
342 | if (statusCode == QOpcUa::Good) { |
343 | m_eventFilterActive = true; |
344 | qCDebug(QT_OPCUA_PLUGINS_QML) << "Event filter was enabled for node" << resolvedNode().fullNodeId(); |
345 | updateEventFilter(); |
346 | } else { |
347 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Failed to enable event filter for node" << resolvedNode().fullNodeId(); |
348 | setStatus(status: Status::FailedToSetupMonitoring); |
349 | } |
350 | }); |
351 | |
352 | m_disableMonitoringFinishedConnection = connect(sender: m_node, signal: &QOpcUaNode::disableMonitoringFinished, context: this, |
353 | slot: [this](QOpcUa::NodeAttribute attr, QOpcUa::UaStatusCode statusCode){ |
354 | if (attr != QOpcUa::NodeAttribute::EventNotifier) |
355 | return; |
356 | if (statusCode == QOpcUa::Good) { |
357 | m_eventFilterActive = false; |
358 | qCDebug(QT_OPCUA_PLUGINS_QML) << "Event filter was disabled for node " << resolvedNode().fullNodeId(); |
359 | } else { |
360 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Failed to disable event filter for node " << resolvedNode().fullNodeId(); |
361 | setStatus(status: Status::FailedToDisableMonitoring); |
362 | } |
363 | }); |
364 | |
365 | m_monitoringStatusChangedConnection = connect(sender: m_node, signal: &QOpcUaNode::monitoringStatusChanged, context: this, |
366 | slot: [this](QOpcUa::NodeAttribute attr, QOpcUaMonitoringParameters::Parameters items, |
367 | QOpcUa::UaStatusCode statusCode) { |
368 | Q_UNUSED(items); |
369 | if (attr != QOpcUa::NodeAttribute::EventNotifier) |
370 | return; |
371 | if (statusCode != QOpcUa::Good) { |
372 | setStatus(status: Status::FailedToModifyMonitoring); |
373 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Failed to modify event filter for" << m_node->nodeId(); |
374 | } |
375 | }); |
376 | |
377 | m_eventOccurredConnection = connect (sender: m_node, signal: &QOpcUaNode::eventOccurred, context: this, slot: &OpcUaNode::eventOccurred); |
378 | |
379 | |
380 | // Read mandatory attributes |
381 | if (!m_node->readAttributes(attributes: m_attributesToRead)) { |
382 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Reading attributes" << m_node->nodeId() << "failed" ; |
383 | setStatus(status: Status::FailedToReadAttributes); |
384 | } |
385 | |
386 | updateEventFilter(); |
387 | } |
388 | |
389 | void OpcUaNode::updateNode() |
390 | { |
391 | retrieveAbsoluteNodePath(m_nodeId, [this](const QString &absoluteNodePath) {setupNode(absoluteNodePath);}); |
392 | } |
393 | |
394 | OpcUaEventFilter *OpcUaNode::eventFilter() const |
395 | { |
396 | return m_eventFilter; |
397 | } |
398 | |
399 | void OpcUaNode::setEventFilter(OpcUaEventFilter *eventFilter) |
400 | { |
401 | bool changed = false; |
402 | |
403 | if (m_eventFilter) { |
404 | disconnect(sender: m_eventFilter, signal: &OpcUaEventFilter::dataChanged, receiver: this, slot: &OpcUaNode::updateEventFilter); |
405 | changed = !(*m_eventFilter == *eventFilter); |
406 | } else { |
407 | changed = true; |
408 | } |
409 | |
410 | m_eventFilter = eventFilter; |
411 | connect(sender: m_eventFilter, signal: &OpcUaEventFilter::dataChanged, context: this, slot: &OpcUaNode::updateEventFilter); |
412 | |
413 | if (changed) |
414 | emit eventFilterChanged(); |
415 | } |
416 | |
417 | |
418 | void OpcUaNode::updateEventFilter() |
419 | { |
420 | if (!m_connection || !m_node || !m_eventFilter) |
421 | return; |
422 | |
423 | if (m_eventFilterActive) { |
424 | m_node->modifyEventFilter(eventFilter: m_eventFilter->filter(client: m_connection->m_client)); |
425 | } else { |
426 | QOpcUaMonitoringParameters parameters; |
427 | parameters.setFilter(m_eventFilter->filter(client: m_connection->m_client)); |
428 | m_node->enableMonitoring(attr: QOpcUa::NodeAttribute::EventNotifier, settings: parameters); |
429 | m_eventFilterActive = true; |
430 | } |
431 | } |
432 | |
433 | void OpcUaNode::setStatus(OpcUaNode::Status status, const QString &message) |
434 | { |
435 | QString errorMessage(message); |
436 | bool emitStatusChanged = false; |
437 | bool emitErrorMessageChanged = false; |
438 | |
439 | if (m_status != status) { |
440 | m_status = status; |
441 | emitStatusChanged = true; |
442 | } |
443 | |
444 | // if error message is not given, use default error message |
445 | if (errorMessage.isEmpty()) { |
446 | switch (m_status) { |
447 | case Status::Valid: |
448 | errorMessage = tr(s: "Node is valid" ); |
449 | break; |
450 | case Status::InvalidNodeId: |
451 | errorMessage = tr(s: "Node Id is invalid" ); |
452 | break; |
453 | case Status::NoConnection: |
454 | errorMessage = tr(s: "Not connected to server" ); |
455 | break; |
456 | case Status::InvalidNodeType: |
457 | errorMessage = tr(s: "QML element does not match node type on the server" ); |
458 | break; |
459 | case Status::InvalidClient: |
460 | errorMessage = tr(s: "Connecting client is invalid" ); |
461 | break; |
462 | case Status::FailedToResolveNode: |
463 | errorMessage = tr(s: "Failed to resolve node" ); |
464 | break; |
465 | case Status::InvalidObjectNode: |
466 | errorMessage = tr(s: "Invalid object node" ); |
467 | break; |
468 | case Status::FailedToReadAttributes: |
469 | errorMessage = tr(s: "Failed to read attributes" ); |
470 | break; |
471 | case Status::FailedToSetupMonitoring: |
472 | errorMessage = tr(s: "Failed to setup monitoring" ); |
473 | break; |
474 | case Status::FailedToWriteAttribute: |
475 | errorMessage = tr(s: "Failed to write attribute" ); |
476 | break; |
477 | case Status::FailedToModifyMonitoring: |
478 | errorMessage = tr(s: "Failed to modify monitoring" ); |
479 | break; |
480 | case Status::FailedToDisableMonitoring: |
481 | errorMessage = tr(s: "Failed to disable monitoring" ); |
482 | break; |
483 | } |
484 | } |
485 | |
486 | if (errorMessage != m_errorMessage) { |
487 | m_errorMessage = errorMessage; |
488 | emitErrorMessageChanged = true; |
489 | } |
490 | |
491 | if (emitStatusChanged) |
492 | emit statusChanged(); |
493 | if (emitErrorMessageChanged) |
494 | emit errorMessageChanged(); |
495 | } |
496 | |
497 | const UniversalNode &OpcUaNode::resolvedNode() const |
498 | { |
499 | return m_resolvedNode; |
500 | } |
501 | |
502 | QOpcUaNode *OpcUaNode::node() const |
503 | { |
504 | return m_node; |
505 | } |
506 | |
507 | void OpcUaNode::setAttributesToRead(QOpcUa::NodeAttributes attributes) |
508 | { |
509 | m_attributesToRead = attributes; |
510 | } |
511 | |
512 | QOpcUa::NodeAttributes OpcUaNode::attributesToRead() const |
513 | { |
514 | return m_attributesToRead; |
515 | } |
516 | |
517 | void OpcUaNode::retrieveAbsoluteNodePath(OpcUaNodeIdType *node, std::function<void (const QString &)> functor) |
518 | { |
519 | auto conn = connection(); |
520 | if (!conn) { |
521 | qCWarning(QT_OPCUA_PLUGINS_QML) << "No connection to server" ; |
522 | setStatus(status: Status::NoConnection); |
523 | return; |
524 | } |
525 | if (!m_nodeId) { |
526 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Invalid node ID" ; |
527 | setStatus(status: Status::InvalidNodeId); |
528 | return; |
529 | } |
530 | if (!conn->m_client) { |
531 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Client instance is invalid" ; |
532 | setStatus(status: Status::InvalidClient); |
533 | return; |
534 | } |
535 | |
536 | if (!conn->connected()) { |
537 | qCWarning(QT_OPCUA_PLUGINS_QML) << "not connected" ; |
538 | return; |
539 | } |
540 | |
541 | if (qobject_cast<const OpcUaNodeId *>(object: node)) { |
542 | UniversalNode tmp(node); |
543 | tmp.resolveNamespace(client: conn->m_client); |
544 | m_resolvedNode.from(tmp); |
545 | functor(m_resolvedNode.fullNodeId()); |
546 | emit nodeIdChanged(nodeId: m_nodeId); |
547 | emit nodeChanged(); |
548 | } else if (qobject_cast<OpcUaRelativeNodeId *>(object: node)) { |
549 | auto nodeId = qobject_cast<OpcUaRelativeNodeId *>(object: node); |
550 | OpcUaPathResolver *resolver = new OpcUaPathResolver(nodeId, conn->m_client, this); |
551 | connect(sender: resolver, signal: &OpcUaPathResolver::resolvedNode, context: this, slot: [this, functor, resolver](UniversalNode nodeToUse, const QString &errorMessage) { |
552 | resolver->deleteLater(); |
553 | |
554 | if (!errorMessage.isEmpty()) { |
555 | qCWarning(QT_OPCUA_PLUGINS_QML) << "Failed to resolve node:" << errorMessage; |
556 | setStatus(status: Status::FailedToResolveNode, message: errorMessage); |
557 | functor(QString()); |
558 | return; |
559 | } |
560 | |
561 | m_resolvedNode.from(nodeToUse); |
562 | functor(m_resolvedNode.fullNodeId()); |
563 | emit nodeIdChanged(nodeId: m_nodeId); |
564 | emit nodeChanged(); |
565 | }); |
566 | resolver->startResolving(); |
567 | } else { |
568 | functor(QString()); |
569 | } |
570 | } |
571 | |
572 | void OpcUaNode::setReadyToUse(bool value) |
573 | { |
574 | if (value && !checkValidity()) |
575 | value = false; |
576 | |
577 | bool old = m_readyToUse; |
578 | m_readyToUse = value; |
579 | |
580 | if (value) |
581 | setStatus(status: Status::Valid); |
582 | |
583 | if (!old && value) |
584 | emit readyToUseChanged(); |
585 | } |
586 | |
587 | bool OpcUaNode::checkValidity() |
588 | { |
589 | return true; |
590 | } |
591 | |
592 | QT_END_NAMESPACE |
593 | |