diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 06:18:29 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 06:18:29 +0100 |
| commit | 0c1df583acba434e2d7f6905a30fdefe288d0f9d (patch) | |
| tree | e485fb1510ce2441622c4b29b8762633849f6fd2 /src | |
| parent | a0c7f2a7ef04dbe2e7491eabf828e26423d1bd10 (diff) | |
| download | BobinkQtOpcUa-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.cpp | 297 | ||||
| -rw-r--r-- | src/BobinkNode.h | 105 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 2 |
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") |
