summaryrefslogtreecommitdiffstats
path: root/src/OpcUaMonitoredNode.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/OpcUaMonitoredNode.cpp')
-rw-r--r--src/OpcUaMonitoredNode.cpp507
1 files changed, 507 insertions, 0 deletions
diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp
new file mode 100644
index 0000000..1a2da86
--- /dev/null
+++ b/src/OpcUaMonitoredNode.cpp
@@ -0,0 +1,507 @@
+/**
+ * @file OpcUaMonitoredNode.cpp
+ * @brief OpcUaMonitoredNode implementation.
+ */
+#include "OpcUaMonitoredNode.h"
+#include "OpcUaClient.h"
+
+#include <QMetaEnum>
+#include <QOpcUaLocalizedText>
+#include <QOpcUaMonitoringParameters>
+#include <QStringList>
+
+OpcUaMonitoredNode::OpcUaMonitoredNode (QObject *parent) : QObject (parent) {}
+
+/* ======================================
+ * Properties
+ * ====================================== */
+
+QString
+OpcUaMonitoredNode::nodeId () const
+{
+ return m_nodeId;
+}
+
+void
+OpcUaMonitoredNode::setNodeId (const QString &id)
+{
+ if (m_nodeId == id)
+ return;
+ m_nodeId = id;
+ emit nodeIdChanged ();
+
+ if (!m_componentComplete)
+ return;
+
+ teardownNode ();
+ auto *client = OpcUaClient::instance ();
+ if (client && client->connected ())
+ setupNode ();
+}
+
+bool
+OpcUaMonitoredNode::monitored () const
+{
+ return m_monitored;
+}
+
+void
+OpcUaMonitoredNode::setMonitored (bool monitored)
+{
+ if (m_monitored == monitored)
+ return;
+ m_monitored = monitored;
+ emit monitoredChanged ();
+
+ if (!m_componentComplete || !m_node)
+ return;
+
+ if (m_monitored)
+ startMonitoring ();
+ else
+ stopMonitoring ();
+}
+
+double
+OpcUaMonitoredNode::publishingInterval () const
+{
+ return m_publishingInterval;
+}
+
+void
+OpcUaMonitoredNode::setPublishingInterval (double interval)
+{
+ if (qFuzzyCompare (m_publishingInterval, interval))
+ return;
+ m_publishingInterval = interval;
+ emit publishingIntervalChanged ();
+}
+
+QVariant
+OpcUaMonitoredNode::value () const
+{
+ return m_value;
+}
+
+bool
+OpcUaMonitoredNode::writable () const
+{
+ return m_writable;
+}
+
+OpcUaNodeInfo
+OpcUaMonitoredNode::info () const
+{
+ return m_info;
+}
+
+/* ======================================
+ * QQmlParserStatus
+ * ====================================== */
+
+void
+OpcUaMonitoredNode::classBegin ()
+{
+}
+
+void
+OpcUaMonitoredNode::componentComplete ()
+{
+ m_componentComplete = true;
+
+ auto *client = OpcUaClient::instance ();
+ if (!client)
+ return;
+
+ connect (client, &OpcUaClient::connectedChanged, this,
+ &OpcUaMonitoredNode::handleClientConnectedChanged);
+
+ if (client->connected () && !m_nodeId.isEmpty ())
+ setupNode ();
+}
+
+/* ======================================
+ * Node lifecycle
+ * ====================================== */
+
+void
+OpcUaMonitoredNode::setupNode ()
+{
+ if (m_node || m_nodeId.isEmpty ())
+ return;
+
+ auto *client = OpcUaClient::instance ();
+ if (!client || !client->connected ())
+ return;
+
+ m_node = client->opcuaClient ()->node (m_nodeId);
+ if (!m_node)
+ return;
+
+ m_node->setParent (this);
+
+ connect (m_node, &QOpcUaNode::attributeUpdated, this,
+ &OpcUaMonitoredNode::handleAttributeUpdated);
+ connect (m_node, &QOpcUaNode::valueAttributeUpdated, this,
+ &OpcUaMonitoredNode::handleValueUpdated);
+ connect (m_node, &QOpcUaNode::attributeWritten, this,
+ &OpcUaMonitoredNode::handleAttributeWritten);
+ connect (m_node, &QOpcUaNode::enableMonitoringFinished, this,
+ &OpcUaMonitoredNode::handleEnableMonitoringFinished);
+ connect (m_node, &QOpcUaNode::disableMonitoringFinished, this,
+ &OpcUaMonitoredNode::handleDisableMonitoringFinished);
+
+ m_node->readAttributes (
+ QOpcUa::NodeAttribute::DisplayName | QOpcUa::NodeAttribute::Description
+ | QOpcUa::NodeAttribute::NodeClass | QOpcUa::NodeAttribute::DataType
+ | QOpcUa::NodeAttribute::AccessLevel);
+
+ if (m_monitored)
+ startMonitoring ();
+}
+
+void
+OpcUaMonitoredNode::teardownNode ()
+{
+ if (!m_node)
+ return;
+
+ delete m_node;
+ m_node = nullptr;
+
+ m_value.clear ();
+ emit valueChanged ();
+
+ m_info = OpcUaNodeInfo{};
+ emit infoChanged ();
+
+ m_valueType = QOpcUa::Types::Undefined;
+
+ if (m_writable)
+ {
+ m_writable = false;
+ emit writableChanged ();
+ }
+}
+
+void
+OpcUaMonitoredNode::startMonitoring ()
+{
+ if (!m_node || !m_monitored)
+ return;
+ QOpcUaMonitoringParameters params (m_publishingInterval);
+ m_node->enableMonitoring (QOpcUa::NodeAttribute::Value, params);
+}
+
+void
+OpcUaMonitoredNode::stopMonitoring ()
+{
+ if (!m_node)
+ return;
+ m_node->disableMonitoring (QOpcUa::NodeAttribute::Value);
+}
+
+/* ======================================
+ * Signal handlers
+ * ====================================== */
+
+void
+OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
+ const QVariant &value)
+{
+ switch (attr)
+ {
+ case QOpcUa::NodeAttribute::DisplayName:
+ m_info.displayName = value.value<QOpcUaLocalizedText> ().text ();
+ break;
+ case QOpcUa::NodeAttribute::Description:
+ m_info.description = value.value<QOpcUaLocalizedText> ().text ();
+ break;
+ case QOpcUa::NodeAttribute::NodeClass:
+ {
+ auto me = QMetaEnum::fromType<QOpcUa::NodeClass> ();
+ auto nc = static_cast<QOpcUa::NodeClass> (value.toInt ());
+ const char *key = me.valueToKey (static_cast<int> (nc));
+ m_info.nodeClass
+ = key ? QLatin1StringView (key) : QStringLiteral ("Unknown");
+ }
+ break;
+ case QOpcUa::NodeAttribute::DataType:
+ {
+ const QString rawId = value.toString ();
+ m_valueType = QOpcUa::opcUaDataTypeToQOpcUaType (rawId);
+ auto ns0 = QOpcUa::namespace0IdFromNodeId (rawId);
+ m_info.dataType = QOpcUa::namespace0IdName (ns0);
+ if (m_info.dataType.isEmpty ())
+ m_info.dataType = rawId;
+ }
+ break;
+ case QOpcUa::NodeAttribute::AccessLevel:
+ {
+ quint32 bits = value.toUInt ();
+ bool newWritable
+ = (bits & quint32 (QOpcUa::AccessLevelBit::CurrentWrite)) != 0;
+ if (m_writable != newWritable)
+ {
+ m_writable = newWritable;
+ emit writableChanged ();
+ }
+ QStringList flags;
+ if (bits & quint32 (QOpcUa::AccessLevelBit::CurrentRead))
+ flags << QStringLiteral ("Read");
+ if (bits & quint32 (QOpcUa::AccessLevelBit::CurrentWrite))
+ flags << QStringLiteral ("Write");
+ if (bits & quint32 (QOpcUa::AccessLevelBit::HistoryRead))
+ flags << QStringLiteral ("History Read");
+ if (bits & quint32 (QOpcUa::AccessLevelBit::HistoryWrite))
+ flags << QStringLiteral ("History Write");
+ m_info.accessLevel = flags.isEmpty ()
+ ? QStringLiteral ("None")
+ : flags.join (QStringLiteral (", "));
+ }
+ break;
+ default:
+ return; // Skip infoChanged for attributes we don't track.
+ }
+ emit infoChanged ();
+}
+
+void
+OpcUaMonitoredNode::handleValueUpdated (const QVariant &value)
+{
+ m_value = value;
+ emit valueChanged ();
+
+ m_info.status = QOpcUa::statusToString (m_node->valueAttributeError ());
+ m_info.sourceTimestamp
+ = m_node->sourceTimestamp (QOpcUa::NodeAttribute::Value);
+ m_info.serverTimestamp
+ = m_node->serverTimestamp (QOpcUa::NodeAttribute::Value);
+ emit infoChanged ();
+}
+
+void
+OpcUaMonitoredNode::handleClientConnectedChanged ()
+{
+ auto *client = OpcUaClient::instance ();
+ if (!client)
+ return;
+
+ if (client->connected ())
+ {
+ if (!m_nodeId.isEmpty ())
+ setupNode ();
+ }
+ else
+ {
+ teardownNode ();
+ }
+}
+
+void
+OpcUaMonitoredNode::handleAttributeWritten (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+
+ bool ok = (statusCode == QOpcUa::Good);
+ emit writeCompleted (ok, ok ? QStringLiteral ("Write successful")
+ : QOpcUa::statusToString (statusCode));
+}
+
+void
+OpcUaMonitoredNode::handleEnableMonitoringFinished (
+ QOpcUa::NodeAttribute attr, QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+ if (statusCode != QOpcUa::Good)
+ qWarning () << "OpcUaMonitoredNode: enableMonitoring failed for"
+ << m_nodeId << ":" << QOpcUa::statusToString (statusCode);
+}
+
+void
+OpcUaMonitoredNode::handleDisableMonitoringFinished (
+ QOpcUa::NodeAttribute attr, QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+ if (statusCode != QOpcUa::Good)
+ qWarning () << "OpcUaMonitoredNode: disableMonitoring failed for"
+ << m_nodeId << ":" << QOpcUa::statusToString (statusCode);
+}
+
+/* ======================================
+ * Write support
+ * ====================================== */
+
+/**
+ * @brief Convert a QML value to the C++ type matching m_valueType.
+ *
+ * Returns an invalid QVariant if conversion fails (e.g. non-numeric
+ * string for an integer node, or out-of-range for narrow types).
+ * Arrays are handled by recursing over each element.
+ */
+QVariant
+OpcUaMonitoredNode::coerceValue (const QVariant &input) const
+{
+ if (input.metaType ().id () == QMetaType::QVariantList)
+ {
+ QVariantList list = input.toList ();
+ QVariantList result;
+ result.reserve (list.size ());
+ for (const QVariant &item : list)
+ {
+ QVariant coerced = coerceValue (item);
+ if (!coerced.isValid ())
+ return {};
+ result.append (coerced);
+ }
+ return QVariant::fromValue (result);
+ }
+
+ switch (m_valueType)
+ {
+ case QOpcUa::Boolean:
+ {
+ if (input.metaType ().id () == QMetaType::Bool)
+ return QVariant (input.toBool ());
+ QString s = input.toString ().trimmed ().toLower ();
+ if (s == QStringLiteral ("true") || s == QStringLiteral ("1"))
+ return QVariant (true);
+ if (s == QStringLiteral ("false") || s == QStringLiteral ("0"))
+ return QVariant (false);
+ return {};
+ }
+ case QOpcUa::SByte:
+ {
+ bool ok = false;
+ qint32 v = input.toInt (&ok);
+ if (!ok || v < -128 || v > 127)
+ return {};
+ return QVariant::fromValue (static_cast<qint8> (v));
+ }
+ case QOpcUa::Byte:
+ {
+ bool ok = false;
+ quint32 v = input.toUInt (&ok);
+ if (!ok || v > 255)
+ return {};
+ return QVariant::fromValue (static_cast<quint8> (v));
+ }
+ case QOpcUa::Int16:
+ {
+ bool ok = false;
+ qint32 v = input.toInt (&ok);
+ if (!ok || v < -32768 || v > 32767)
+ return {};
+ return QVariant::fromValue (static_cast<qint16> (v));
+ }
+ case QOpcUa::UInt16:
+ {
+ bool ok = false;
+ quint32 v = input.toUInt (&ok);
+ if (!ok || v > 65535)
+ return {};
+ return QVariant::fromValue (static_cast<quint16> (v));
+ }
+ case QOpcUa::Int32:
+ {
+ bool ok = false;
+ qint32 v = input.toInt (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::UInt32:
+ {
+ bool ok = false;
+ quint32 v = input.toUInt (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::Int64:
+ {
+ bool ok = false;
+ qint64 v = input.toLongLong (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::UInt64:
+ {
+ bool ok = false;
+ quint64 v = input.toULongLong (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::Float:
+ {
+ bool ok = false;
+ float v = input.toFloat (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::Double:
+ {
+ bool ok = false;
+ double v = input.toDouble (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::String:
+ return QVariant (input.toString ());
+ case QOpcUa::DateTime:
+ {
+ QDateTime dt = input.toDateTime ();
+ return dt.isValid () ? QVariant::fromValue (dt) : QVariant{};
+ }
+ case QOpcUa::ByteString:
+ return QVariant::fromValue (input.toByteArray ());
+ default:
+ return input;
+ }
+}
+
+void
+OpcUaMonitoredNode::writeValue (const QVariant &value)
+{
+ if (!m_node)
+ {
+ emit writeCompleted (false, QStringLiteral ("Node not connected"));
+ return;
+ }
+ if (!m_writable)
+ {
+ emit writeCompleted (false, QStringLiteral ("Node is read-only"));
+ return;
+ }
+ if (m_valueType == QOpcUa::Types::Undefined)
+ {
+ emit writeCompleted (false,
+ QStringLiteral ("Data type not yet resolved"));
+ return;
+ }
+
+ QVariant toWrite = value;
+
+ // QML text fields always send QString. For array nodes (current
+ // value is QVariantList) split "1, 2, 3" into a list first so
+ // coerceValue can handle each element individually.
+ if (m_value.metaType ().id () == QMetaType::QVariantList
+ && value.metaType ().id () == QMetaType::QString)
+ {
+ QStringList parts
+ = value.toString ().split (QLatin1Char (','), Qt::SkipEmptyParts);
+ QVariantList list;
+ list.reserve (parts.size ());
+ for (const QString &part : parts)
+ list.append (QVariant (part.trimmed ()));
+ toWrite = QVariant::fromValue (list);
+ }
+
+ QVariant coerced = coerceValue (toWrite);
+ if (!coerced.isValid ())
+ {
+ emit writeCompleted (
+ false, QStringLiteral ("Cannot convert value to node data type"));
+ return;
+ }
+
+ if (!m_node->writeValueAttribute (coerced, m_valueType))
+ emit writeCompleted (false,
+ QStringLiteral ("Write request failed to dispatch"));
+}