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
15QT_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
148Q_LOGGING_CATEGORY(QT_OPCUA_PLUGINS_QML, "qt.opcua.plugins.qml")
149
150OpcUaNode::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
164OpcUaNode::~OpcUaNode()
165{
166 delete m_node;
167}
168
169OpcUaNodeIdType *OpcUaNode::nodeId() const
170{
171 return m_nodeId;
172}
173
174OpcUaConnection *OpcUaNode::connection()
175{
176 if (!m_connection)
177 setConnection(OpcUaConnection::defaultConnection());
178
179 return m_connection;
180}
181
182bool OpcUaNode::readyToUse() const
183{
184 return m_readyToUse;
185}
186
187void 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
197QString OpcUaNode::browseName()
198{
199 return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::BrowseName).value<QOpcUaQualifiedName>().name();
200}
201
202QOpcUa::NodeClass OpcUaNode::nodeClass()
203{
204 return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::NodeClass).value<QOpcUa::NodeClass>();
205}
206
207void 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
214QOpcUaLocalizedText OpcUaNode::displayName()
215{
216 return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::DisplayName).value<QOpcUaLocalizedText>();
217}
218
219void 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
226QOpcUaLocalizedText OpcUaNode::description()
227{
228 return m_attributeCache.attributeValue(QOpcUa::NodeAttribute::Description).value<QOpcUaLocalizedText>();
229}
230
231OpcUaNode::Status OpcUaNode::status() const
232{
233 return m_status;
234}
235
236const 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*/
246QDateTime 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*/
258QDateTime OpcUaNode::getServerTimestamp(QOpcUa::NodeAttribute attribute) const
259{
260 if (!m_connection || !m_node)
261 return QDateTime();
262 return m_node->serverTimestamp(attribute);
263}
264
265void 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
284void 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
296void 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
389void OpcUaNode::updateNode()
390{
391 retrieveAbsoluteNodePath(m_nodeId, [this](const QString &absoluteNodePath) {setupNode(absoluteNodePath);});
392}
393
394OpcUaEventFilter *OpcUaNode::eventFilter() const
395{
396 return m_eventFilter;
397}
398
399void 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
418void 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
433void 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
497const UniversalNode &OpcUaNode::resolvedNode() const
498{
499 return m_resolvedNode;
500}
501
502QOpcUaNode *OpcUaNode::node() const
503{
504 return m_node;
505}
506
507void OpcUaNode::setAttributesToRead(QOpcUa::NodeAttributes attributes)
508{
509 m_attributesToRead = attributes;
510}
511
512QOpcUa::NodeAttributes OpcUaNode::attributesToRead() const
513{
514 return m_attributesToRead;
515}
516
517void 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
572void 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
587bool OpcUaNode::checkValidity()
588{
589 return true;
590}
591
592QT_END_NAMESPACE
593

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