From 690db0fa630ac57d0ec99010862c7b7e4a7ac589 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Fri, 20 Feb 2026 12:16:50 +0100 Subject: Implement OpcUaMonitoredNode attribute reading with OpcUaNodeInfo gadget --- demo/NodePage.qml | 138 +++++++++++++++++++++++++++++++++++---- src/OpcUaMonitoredNode.cpp | 157 +++++++++++++++++++++++++++++++++++++++++++++ src/OpcUaMonitoredNode.h | 55 ++++++++++++++++ 3 files changed, 337 insertions(+), 13 deletions(-) diff --git a/demo/NodePage.qml b/demo/NodePage.qml index e00f468..2de0a15 100644 --- a/demo/NodePage.qml +++ b/demo/NodePage.qml @@ -1,4 +1,4 @@ -// NodePage.qml — Demo page with OpcUaMonitoredNode lifecycle logging. +// NodePage.qml — Demo page displaying a single OPC UA node. import QtQuick import QtQuick.Controls @@ -14,17 +14,16 @@ Page { OpcUaMonitoredNode { id: demoNode - nodeId: "ns=2;s=DemoVariable.Page" + nodePage.pageNumber + nodeId: nodePage.pageNumber === 1 ? "ns=1;s=double_rw_scalar" : "ns=1;s=string_rw_scalar" monitored: nodePage.StackView.status === StackView.Active - onMonitoredChanged: nodePage.logFunction( - "Page " + nodePage.pageNumber + " node [" + nodeId + "] " - + (monitored ? "MONITORED" : "UNMONITORED")) + onMonitoredChanged: nodePage.logFunction("Page " + nodePage.pageNumber + " node [" + nodeId + "] " + (monitored ? "MONITORED" : "UNMONITORED")) + onValueChanged: nodePage.logFunction("Page " + nodePage.pageNumber + " value = " + value) } ColumnLayout { anchors.fill: parent anchors.margins: 20 - spacing: 12 + spacing: 8 RowLayout { Label { @@ -32,22 +31,135 @@ Page { font.bold: true font.pointSize: 14 } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + } Button { text: "Disconnect" onClicked: Bobink.disconnectFromServer() } } - Label { - text: "Node: " + demoNode.nodeId + // Value (prominent) + Rectangle { + Layout.fillWidth: true + height: 60 + color: "#f0f0f0" + radius: 4 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 2 + + Label { + text: "Value" + font.pointSize: 10 + color: "gray" + } + Label { + text: demoNode.value !== undefined ? String(demoNode.value) : "—" + font.pointSize: 16 + font.bold: true + elide: Text.ElideRight + Layout.fillWidth: true + } + } } - Label { - text: "Monitored: " + demoNode.monitored - color: demoNode.monitored ? "green" : "gray" + + // Node info + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: 12 + rowSpacing: 4 + + Label { + text: "Node ID:" + color: "gray" + } + Label { + text: demoNode.nodeId + Layout.fillWidth: true + } + + Label { + text: "Display Name:" + color: "gray" + } + Label { + text: demoNode.info.displayName || "—" + Layout.fillWidth: true + } + + Label { + text: "Data Type:" + color: "gray" + } + Label { + text: demoNode.info.dataType || "—" + Layout.fillWidth: true + } + + Label { + text: "Node Class:" + color: "gray" + } + Label { + text: demoNode.info.nodeClass || "—" + Layout.fillWidth: true + } + + Label { + text: "Access Level:" + color: "gray" + } + Label { + text: demoNode.info.accessLevel + Layout.fillWidth: true + } + + Label { + text: "Status:" + color: "gray" + } + Label { + text: demoNode.info.status || "—" + Layout.fillWidth: true + } + + Label { + text: "Source Time:" + color: "gray" + } + Label { + text: demoNode.info.sourceTimestamp.toLocaleString() || "—" + Layout.fillWidth: true + } + + Label { + text: "Server Time:" + color: "gray" + } + Label { + text: demoNode.info.serverTimestamp.toLocaleString() || "—" + Layout.fillWidth: true + } + + Label { + text: "Monitored:" + color: "gray" + } + Label { + text: demoNode.monitored ? "Yes" : "No" + color: demoNode.monitored ? "green" : "gray" + Layout.fillWidth: true + } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } Button { Layout.fillWidth: true diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp index f7a5c9f..0ffe300 100644 --- a/src/OpcUaMonitoredNode.cpp +++ b/src/OpcUaMonitoredNode.cpp @@ -3,9 +3,17 @@ * @brief OpcUaMonitoredNode implementation. */ #include "OpcUaMonitoredNode.h" +#include "OpcUaClient.h" + +#include +#include OpcUaMonitoredNode::OpcUaMonitoredNode (QObject *parent) : QObject (parent) {} +/* ====================================== + * Properties + * ====================================== */ + QString OpcUaMonitoredNode::nodeId () const { @@ -19,6 +27,13 @@ OpcUaMonitoredNode::setNodeId (const QString &id) return; m_nodeId = id; emit nodeIdChanged (); + + if (!m_componentComplete) + return; + + teardownNode (); + if (OpcUaClient::instance () && OpcUaClient::instance ()->connected ()) + setupNode (); } bool @@ -36,6 +51,22 @@ OpcUaMonitoredNode::setMonitored (bool monitored) emit monitoredChanged (); } +QVariant +OpcUaMonitoredNode::value () const +{ + return m_value; +} + +OpcUaNodeInfo +OpcUaMonitoredNode::info () const +{ + return m_info; +} + +/* ====================================== + * QQmlParserStatus + * ====================================== */ + void OpcUaMonitoredNode::classBegin () { @@ -45,4 +76,130 @@ void OpcUaMonitoredNode::componentComplete () { m_componentComplete = true; + + auto *client = OpcUaClient::instance (); + if (!client) + return; + + connect (client, &OpcUaClient::connectedChanged, this, + &OpcUaMonitoredNode::handleClientConnectedChanged); + + if (client->connected () && !m_nodeId.isEmpty ()) + setupNode (); +} + +/* ====================================== + * Node lifecycle + * ====================================== */ + +void +OpcUaMonitoredNode::setupNode () +{ + if (m_node || m_nodeId.isEmpty ()) + return; + + auto *client = OpcUaClient::instance (); + if (!client || !client->connected ()) + return; + + m_node = client->opcuaClient ()->node (m_nodeId); + if (!m_node) + return; + + m_node->setParent (this); + + connect (m_node, &QOpcUaNode::attributeUpdated, this, + &OpcUaMonitoredNode::handleAttributeUpdated); + connect (m_node, &QOpcUaNode::valueAttributeUpdated, this, + &OpcUaMonitoredNode::handleValueUpdated); + + m_node->readAttributes ( + QOpcUa::NodeAttribute::Value | QOpcUa::NodeAttribute::DisplayName + | QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::NodeClass + | QOpcUa::NodeAttribute::DataType | QOpcUa::NodeAttribute::AccessLevel); +} + +void +OpcUaMonitoredNode::teardownNode () +{ + if (!m_node) + return; + + delete m_node; + m_node = nullptr; + + m_value.clear (); + emit valueChanged (); + + m_info = OpcUaNodeInfo{}; + emit infoChanged (); +} + +/* ====================================== + * Signal handlers + * ====================================== */ + +void +OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr, + const QVariant &value) +{ + switch (attr) + { + case QOpcUa::NodeAttribute::DisplayName: + m_info.displayName = value.value ().text (); + break; + case QOpcUa::NodeAttribute::Description: + m_info.description = value.value ().text (); + break; + case QOpcUa::NodeAttribute::NodeClass: + { + auto me = QMetaEnum::fromType (); + auto nc = static_cast (value.toInt ()); + const char *key = me.valueToKey (static_cast (nc)); + m_info.nodeClass + = key ? QLatin1StringView (key) : QStringLiteral ("Unknown"); + } + break; + case QOpcUa::NodeAttribute::DataType: + m_info.dataType = value.toString (); + break; + case QOpcUa::NodeAttribute::AccessLevel: + m_info.accessLevel = value.toUInt (); + break; + default: + return; + } + emit infoChanged (); +} + +void +OpcUaMonitoredNode::handleValueUpdated (const QVariant &value) +{ + m_value = value; + emit valueChanged (); + + m_info.status = QOpcUa::statusToString (m_node->valueAttributeError ()); + m_info.sourceTimestamp + = m_node->sourceTimestamp (QOpcUa::NodeAttribute::Value); + m_info.serverTimestamp + = m_node->serverTimestamp (QOpcUa::NodeAttribute::Value); + emit infoChanged (); +} + +void +OpcUaMonitoredNode::handleClientConnectedChanged () +{ + auto *client = OpcUaClient::instance (); + if (!client) + return; + + if (client->connected ()) + { + if (!m_nodeId.isEmpty ()) + setupNode (); + } + else + { + teardownNode (); + } } diff --git a/src/OpcUaMonitoredNode.h b/src/OpcUaMonitoredNode.h index 9d093b5..53e9537 100644 --- a/src/OpcUaMonitoredNode.h +++ b/src/OpcUaMonitoredNode.h @@ -8,9 +8,44 @@ #ifndef OPCUAMONITOREDNODE_H #define OPCUAMONITOREDNODE_H +#include #include +#include #include #include +#include + +/** + * @brief Metadata bundle for a monitored OPC UA node. + * + * Exposed as a single Q_PROPERTY on OpcUaMonitoredNode so that + * the QML API stays simple (only `value` is top-level). + * Advanced users can access fields via dot notation: + * node.info.displayName, node.info.status, etc. + */ +struct OpcUaNodeInfo +{ + Q_GADGET + Q_PROPERTY (QString displayName MEMBER displayName) + Q_PROPERTY (QString description MEMBER description) + Q_PROPERTY (QString nodeClass MEMBER nodeClass) + Q_PROPERTY (QString dataType MEMBER dataType) + Q_PROPERTY (quint32 accessLevel MEMBER accessLevel) + Q_PROPERTY (QString status MEMBER status) + Q_PROPERTY (QDateTime sourceTimestamp MEMBER sourceTimestamp) + Q_PROPERTY (QDateTime serverTimestamp MEMBER serverTimestamp) + +public: + QString displayName; + QString description; + QString nodeClass; + QString dataType; + quint32 accessLevel = 0; + QString status; + QDateTime sourceTimestamp; + QDateTime serverTimestamp; +}; +Q_DECLARE_METATYPE (OpcUaNodeInfo) class OpcUaMonitoredNode : public QObject, public QQmlParserStatus { @@ -21,6 +56,8 @@ class OpcUaMonitoredNode : public QObject, public QQmlParserStatus Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged) Q_PROPERTY ( bool monitored READ monitored WRITE setMonitored NOTIFY monitoredChanged) + Q_PROPERTY (QVariant value READ value NOTIFY valueChanged) + Q_PROPERTY (OpcUaNodeInfo info READ info NOTIFY infoChanged) public: explicit OpcUaMonitoredNode (QObject *parent = nullptr); @@ -31,17 +68,35 @@ public: bool monitored () const; void setMonitored (bool monitored); + QVariant value () const; + OpcUaNodeInfo info () const; + void classBegin () override; void componentComplete () override; signals: void nodeIdChanged (); void monitoredChanged (); + void valueChanged (); + void infoChanged (); + +private slots: + void handleAttributeUpdated (QOpcUa::NodeAttribute attr, + const QVariant &value); + void handleValueUpdated (const QVariant &value); + void handleClientConnectedChanged (); private: + void setupNode (); + void teardownNode (); + QString m_nodeId; bool m_monitored = true; bool m_componentComplete = false; + + QOpcUaNode *m_node = nullptr; + QVariant m_value; + OpcUaNodeInfo m_info; }; #endif // OPCUAMONITOREDNODE_H -- cgit v1.2.3