summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--demo/Main.qml10
-rw-r--r--demo/NodePage.qml282
-rw-r--r--src/BobinkNode.cpp410
-rw-r--r--src/BobinkNode.h111
-rw-r--r--src/CMakeLists.txt12
-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.cpp437
-rw-r--r--src/OpcUaMonitoredNode.h121
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