diff options
| -rw-r--r-- | demo/Main.qml | 10 | ||||
| -rw-r--r-- | demo/NodePage.qml | 282 | ||||
| -rw-r--r-- | src/BobinkNode.cpp | 410 | ||||
| -rw-r--r-- | src/BobinkNode.h | 111 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 12 | ||||
| -rw-r--r-- | src/OpcUaAuth.cpp (renamed from src/BobinkAuth.cpp) | 32 | ||||
| -rw-r--r-- | src/OpcUaAuth.h (renamed from src/BobinkAuth.h) | 12 | ||||
| -rw-r--r-- | src/OpcUaClient.cpp (renamed from src/BobinkClient.cpp) | 110 | ||||
| -rw-r--r-- | src/OpcUaClient.h (renamed from src/BobinkClient.h) | 26 | ||||
| -rw-r--r-- | src/OpcUaMonitoredNode.cpp | 437 | ||||
| -rw-r--r-- | src/OpcUaMonitoredNode.h | 121 |
11 files changed, 832 insertions, 731 deletions
diff --git a/demo/Main.qml b/demo/Main.qml index 9d3e168..ca0a419 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -82,7 +82,7 @@ ApplicationWindow { Bobink.startDiscovery(); } - BobinkAuth { + OpcUaAuth { id: auth mode: authModeCombo.currentValue username: usernameField.text @@ -267,22 +267,22 @@ ApplicationWindow { model: [ { text: "Anonymous", - mode: BobinkAuth.Anonymous + mode: OpcUaAuth.Anonymous }, { text: "Username / Password", - mode: BobinkAuth.UserPass + mode: OpcUaAuth.UserPass }, { text: "Certificate", - mode: BobinkAuth.Certificate + mode: OpcUaAuth.Certificate } ] } GridLayout { columns: 2 - visible: authModeCombo.currentValue === BobinkAuth.UserPass + visible: authModeCombo.currentValue === OpcUaAuth.UserPass Layout.fillWidth: true Label { diff --git a/demo/NodePage.qml b/demo/NodePage.qml index fd81db5..442a6fd 100644 --- a/demo/NodePage.qml +++ b/demo/NodePage.qml @@ -1,6 +1,4 @@ -// NodePage.qml — Monitors a single OPC UA node. -// Two instances demonstrate visibility lifecycle: switching pages -// stops monitoring on the hidden page automatically. +// NodePage.qml — Displays 10 OPC UA nodes per page with tooltips. import QtQuick import QtQuick.Controls @@ -14,35 +12,75 @@ Page { required property int pageNumber required property var logFunction - property string monitoredNodeId: "" - property string nodeDescription: "" - property bool nodeWritable: true - property bool metadataLoaded: false - - BobinkNode { - id: node - nodeId: nodePage.monitoredNodeId - onAttributeRead: (name, val) => { - if (name === "DisplayName") - nodePage.nodeDescription = val.toString(); - else if (name === "AccessLevel") - nodePage.nodeWritable = (val & 0x02) !== 0; - nodePage.metadataLoaded = true; + readonly property var pages: [ + { + title: "Read-Write Scalars", + nodes: [ + "ns=1;s=bool_rw_scalar", + "ns=1;s=int16_rw_scalar", + "ns=1;s=uint16_rw_scalar", + "ns=1;s=int32_rw_scalar", + "ns=1;s=uint32_rw_scalar", + "ns=1;s=int64_rw_scalar", + "ns=1;s=uint64_rw_scalar", + "ns=1;s=float_rw_scalar", + "ns=1;s=double_rw_scalar", + "ns=1;s=string_rw_scalar" + ] + }, + { + title: "Read-Only Scalars", + nodes: [ + "ns=1;s=bool_ro_scalar", + "ns=1;s=int16_ro_scalar", + "ns=1;s=uint16_ro_scalar", + "ns=1;s=int32_ro_scalar", + "ns=1;s=uint32_ro_scalar", + "ns=1;s=int64_ro_scalar", + "ns=1;s=uint64_ro_scalar", + "ns=1;s=float_ro_scalar", + "ns=1;s=double_ro_scalar", + "ns=1;s=string_ro_scalar" + ] + }, + { + title: "Read-Write Arrays", + nodes: [ + "ns=1;s=bool_rw_array", + "ns=1;s=int16_rw_array", + "ns=1;s=uint16_rw_array", + "ns=1;s=int32_rw_array", + "ns=1;s=uint32_rw_array", + "ns=1;s=int64_rw_array", + "ns=1;s=uint64_rw_array", + "ns=1;s=float_rw_array", + "ns=1;s=double_rw_array", + "ns=1;s=string_rw_array" + ] } - onWriteError: (message) => nodePage.logFunction("WRITE: " + message) - } + ] + + readonly property var currentPage: pages[pageNumber - 1] + + Component.onCompleted: nodePage.logFunction( + currentPage.title + " page loaded (" + currentPage.nodes.length + " nodes)") ColumnLayout { anchors.fill: parent anchors.margins: 20 - spacing: 12 + spacing: 4 + // Header RowLayout { Label { - text: "Node Page " + nodePage.pageNumber + text: currentPage.title font.bold: true font.pointSize: 14 } + Label { + text: "(" + nodePage.pageNumber + "/3)" + color: "gray" + } Item { Layout.fillWidth: true } Button { text: "Disconnect" @@ -50,114 +88,140 @@ Page { } } + // Column headers RowLayout { - TextField { - id: nodeIdField - Layout.fillWidth: true - placeholderText: "ns=2;s=Temperature" - enabled: !nodePage.monitoredNodeId + Layout.fillWidth: true + Layout.leftMargin: 12 + Layout.rightMargin: 12 + spacing: 12 + Label { + text: "Identifier" + font.bold: true + Layout.preferredWidth: 160 } - Button { - text: nodePage.monitoredNodeId ? "Stop" : "Monitor" - onClicked: { - if (nodePage.monitoredNodeId) { - nodePage.monitoredNodeId = ""; - nodePage.nodeDescription = ""; - nodePage.nodeWritable = true; - nodePage.metadataLoaded = false; - } else { - nodePage.monitoredNodeId = nodeIdField.text; - node.readAttribute("DisplayName"); - node.readAttribute("AccessLevel"); - } - } + Label { + text: "Value" + font.bold: true + Layout.preferredWidth: 160 + } + Label { + text: "Write" + font.bold: true + Layout.fillWidth: true } } - GroupBox { + Rectangle { + id: separator Layout.fillWidth: true - visible: nodePage.monitoredNodeId.length > 0 - title: nodePage.nodeDescription || nodePage.monitoredNodeId - - GridLayout { - columns: 2 - width: parent.width - - Label { text: "Value:" } - Label { - text: node.value !== undefined ? node.value.toString() : "—" - font.family: "monospace" - font.bold: true - } + height: 1 + color: "#ccc" + } - Label { text: "Status:" } - Label { - text: node.status === BobinkNode.Good ? "Good" - : node.status === BobinkNode.Uncertain ? "Uncertain" - : "Bad" - color: node.status === BobinkNode.Good ? "green" - : node.status === BobinkNode.Uncertain ? "orange" - : "red" - } + // Nodes + Repeater { + model: currentPage.nodes - Label { text: "Source TS:" } - Label { - text: node.sourceTimestamp.toLocaleString( - Qt.locale(), "HH:mm:ss.zzz") - font.family: "monospace" + ItemDelegate { + id: delegate + required property string modelData + required property int index + Layout.fillWidth: true + padding: 4 + leftPadding: 12 + rightPadding: 12 + + OpcUaMonitoredNode { + id: node + nodeId: delegate.modelData + monitored: nodePage.StackView.status === StackView.Active + onWriteCompleted: (success, message) => { + var short_id = node.nodeId.substring(node.nodeId.indexOf(";s=") + 3); + nodePage.logFunction(short_id + ": " + message); + } } - Label { text: "Server TS:" } - Label { - text: node.serverTimestamp.toLocaleString( - Qt.locale(), "HH:mm:ss.zzz") - font.family: "monospace" + background: Rectangle { + color: delegate.index % 2 === 0 ? "#f8f8f8" : "transparent" + radius: 2 } - } - } - RowLayout { - visible: nodePage.monitoredNodeId.length > 0 + contentItem: RowLayout { + spacing: 12 + + // Column 1: Identifier + Label { + text: { + var idx = node.nodeId.indexOf(";s="); + return idx >= 0 ? node.nodeId.substring(idx + 3) : node.nodeId; + } + Layout.preferredWidth: 160 + elide: Text.ElideRight + } - TextField { - id: writeField - Layout.fillWidth: true - enabled: nodePage.nodeWritable - placeholderText: nodePage.metadataLoaded && !nodePage.nodeWritable - ? "Read-only node" : "Value to write" - } - Button { - text: "Write" - enabled: nodePage.nodeWritable - onClicked: { - node.value = writeField.text; - writeField.clear(); + // Column 2: Live value (always visible) + Label { + text: node.value !== undefined && String(node.value) !== "" ? String(node.value) : "—" + Layout.preferredWidth: 160 + elide: Text.ElideRight + } + + // Column 3: Edit area (writable) or READ-ONLY label + TextField { + id: editField + visible: node.writable + Layout.fillWidth: true + placeholderText: "Enter value..." + onAccepted: node.writeValue(text) + } + Button { + id: writeButton + visible: node.writable + text: "Write" + onClicked: node.writeValue(editField.text) + } + Label { + visible: !node.writable + text: "(READ-ONLY)" + color: "gray" + font.italic: true + Layout.fillWidth: true + } } - } - } - Label { - visible: nodePage.monitoredNodeId.length > 0 - && nodePage.metadataLoaded && !nodePage.nodeWritable - text: "Read-only node" - font.italic: true - color: "gray" + ToolTip.visible: hovered + ToolTip.delay: 400 + ToolTip.text: + "Display Name: " + (node.info.displayName || "—") + + "\nDescription: " + (node.info.description || "—") + + "\nNode Class: " + (node.info.nodeClass || "—") + + "\nData Type: " + (node.info.dataType || "—") + + "\nAccess Level: " + node.info.accessLevel + + "\nStatus: " + (node.info.status || "—") + + "\nSource Time: " + (node.info.sourceTimestamp.toLocaleString() || "—") + + "\nServer Time: " + (node.info.serverTimestamp.toLocaleString() || "—") + } } Item { Layout.fillHeight: true } - Button { + // Navigation + RowLayout { Layout.fillWidth: true - text: nodePage.pageNumber === 1 ? "Go to Page 2" : "Back to Page 1" - onClicked: { - if (nodePage.pageNumber === 1) - nodePage.stackRef.push("NodePage.qml", { - stackRef: nodePage.stackRef, - pageNumber: 2, - logFunction: nodePage.logFunction - }); - else - nodePage.stackRef.pop(); + Button { + text: "← Previous" + visible: nodePage.pageNumber > 1 + onClicked: nodePage.stackRef.pop() + } + Item { Layout.fillWidth: true } + Button { + text: "Next →" + visible: nodePage.pageNumber < 3 + onClicked: nodePage.stackRef.push("NodePage.qml", { + stackRef: nodePage.stackRef, + pageNumber: nodePage.pageNumber + 1, + logFunction: nodePage.logFunction + }) } } } diff --git a/src/BobinkNode.cpp b/src/BobinkNode.cpp deleted file mode 100644 index 55e3b75..0000000 --- a/src/BobinkNode.cpp +++ /dev/null @@ -1,410 +0,0 @@ -/** - * @file BobinkNode.cpp - * @brief QML component representing a single OPC UA node. - */ -#include "BobinkNode.h" -#include "BobinkClient.h" - -#include <QOpcUaClient> -#include <QOpcUaMonitoringParameters> - -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<quint32> (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<QOpcUa::NodeAttribute> (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<quint32> (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<quint32> (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<quint32> (statusCode), 8, 16, - QLatin1Char ('0'))); -} - -/* ------------------------------------------------------------------ */ -/* 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 = { - { 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; - } -} diff --git a/src/BobinkNode.h b/src/BobinkNode.h deleted file mode 100644 index c37a883..0000000 --- a/src/BobinkNode.h +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file BobinkNode.h - * @brief QML component representing a single OPC UA node. - * - * Inherits QQuickItem so that monitoring is automatically tied to - * visibility: when the item (or any ancestor) becomes invisible - * (e.g. StackView navigates away, Loader unloads), the monitored - * item is removed from the subscription. When visible again, - * monitoring resumes. - */ -#ifndef BOBINKNODE_H -#define BOBINKNODE_H - -#include <QDateTime> -#include <QHash> -#include <QOpcUaNode> -#include <QQuickItem> - -class BobinkNode : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged) - Q_PROPERTY (QVariant value READ value WRITE setValue NOTIFY valueChanged) - Q_PROPERTY (NodeStatus status READ status NOTIFY statusChanged) - Q_PROPERTY (QDateTime sourceTimestamp READ sourceTimestamp NOTIFY - sourceTimestampChanged) - Q_PROPERTY (QDateTime serverTimestamp READ serverTimestamp NOTIFY - serverTimestampChanged) - -public: - explicit BobinkNode (QQuickItem *parent = nullptr); - ~BobinkNode () override; - - /// Simplified OPC UA status severity. - enum NodeStatus - { - Good, - Uncertain, - Bad - }; - Q_ENUM (NodeStatus) - - QString nodeId () const; - void setNodeId (const QString &id); - - QVariant value () const; - /** Setting value writes to the server; the property updates when - * the server confirms via the monitored data change. */ - void setValue (const QVariant &value); - - NodeStatus status () const; - QDateTime sourceTimestamp () const; - QDateTime serverTimestamp () const; - - /** Read an attribute on demand (DisplayName, Description, DataType, …). - * Result arrives asynchronously via attributeRead(). */ - Q_INVOKABLE void readAttribute (const QString &attributeName); - -signals: - void nodeIdChanged (); - void valueChanged (); - void statusChanged (); - void sourceTimestampChanged (); - void serverTimestampChanged (); - - /** Emitted when a readAttribute() call completes. */ - void attributeRead (const QString &attributeName, const QVariant &value); - - /** Emitted when a write to the server fails. */ - void writeError (const QString &message); - -protected: - void componentComplete () override; - void itemChange (ItemChange change, const ItemChangeData &data) override; - -private: - void startMonitoring (); - void stopMonitoring (); - - void handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val); - void handleAttributeUpdated (QOpcUa::NodeAttribute attr, - const QVariant &val); - void handleAttributeWritten (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode); - void handleClientConnectedChanged (); - void handleAttributeReadFinished (QOpcUa::NodeAttributes attrs); - void handleEnableMonitoringFinished (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode); - void handleDisableMonitoringFinished (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode); - - static NodeStatus statusFromCode (QOpcUa::UaStatusCode code); - static QOpcUa::NodeAttribute attributeFromName (const QString &name); - static QLatin1StringView nameFromAttribute (QOpcUa::NodeAttribute attr); - - QString m_nodeId; - QVariant m_value; - NodeStatus m_status = Bad; - QDateTime m_sourceTimestamp; - QDateTime m_serverTimestamp; - - QOpcUaNode *m_opcuaNode = nullptr; - bool m_componentComplete = false; - - /** Tracks in-flight readAttribute() requests (enum → original name). */ - QHash<QOpcUa::NodeAttribute, QString> m_pendingReads; -}; - -#endif // BOBINKNODE_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1292194..b0781cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,12 +7,12 @@ qt_add_qml_module( VERSION 1.0 SOURCES - BobinkAuth.h - BobinkAuth.cpp - BobinkClient.h - BobinkClient.cpp - BobinkNode.h - BobinkNode.cpp + OpcUaAuth.h + OpcUaAuth.cpp + OpcUaClient.h + OpcUaClient.cpp + OpcUaMonitoredNode.h + OpcUaMonitoredNode.cpp OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml/Bobink") diff --git a/src/BobinkAuth.cpp b/src/OpcUaAuth.cpp index 132e4be..258c9c7 100644 --- a/src/BobinkAuth.cpp +++ b/src/OpcUaAuth.cpp @@ -1,19 +1,19 @@ /** - * @file BobinkAuth.cpp - * @brief BobinkAuth implementation. + * @file OpcUaAuth.cpp + * @brief OpcUaAuth implementation. */ -#include "BobinkAuth.h" +#include "OpcUaAuth.h" -BobinkAuth::BobinkAuth (QObject *parent) : QObject (parent) {} +OpcUaAuth::OpcUaAuth (QObject *parent) : QObject (parent) {} -BobinkAuth::AuthMode -BobinkAuth::mode () const +OpcUaAuth::AuthMode +OpcUaAuth::mode () const { return m_mode; } void -BobinkAuth::setMode (AuthMode mode) +OpcUaAuth::setMode (AuthMode mode) { if (m_mode == mode) return; @@ -22,13 +22,13 @@ BobinkAuth::setMode (AuthMode mode) } QString -BobinkAuth::username () const +OpcUaAuth::username () const { return m_username; } void -BobinkAuth::setUsername (const QString &username) +OpcUaAuth::setUsername (const QString &username) { if (m_username == username) return; @@ -37,13 +37,13 @@ BobinkAuth::setUsername (const QString &username) } QString -BobinkAuth::password () const +OpcUaAuth::password () const { return m_password; } void -BobinkAuth::setPassword (const QString &password) +OpcUaAuth::setPassword (const QString &password) { if (m_password == password) return; @@ -52,13 +52,13 @@ BobinkAuth::setPassword (const QString &password) } QString -BobinkAuth::certPath () const +OpcUaAuth::certPath () const { return m_certPath; } void -BobinkAuth::setCertPath (const QString &path) +OpcUaAuth::setCertPath (const QString &path) { if (m_certPath == path) return; @@ -67,13 +67,13 @@ BobinkAuth::setCertPath (const QString &path) } QString -BobinkAuth::keyPath () const +OpcUaAuth::keyPath () const { return m_keyPath; } void -BobinkAuth::setKeyPath (const QString &path) +OpcUaAuth::setKeyPath (const QString &path) { if (m_keyPath == path) return; @@ -82,7 +82,7 @@ BobinkAuth::setKeyPath (const QString &path) } QOpcUaAuthenticationInformation -BobinkAuth::toAuthenticationInformation () const +OpcUaAuth::toAuthenticationInformation () const { QOpcUaAuthenticationInformation info; switch (m_mode) diff --git a/src/BobinkAuth.h b/src/OpcUaAuth.h index 2bd3c05..8d53c1d 100644 --- a/src/BobinkAuth.h +++ b/src/OpcUaAuth.h @@ -1,15 +1,15 @@ /** - * @file BobinkAuth.h + * @file OpcUaAuth.h * @brief QML component for OPC UA authentication configuration. */ -#ifndef BOBINKAUTH_H -#define BOBINKAUTH_H +#ifndef OPCUAAUTH_H +#define OPCUAAUTH_H #include <QObject> #include <QOpcUaAuthenticationInformation> #include <QQmlEngine> -class BobinkAuth : public QObject +class OpcUaAuth : public QObject { Q_OBJECT QML_ELEMENT @@ -34,7 +34,7 @@ public: }; Q_ENUM (AuthMode) - explicit BobinkAuth (QObject *parent = nullptr); + explicit OpcUaAuth (QObject *parent = nullptr); AuthMode mode () const; void setMode (AuthMode mode); @@ -72,4 +72,4 @@ private: QString m_keyPath; }; -#endif // BOBINKAUTH_H +#endif // OPCUAAUTH_H diff --git a/src/BobinkClient.cpp b/src/OpcUaClient.cpp index 41b7dbf..91ce47b 100644 --- a/src/BobinkClient.cpp +++ b/src/OpcUaClient.cpp @@ -1,9 +1,9 @@ /** - * @file BobinkClient.cpp - * @brief BobinkClient implementation. + * @file OpcUaClient.cpp + * @brief OpcUaClient implementation. */ -#include "BobinkClient.h" -#include "BobinkAuth.h" +#include "OpcUaClient.h" +#include "OpcUaAuth.h" #include <QDir> #include <QMetaEnum> @@ -35,17 +35,17 @@ ensurePkiDirs (const QString &base) } static QString -securityPolicyUri (BobinkClient::SecurityPolicy policy) +securityPolicyUri (OpcUaClient::SecurityPolicy policy) { switch (policy) { - case BobinkClient::Basic256Sha256: + case OpcUaClient::Basic256Sha256: return QStringLiteral ( "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); - case BobinkClient::Aes128_Sha256_RsaOaep: + case OpcUaClient::Aes128_Sha256_RsaOaep: return QStringLiteral ( "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"); - case BobinkClient::Aes256_Sha256_RsaPss: + case OpcUaClient::Aes256_Sha256_RsaPss: return QStringLiteral ( "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss"); } @@ -56,7 +56,7 @@ securityPolicyUri (BobinkClient::SecurityPolicy policy) * Construction * ====================================== */ -BobinkClient::BobinkClient (QObject *parent) +OpcUaClient::OpcUaClient (QObject *parent) : QObject (parent), m_provider (new QOpcUaProvider (this)), m_pkiDir (defaultPkiDir ()) { @@ -66,56 +66,56 @@ BobinkClient::BobinkClient (QObject *parent) autoDetectPki (); applyPki (); connect (&m_discoveryTimer, &QTimer::timeout, this, - &BobinkClient::doDiscovery); + &OpcUaClient::doDiscovery); s_instance = this; } void -BobinkClient::setupClient () +OpcUaClient::setupClient () { m_client = m_provider->createClient (QStringLiteral ("open62541")); if (!m_client) { - qWarning () << "BobinkClient: failed to create open62541 backend"; + qWarning () << "OpcUaClient: failed to create open62541 backend"; return; } connect (m_client, &QOpcUaClient::stateChanged, this, - &BobinkClient::handleStateChanged); + &OpcUaClient::handleStateChanged); connect (m_client, &QOpcUaClient::endpointsRequestFinished, this, - &BobinkClient::handleEndpointsReceived); + &OpcUaClient::handleEndpointsReceived); connect (m_client, &QOpcUaClient::connectError, this, - &BobinkClient::handleConnectError); + &OpcUaClient::handleConnectError); connect (m_client, &QOpcUaClient::findServersFinished, this, - &BobinkClient::handleFindServersFinished); + &OpcUaClient::handleFindServersFinished); connect (m_client, &QOpcUaClient::errorChanged, this, - &BobinkClient::handleClientError); + &OpcUaClient::handleClientError); } /* ====================================== * Connection * ====================================== */ -BobinkClient * -BobinkClient::instance () +OpcUaClient * +OpcUaClient::instance () { return s_instance; } bool -BobinkClient::connected () const +OpcUaClient::connected () const { return m_connected; } QString -BobinkClient::serverUrl () const +OpcUaClient::serverUrl () const { return m_serverUrl; } void -BobinkClient::setServerUrl (const QString &url) +OpcUaClient::setServerUrl (const QString &url) { if (m_serverUrl == url) return; @@ -123,14 +123,14 @@ BobinkClient::setServerUrl (const QString &url) emit serverUrlChanged (); } -BobinkAuth * -BobinkClient::auth () const +OpcUaAuth * +OpcUaClient::auth () const { return m_auth; } void -BobinkClient::setAuth (BobinkAuth *auth) +OpcUaClient::setAuth (OpcUaAuth *auth) { if (m_auth == auth) return; @@ -139,13 +139,13 @@ BobinkClient::setAuth (BobinkAuth *auth) } QOpcUaClient * -BobinkClient::opcuaClient () const +OpcUaClient::opcuaClient () const { return m_client; } void -BobinkClient::connectToServer () +OpcUaClient::connectToServer () { if (!m_client) { @@ -175,7 +175,7 @@ BobinkClient::connectToServer () } void -BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode) +OpcUaClient::connectDirect (SecurityPolicy policy, SecurityMode mode) { if (!m_client) { @@ -205,15 +205,15 @@ BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode) { switch (m_auth->mode ()) { - case BobinkAuth::Anonymous: + case OpcUaAuth::Anonymous: tokenPolicy.setTokenType ( QOpcUaUserTokenPolicy::TokenType::Anonymous); break; - case BobinkAuth::UserPass: + case OpcUaAuth::UserPass: tokenPolicy.setTokenType ( QOpcUaUserTokenPolicy::TokenType::Username); break; - case BobinkAuth::Certificate: + case OpcUaAuth::Certificate: tokenPolicy.setTokenType ( QOpcUaUserTokenPolicy::TokenType::Certificate); break; @@ -231,14 +231,14 @@ BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode) } void -BobinkClient::disconnectFromServer () +OpcUaClient::disconnectFromServer () { if (m_client) m_client->disconnectFromEndpoint (); } void -BobinkClient::acceptCertificate () +OpcUaClient::acceptCertificate () { m_certAccepted = true; if (m_certLoop) @@ -246,7 +246,7 @@ BobinkClient::acceptCertificate () } void -BobinkClient::rejectCertificate () +OpcUaClient::rejectCertificate () { m_certAccepted = false; if (m_certLoop) @@ -254,7 +254,7 @@ BobinkClient::rejectCertificate () } void -BobinkClient::handleStateChanged (QOpcUaClient::ClientState state) +OpcUaClient::handleStateChanged (QOpcUaClient::ClientState state) { bool nowConnected = (state == QOpcUaClient::Connected); if (m_connected != nowConnected) @@ -265,7 +265,7 @@ BobinkClient::handleStateChanged (QOpcUaClient::ClientState state) } void -BobinkClient::handleEndpointsReceived ( +OpcUaClient::handleEndpointsReceived ( const QList<QOpcUaEndpointDescription> &endpoints, QOpcUa::UaStatusCode statusCode, const QUrl &) { @@ -291,7 +291,7 @@ BobinkClient::handleEndpointsReceived ( } void -BobinkClient::handleConnectError (QOpcUaErrorState *errorState) +OpcUaClient::handleConnectError (QOpcUaErrorState *errorState) { if (errorState->connectionStep () == QOpcUaErrorState::ConnectionStep::CertificateValidation) @@ -324,7 +324,7 @@ BobinkClient::handleConnectError (QOpcUaErrorState *errorState) } void -BobinkClient::handleClientError (QOpcUaClient::ClientError error) +OpcUaClient::handleClientError (QOpcUaClient::ClientError error) { if (error == QOpcUaClient::NoError) return; @@ -340,13 +340,13 @@ BobinkClient::handleClientError (QOpcUaClient::ClientError error) * ====================================== */ QString -BobinkClient::discoveryUrl () const +OpcUaClient::discoveryUrl () const { return m_discoveryUrl; } void -BobinkClient::setDiscoveryUrl (const QString &url) +OpcUaClient::setDiscoveryUrl (const QString &url) { if (m_discoveryUrl == url) return; @@ -355,25 +355,25 @@ BobinkClient::setDiscoveryUrl (const QString &url) } bool -BobinkClient::discovering () const +OpcUaClient::discovering () const { return m_discovering; } const QList<QOpcUaApplicationDescription> & -BobinkClient::discoveredServers () const +OpcUaClient::discoveredServers () const { return m_discoveredServers; } QVariantList -BobinkClient::servers () const +OpcUaClient::servers () const { return m_serversCache; } void -BobinkClient::startDiscovery () +OpcUaClient::startDiscovery () { if (m_discoveryUrl.isEmpty () || !m_client) return; @@ -389,7 +389,7 @@ BobinkClient::startDiscovery () } void -BobinkClient::stopDiscovery () +OpcUaClient::stopDiscovery () { m_discoveryTimer.stop (); @@ -401,7 +401,7 @@ BobinkClient::stopDiscovery () } void -BobinkClient::doDiscovery () +OpcUaClient::doDiscovery () { if (!m_client || m_discoveryUrl.isEmpty ()) return; @@ -412,7 +412,7 @@ BobinkClient::doDiscovery () } void -BobinkClient::handleFindServersFinished ( +OpcUaClient::handleFindServersFinished ( const QList<QOpcUaApplicationDescription> &servers, QOpcUa::UaStatusCode statusCode, const QUrl &) { @@ -438,13 +438,13 @@ BobinkClient::handleFindServersFinished ( * ====================================== */ QString -BobinkClient::pkiDir () const +OpcUaClient::pkiDir () const { return m_pkiDir; } void -BobinkClient::setPkiDir (const QString &path) +OpcUaClient::setPkiDir (const QString &path) { if (m_pkiDir == path) return; @@ -454,13 +454,13 @@ BobinkClient::setPkiDir (const QString &path) } QString -BobinkClient::certFile () const +OpcUaClient::certFile () const { return m_certFile; } void -BobinkClient::setCertFile (const QString &path) +OpcUaClient::setCertFile (const QString &path) { if (m_certFile == path) return; @@ -469,13 +469,13 @@ BobinkClient::setCertFile (const QString &path) } QString -BobinkClient::keyFile () const +OpcUaClient::keyFile () const { return m_keyFile; } void -BobinkClient::setKeyFile (const QString &path) +OpcUaClient::setKeyFile (const QString &path) { if (m_keyFile == path) return; @@ -484,7 +484,7 @@ BobinkClient::setKeyFile (const QString &path) } void -BobinkClient::autoDetectPki () +OpcUaClient::autoDetectPki () { if (m_pkiDir.isEmpty ()) return; @@ -503,7 +503,7 @@ BobinkClient::autoDetectPki () } void -BobinkClient::applyPki () +OpcUaClient::applyPki () { if (!m_client || m_pkiDir.isEmpty ()) return; diff --git a/src/BobinkClient.h b/src/OpcUaClient.h index 7eb6c3c..1476911 100644 --- a/src/BobinkClient.h +++ b/src/OpcUaClient.h @@ -1,13 +1,13 @@ /** - * @file BobinkClient.h + * @file OpcUaClient.h * @brief QML singleton managing the OPC UA connection lifecycle. * * Wraps QOpcUaClient into a declarative interface: LDS discovery, * endpoint selection, PKI, and certificate trust flow. * Single connection at a time (app-wide singleton). */ -#ifndef BOBINKCLIENT_H -#define BOBINKCLIENT_H +#ifndef OPCUACLIENT_H +#define OPCUACLIENT_H #include <QEventLoop> #include <QObject> @@ -19,9 +19,9 @@ #include <QQmlEngine> #include <QTimer> -class BobinkAuth; +class OpcUaAuth; -class BobinkClient : public QObject +class OpcUaClient : public QObject { Q_OBJECT QML_SINGLETON @@ -31,7 +31,7 @@ class BobinkClient : public QObject Q_PROPERTY (bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY (QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY serverUrlChanged) - Q_PROPERTY (BobinkAuth *auth READ auth WRITE setAuth NOTIFY authChanged) + Q_PROPERTY (OpcUaAuth *auth READ auth WRITE setAuth NOTIFY authChanged) /* -- Discovery -- */ Q_PROPERTY (QString discoveryUrl READ discoveryUrl WRITE setDiscoveryUrl @@ -65,7 +65,7 @@ public: }; Q_ENUM (SecurityPolicy) - BobinkClient (QObject *parent = nullptr); + OpcUaClient (QObject *parent = nullptr); /* -- Connection -- */ @@ -74,8 +74,8 @@ public: QString serverUrl () const; void setServerUrl (const QString &url); - BobinkAuth *auth () const; - void setAuth (BobinkAuth *auth); + OpcUaAuth *auth () const; + void setAuth (OpcUaAuth *auth); /** @brief Discover endpoints, pick the most secure, connect. */ Q_INVOKABLE void connectToServer (); @@ -119,7 +119,7 @@ public: /* -- C++ only -- */ - static BobinkClient *instance (); + static OpcUaClient *instance (); QOpcUaClient *opcuaClient () const; signals: @@ -169,7 +169,7 @@ private: /* -- Connection -- */ QOpcUaProvider *m_provider = nullptr; QOpcUaClient *m_client = nullptr; - BobinkAuth *m_auth = nullptr; + OpcUaAuth *m_auth = nullptr; QString m_serverUrl; bool m_connected = false; QEventLoop *m_certLoop = nullptr; @@ -187,7 +187,7 @@ private: QString m_certFile; QString m_keyFile; - static inline BobinkClient *s_instance = nullptr; + static inline OpcUaClient *s_instance = nullptr; }; -#endif // BOBINKCLIENT_H +#endif // OPCUACLIENT_H diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp new file mode 100644 index 0000000..518ddd5 --- /dev/null +++ b/src/OpcUaMonitoredNode.cpp @@ -0,0 +1,437 @@ +/** + * @file OpcUaMonitoredNode.cpp + * @brief OpcUaMonitoredNode implementation. + */ +#include "OpcUaMonitoredNode.h" +#include "OpcUaClient.h" + +#include <QMetaEnum> +#include <QOpcUaLocalizedText> +#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 (); +} + +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); + + m_node->readAttributes ( + QOpcUa::NodeAttribute::Value | QOpcUa::NodeAttribute::DisplayName + | QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::NodeClass + | QOpcUa::NodeAttribute::DataType | QOpcUa::NodeAttribute::AccessLevel); +} + +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 (); + } +} + +/* ====================================== + * 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)); +} + +/* ====================================== + * 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")); +} diff --git a/src/OpcUaMonitoredNode.h b/src/OpcUaMonitoredNode.h new file mode 100644 index 0000000..5895bea --- /dev/null +++ b/src/OpcUaMonitoredNode.h @@ -0,0 +1,121 @@ +/** + * @file OpcUaMonitoredNode.h + * @brief QML component for monitoring a single OPC UA node. + * + * Inherits QObject + QQmlParserStatus so that initialisation is + * deferred until all QML bindings are applied (componentComplete). + */ +#ifndef OPCUAMONITOREDNODE_H +#define OPCUAMONITOREDNODE_H + +#include <QDateTime> +#include <QObject> +#include <QOpcUaNode> +#include <QQmlEngine> +#include <QQmlParserStatus> +#include <QVariant> + +/** + * @brief Metadata bundle for a monitored OPC UA node. + * + * Exposed as a single Q_PROPERTY on OpcUaMonitoredNode so that + * the QML API stays simple (only `value` is top-level). + * Advanced users can access fields via dot notation: + * node.info.displayName, node.info.status, etc. + */ +struct OpcUaNodeInfo +{ + Q_GADGET + Q_PROPERTY (QString displayName MEMBER displayName) + Q_PROPERTY (QString description MEMBER description) + Q_PROPERTY (QString nodeClass MEMBER nodeClass) + Q_PROPERTY (QString dataType MEMBER dataType) + Q_PROPERTY (QString accessLevel MEMBER accessLevel) + Q_PROPERTY (QString status MEMBER status) + Q_PROPERTY (QDateTime sourceTimestamp MEMBER sourceTimestamp) + Q_PROPERTY (QDateTime serverTimestamp MEMBER serverTimestamp) + +public: + QString displayName; + QString description; + QString nodeClass; + QString dataType; + QString accessLevel; + QString status; + QDateTime sourceTimestamp; + QDateTime serverTimestamp; +}; +Q_DECLARE_METATYPE (OpcUaNodeInfo) + +class OpcUaMonitoredNode : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES (QQmlParserStatus) + QML_ELEMENT + + Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged) + Q_PROPERTY ( + bool monitored READ monitored WRITE setMonitored NOTIFY monitoredChanged) + Q_PROPERTY (QVariant value READ value NOTIFY valueChanged) + Q_PROPERTY (bool writable READ writable NOTIFY writableChanged) + Q_PROPERTY (OpcUaNodeInfo info READ info NOTIFY infoChanged) + +public: + explicit OpcUaMonitoredNode (QObject *parent = nullptr); + + QString nodeId () const; + void setNodeId (const QString &id); + + bool monitored () const; + void setMonitored (bool monitored); + + QVariant value () const; + bool writable () const; + OpcUaNodeInfo info () const; + + /** + * @brief Write a value to the OPC UA node. + * + * Coerces @a value to the node's data type (auto-detected from the + * server's DataType attribute). For array nodes, a comma-separated + * string is split and each element coerced individually. + * Result is reported asynchronously via writeCompleted(). + */ + Q_INVOKABLE void writeValue (const QVariant &value); + + void classBegin () override; + void componentComplete () override; + +signals: + void nodeIdChanged (); + void monitoredChanged (); + void valueChanged (); + void writableChanged (); + void infoChanged (); + void writeCompleted (bool success, const QString &message); + +private slots: + void handleAttributeUpdated (QOpcUa::NodeAttribute attr, + const QVariant &value); + void handleValueUpdated (const QVariant &value); + void handleClientConnectedChanged (); + void handleAttributeWritten (QOpcUa::NodeAttribute attr, + QOpcUa::UaStatusCode statusCode); + +private: + void setupNode (); + void teardownNode (); + QVariant coerceValue (const QVariant &input) const; + + QString m_nodeId; + bool m_monitored = true; + bool m_componentComplete = false; + + QOpcUaNode *m_node = nullptr; + QOpcUa::Types m_valueType = QOpcUa::Types::Undefined; + bool m_writable = false; + QVariant m_value; + OpcUaNodeInfo m_info; +}; + +#endif // OPCUAMONITOREDNODE_H |
