From 0c1df583acba434e2d7f6905a30fdefe288d0f9d Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Thu, 19 Feb 2026 06:18:29 +0100 Subject: Add BobinkNode QML type and two-page demo for node monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/BobinkNode.cpp | 297 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/BobinkNode.cpp (limited to 'src/BobinkNode.cpp') 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 +#include + +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 (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 (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 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); +} -- cgit v1.2.3