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/BobinkNode.cpp | |
| 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/BobinkNode.cpp')
| -rw-r--r-- | src/BobinkNode.cpp | 297 |
1 files changed, 297 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); +} |
