/** * @file BobinkNode.cpp * @brief QML component representing a single OPC UA node. */ #include "BobinkNode.h" #include "BobinkClient.h" #include #include using namespace Qt::Literals::StringLiterals; 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); connect (m_opcuaNode, &QOpcUaNode::attributeRead, this, &BobinkNode::handleAttributeReadFinished); connect (m_opcuaNode, &QOpcUaNode::enableMonitoringFinished, this, &BobinkNode::handleEnableMonitoringFinished); connect (m_opcuaNode, &QOpcUaNode::disableMonitoringFinished, this, &BobinkNode::handleDisableMonitoringFinished); 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 () { if (!BobinkClient::instance ()) return; if (BobinkClient::instance ()->connected ()) { if (m_componentComplete && isVisible ()) startMonitoring (); } else { stopMonitoring (); } } void BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs) { if (!BobinkClient::instance () || !m_opcuaNode) return; for (int bit = 0; bit < 27; ++bit) { auto attr = static_cast (1 << bit); if (!attrs.testFlag (attr)) continue; auto sc = m_opcuaNode->attributeError (attr); QLatin1StringView name = nameFromAttribute (attr); if (sc == QOpcUa::UaStatusCode::Good) emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Read %1.%2 = %3") .arg (m_nodeId, name, m_opcuaNode->attribute (attr).toString ())); else emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Read %1.%2 failed: 0x%3") .arg (m_nodeId, name) .arg (static_cast (sc), 8, 16, QLatin1Char ('0'))); } } void BobinkNode::handleEnableMonitoringFinished (QOpcUa::NodeAttribute, QOpcUa::UaStatusCode statusCode) { if (!BobinkClient::instance ()) return; if (statusCode == QOpcUa::Good) emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Monitoring started: %1").arg (m_nodeId)); else emit BobinkClient::instance () -> statusMessage (QStringLiteral ("Monitoring failed for %1: 0x%2") .arg (m_nodeId) .arg (static_cast (statusCode), 8, 16, QLatin1Char ('0'))); } void BobinkNode::handleDisableMonitoringFinished (QOpcUa::NodeAttribute, QOpcUa::UaStatusCode statusCode) { if (!BobinkClient::instance ()) return; if (statusCode == QOpcUa::Good) emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Monitoring stopped: %1").arg (m_nodeId)); else emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Stop monitoring failed for %1: 0x%2") .arg (m_nodeId) .arg (static_cast (statusCode), 8, 16, QLatin1Char ('0'))); } /* ------------------------------------------------------------------ */ /* 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 = { { QStringLiteral ("NodeId"), QOpcUa::NodeAttribute::NodeId }, { QStringLiteral ("NodeClass"), QOpcUa::NodeAttribute::NodeClass }, { QStringLiteral ("BrowseName"), QOpcUa::NodeAttribute::BrowseName }, { QStringLiteral ("DisplayName"), QOpcUa::NodeAttribute::DisplayName }, { QStringLiteral ("Description"), QOpcUa::NodeAttribute::Description }, { QStringLiteral ("Value"), QOpcUa::NodeAttribute::Value }, { QStringLiteral ("DataType"), QOpcUa::NodeAttribute::DataType }, { QStringLiteral ("ValueRank"), QOpcUa::NodeAttribute::ValueRank }, { QStringLiteral ("ArrayDimensions"), QOpcUa::NodeAttribute::ArrayDimensions }, { QStringLiteral ("AccessLevel"), QOpcUa::NodeAttribute::AccessLevel }, { QStringLiteral ("UserAccessLevel"), QOpcUa::NodeAttribute::UserAccessLevel }, { QStringLiteral ("MinimumSamplingInterval"), QOpcUa::NodeAttribute::MinimumSamplingInterval }, { QStringLiteral ("Historizing"), QOpcUa::NodeAttribute::Historizing }, { QStringLiteral ("Executable"), QOpcUa::NodeAttribute::Executable }, { QStringLiteral ("UserExecutable"), QOpcUa::NodeAttribute::UserExecutable }, }; return map.value (name, QOpcUa::NodeAttribute::None); } QLatin1StringView BobinkNode::nameFromAttribute (QOpcUa::NodeAttribute attr) { switch (attr) { case QOpcUa::NodeAttribute::NodeId: return "NodeId"_L1; case QOpcUa::NodeAttribute::NodeClass: return "NodeClass"_L1; case QOpcUa::NodeAttribute::BrowseName: return "BrowseName"_L1; case QOpcUa::NodeAttribute::DisplayName: return "DisplayName"_L1; case QOpcUa::NodeAttribute::Description: return "Description"_L1; case QOpcUa::NodeAttribute::Value: return "Value"_L1; case QOpcUa::NodeAttribute::DataType: return "DataType"_L1; case QOpcUa::NodeAttribute::ValueRank: return "ValueRank"_L1; case QOpcUa::NodeAttribute::ArrayDimensions: return "ArrayDimensions"_L1; case QOpcUa::NodeAttribute::AccessLevel: return "AccessLevel"_L1; case QOpcUa::NodeAttribute::UserAccessLevel: return "UserAccessLevel"_L1; case QOpcUa::NodeAttribute::MinimumSamplingInterval: return "MinimumSamplingInterval"_L1; case QOpcUa::NodeAttribute::Historizing: return "Historizing"_L1; case QOpcUa::NodeAttribute::Executable: return "Executable"_L1; case QOpcUa::NodeAttribute::UserExecutable: return "UserExecutable"_L1; default: return "Unknown"_L1; } }