/** * @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); }