summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-20 12:16:50 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-20 12:16:50 +0100
commit690db0fa630ac57d0ec99010862c7b7e4a7ac589 (patch)
treeb2e847f9c3dddc71d18be03f0c26090d3939ec25
parent5b93aac1f802d0db838d3d12802f2863eb13e0f8 (diff)
downloadBobinkQtOpcUa-690db0fa630ac57d0ec99010862c7b7e4a7ac589.tar.gz
BobinkQtOpcUa-690db0fa630ac57d0ec99010862c7b7e4a7ac589.zip
Implement OpcUaMonitoredNode attribute reading with OpcUaNodeInfo gadget
-rw-r--r--demo/NodePage.qml138
-rw-r--r--src/OpcUaMonitoredNode.cpp157
-rw-r--r--src/OpcUaMonitoredNode.h55
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 <QMetaEnum>
+#include <QOpcUaLocalizedText>
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<QOpcUaLocalizedText> ().text ();
+ break;
+ case QOpcUa::NodeAttribute::Description:
+ m_info.description = value.value<QOpcUaLocalizedText> ().text ();
+ break;
+ case QOpcUa::NodeAttribute::NodeClass:
+ {
+ auto me = QMetaEnum::fromType<QOpcUa::NodeClass> ();
+ auto nc = static_cast<QOpcUa::NodeClass> (value.toInt ());
+ const char *key = me.valueToKey (static_cast<int> (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 <QDateTime>
#include <QObject>
+#include <QOpcUaNode>
#include <QQmlEngine>
#include <QQmlParserStatus>
+#include <QVariant>
+
+/**
+ * @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