summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-19 06:18:29 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-19 06:18:29 +0100
commit0c1df583acba434e2d7f6905a30fdefe288d0f9d (patch)
treee485fb1510ce2441622c4b29b8762633849f6fd2 /src
parenta0c7f2a7ef04dbe2e7491eabf828e26423d1bd10 (diff)
downloadBobinkQtOpcUa-0c1df583acba434e2d7f6905a30fdefe288d0f9d.tar.gz
BobinkQtOpcUa-0c1df583acba434e2d7f6905a30fdefe288d0f9d.zip
Add BobinkNode QML type and two-page demo for node monitoring
BobinkNode (QQuickItem) monitors a single OPC UA node with automatic lifecycle: monitoring starts/stops based on item visibility (StackView page switches, Loader, etc.). Properties: nodeId, value (r/w), status, sourceTimestamp, serverTimestamp. On-demand readAttribute() for metadata. writeError signal for failed writes. Demo restructured with StackView: connection page → two node pages demonstrating the visibility-based monitoring lifecycle.
Diffstat (limited to 'src')
-rw-r--r--src/BobinkNode.cpp297
-rw-r--r--src/BobinkNode.h105
-rw-r--r--src/CMakeLists.txt2
3 files changed, 404 insertions, 0 deletions
diff --git a/src/BobinkNode.cpp b/src/BobinkNode.cpp
new file mode 100644
index 0000000..241b9c5
--- /dev/null
+++ b/src/BobinkNode.cpp
@@ -0,0 +1,297 @@
+/**
+ * @file BobinkNode.cpp
+ * @brief QML component representing a single OPC UA node.
+ */
+#include "BobinkNode.h"
+#include "BobinkClient.h"
+
+#include <QOpcUaClient>
+#include <QOpcUaMonitoringParameters>
+
+static constexpr double DEFAULT_PUBLISHING_INTERVAL = 250.0; // ms
+
+/* ------------------------------------------------------------------ */
+/* Construction / destruction */
+/* ------------------------------------------------------------------ */
+
+BobinkNode::BobinkNode (QQuickItem *parent) : QQuickItem (parent) {}
+
+BobinkNode::~BobinkNode () { stopMonitoring (); }
+
+/* ------------------------------------------------------------------ */
+/* Properties */
+/* ------------------------------------------------------------------ */
+
+QString
+BobinkNode::nodeId () const
+{
+ return m_nodeId;
+}
+
+void
+BobinkNode::setNodeId (const QString &id)
+{
+ if (m_nodeId == id)
+ return;
+ m_nodeId = id;
+ emit nodeIdChanged ();
+
+ if (m_componentComplete && isVisible ())
+ {
+ stopMonitoring ();
+ startMonitoring ();
+ }
+}
+
+QVariant
+BobinkNode::value () const
+{
+ return m_value;
+}
+
+void
+BobinkNode::setValue (const QVariant &value)
+{
+ if (m_opcuaNode)
+ m_opcuaNode->writeValueAttribute (value);
+}
+
+BobinkNode::NodeStatus
+BobinkNode::status () const
+{
+ return m_status;
+}
+
+QDateTime
+BobinkNode::sourceTimestamp () const
+{
+ return m_sourceTimestamp;
+}
+
+QDateTime
+BobinkNode::serverTimestamp () const
+{
+ return m_serverTimestamp;
+}
+
+/* ------------------------------------------------------------------ */
+/* readAttribute */
+/* ------------------------------------------------------------------ */
+
+void
+BobinkNode::readAttribute (const QString &attributeName)
+{
+ if (!m_opcuaNode)
+ return;
+
+ QOpcUa::NodeAttribute attr = attributeFromName (attributeName);
+ if (attr == QOpcUa::NodeAttribute::None)
+ return;
+
+ m_pendingReads.insert (attr, attributeName);
+ m_opcuaNode->readAttributes (attr);
+}
+
+/* ------------------------------------------------------------------ */
+/* QQuickItem lifecycle */
+/* ------------------------------------------------------------------ */
+
+void
+BobinkNode::componentComplete ()
+{
+ QQuickItem::componentComplete ();
+ m_componentComplete = true;
+
+ auto *client = BobinkClient::instance ();
+ if (client)
+ connect (client, &BobinkClient::connectedChanged, this,
+ &BobinkNode::handleClientConnectedChanged);
+
+ if (isVisible ())
+ startMonitoring ();
+}
+
+void
+BobinkNode::itemChange (ItemChange change, const ItemChangeData &data)
+{
+ QQuickItem::itemChange (change, data);
+
+ if (change == ItemVisibleHasChanged && m_componentComplete)
+ {
+ if (data.boolValue)
+ startMonitoring ();
+ else
+ stopMonitoring ();
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Monitoring lifecycle */
+/* ------------------------------------------------------------------ */
+
+void
+BobinkNode::startMonitoring ()
+{
+ if (m_opcuaNode || m_nodeId.isEmpty ())
+ return;
+
+ auto *client = BobinkClient::instance ();
+ if (!client || !client->connected ())
+ return;
+
+ QOpcUaClient *opcua = client->opcuaClient ();
+ if (!opcua)
+ return;
+
+ m_opcuaNode = opcua->node (m_nodeId);
+ if (!m_opcuaNode)
+ return;
+
+ connect (m_opcuaNode, &QOpcUaNode::dataChangeOccurred, this,
+ &BobinkNode::handleDataChange);
+ connect (m_opcuaNode, &QOpcUaNode::attributeUpdated, this,
+ &BobinkNode::handleAttributeUpdated);
+ connect (m_opcuaNode, &QOpcUaNode::attributeWritten, this,
+ &BobinkNode::handleAttributeWritten);
+
+ QOpcUaMonitoringParameters params (DEFAULT_PUBLISHING_INTERVAL);
+ m_opcuaNode->enableMonitoring (QOpcUa::NodeAttribute::Value, params);
+}
+
+void
+BobinkNode::stopMonitoring ()
+{
+ if (!m_opcuaNode)
+ return;
+
+ m_pendingReads.clear ();
+ delete m_opcuaNode;
+ m_opcuaNode = nullptr;
+}
+
+/* ------------------------------------------------------------------ */
+/* Signal handlers */
+/* ------------------------------------------------------------------ */
+
+void
+BobinkNode::handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+
+ if (m_value != val)
+ {
+ m_value = val;
+ emit valueChanged ();
+ }
+
+ NodeStatus newStatus = statusFromCode (m_opcuaNode->valueAttributeError ());
+ if (m_status != newStatus)
+ {
+ m_status = newStatus;
+ emit statusChanged ();
+ }
+
+ QDateTime srcTs
+ = m_opcuaNode->sourceTimestamp (QOpcUa::NodeAttribute::Value);
+ if (m_sourceTimestamp != srcTs)
+ {
+ m_sourceTimestamp = srcTs;
+ emit sourceTimestampChanged ();
+ }
+
+ QDateTime srvTs
+ = m_opcuaNode->serverTimestamp (QOpcUa::NodeAttribute::Value);
+ if (m_serverTimestamp != srvTs)
+ {
+ m_serverTimestamp = srvTs;
+ emit serverTimestampChanged ();
+ }
+}
+
+void
+BobinkNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
+ const QVariant &val)
+{
+ auto it = m_pendingReads.find (attr);
+ if (it != m_pendingReads.end ())
+ {
+ emit attributeRead (it.value (), val);
+ m_pendingReads.erase (it);
+ }
+}
+
+void
+BobinkNode::handleAttributeWritten (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+
+ if (statusCode != QOpcUa::UaStatusCode::Good)
+ emit writeError (QStringLiteral ("Write failed: 0x%1")
+ .arg (static_cast<quint32> (statusCode), 8, 16,
+ QLatin1Char ('0')));
+}
+
+void
+BobinkNode::handleClientConnectedChanged ()
+{
+ auto *client = BobinkClient::instance ();
+ if (!client)
+ return;
+
+ if (client->connected ())
+ {
+ if (m_componentComplete && isVisible ())
+ startMonitoring ();
+ }
+ else
+ {
+ stopMonitoring ();
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Helpers */
+/* ------------------------------------------------------------------ */
+
+BobinkNode::NodeStatus
+BobinkNode::statusFromCode (QOpcUa::UaStatusCode code)
+{
+ quint32 severity = static_cast<quint32> (code) >> 30;
+ switch (severity)
+ {
+ case 0:
+ return Good;
+ case 1:
+ return Uncertain;
+ default:
+ return Bad;
+ }
+}
+
+QOpcUa::NodeAttribute
+BobinkNode::attributeFromName (const QString &name)
+{
+ static const QHash<QString, QOpcUa::NodeAttribute> map = {
+ { "NodeId", QOpcUa::NodeAttribute::NodeId },
+ { "NodeClass", QOpcUa::NodeAttribute::NodeClass },
+ { "BrowseName", QOpcUa::NodeAttribute::BrowseName },
+ { "DisplayName", QOpcUa::NodeAttribute::DisplayName },
+ { "Description", QOpcUa::NodeAttribute::Description },
+ { "Value", QOpcUa::NodeAttribute::Value },
+ { "DataType", QOpcUa::NodeAttribute::DataType },
+ { "ValueRank", QOpcUa::NodeAttribute::ValueRank },
+ { "ArrayDimensions", QOpcUa::NodeAttribute::ArrayDimensions },
+ { "AccessLevel", QOpcUa::NodeAttribute::AccessLevel },
+ { "UserAccessLevel", QOpcUa::NodeAttribute::UserAccessLevel },
+ { "MinimumSamplingInterval",
+ QOpcUa::NodeAttribute::MinimumSamplingInterval },
+ { "Historizing", QOpcUa::NodeAttribute::Historizing },
+ { "Executable", QOpcUa::NodeAttribute::Executable },
+ { "UserExecutable", QOpcUa::NodeAttribute::UserExecutable },
+ };
+
+ return map.value (name, QOpcUa::NodeAttribute::None);
+}
diff --git a/src/BobinkNode.h b/src/BobinkNode.h
new file mode 100644
index 0000000..0fa1678
--- /dev/null
+++ b/src/BobinkNode.h
@@ -0,0 +1,105 @@
+/**
+ * @file BobinkNode.h
+ * @brief QML component representing a single OPC UA node.
+ *
+ * Inherits QQuickItem so that monitoring is automatically tied to
+ * visibility: when the item (or any ancestor) becomes invisible
+ * (e.g. StackView navigates away, Loader unloads), the monitored
+ * item is removed from the subscription. When visible again,
+ * monitoring resumes.
+ */
+#ifndef BOBINKNODE_H
+#define BOBINKNODE_H
+
+#include <QDateTime>
+#include <QHash>
+#include <QOpcUaNode>
+#include <QQuickItem>
+
+class BobinkNode : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+ Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged)
+ Q_PROPERTY (QVariant value READ value WRITE setValue NOTIFY valueChanged)
+ Q_PROPERTY (NodeStatus status READ status NOTIFY statusChanged)
+ Q_PROPERTY (QDateTime sourceTimestamp READ sourceTimestamp NOTIFY
+ sourceTimestampChanged)
+ Q_PROPERTY (QDateTime serverTimestamp READ serverTimestamp NOTIFY
+ serverTimestampChanged)
+
+public:
+ explicit BobinkNode (QQuickItem *parent = nullptr);
+ ~BobinkNode () override;
+
+ /// Simplified OPC UA status severity.
+ enum NodeStatus
+ {
+ Good,
+ Uncertain,
+ Bad
+ };
+ Q_ENUM (NodeStatus)
+
+ QString nodeId () const;
+ void setNodeId (const QString &id);
+
+ QVariant value () const;
+ /** Setting value writes to the server; the property updates when
+ * the server confirms via the monitored data change. */
+ void setValue (const QVariant &value);
+
+ NodeStatus status () const;
+ QDateTime sourceTimestamp () const;
+ QDateTime serverTimestamp () const;
+
+ /** Read an attribute on demand (DisplayName, Description, DataType, …).
+ * Result arrives asynchronously via attributeRead(). */
+ Q_INVOKABLE void readAttribute (const QString &attributeName);
+
+signals:
+ void nodeIdChanged ();
+ void valueChanged ();
+ void statusChanged ();
+ void sourceTimestampChanged ();
+ void serverTimestampChanged ();
+
+ /** Emitted when a readAttribute() call completes. */
+ void attributeRead (const QString &attributeName, const QVariant &value);
+
+ /** Emitted when a write to the server fails. */
+ void writeError (const QString &message);
+
+protected:
+ void componentComplete () override;
+ void itemChange (ItemChange change, const ItemChangeData &data) override;
+
+private:
+ void startMonitoring ();
+ void stopMonitoring ();
+
+ void handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val);
+ void handleAttributeUpdated (QOpcUa::NodeAttribute attr,
+ const QVariant &val);
+ void handleAttributeWritten (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode);
+ void handleClientConnectedChanged ();
+
+ static NodeStatus statusFromCode (QOpcUa::UaStatusCode code);
+ static QOpcUa::NodeAttribute attributeFromName (const QString &name);
+
+ QString m_nodeId;
+ QVariant m_value;
+ NodeStatus m_status = Bad;
+ QDateTime m_sourceTimestamp;
+ QDateTime m_serverTimestamp;
+
+ QOpcUaNode *m_opcuaNode = nullptr;
+ bool m_componentComplete = false;
+
+ /** Tracks in-flight readAttribute() requests (enum → original name). */
+ QHash<QOpcUa::NodeAttribute, QString> m_pendingReads;
+};
+
+#endif // BOBINKNODE_H
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 615e90c..5898132 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -10,6 +10,8 @@ qt_add_qml_module(
BobinkAuth.cpp
BobinkClient.h
BobinkClient.cpp
+ BobinkNode.h
+ BobinkNode.cpp
OUTPUT_DIRECTORY
"${CMAKE_BINARY_DIR}/qml/Bobink")