aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-03-23 14:55:32 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-03-23 14:55:32 +0100
commit5ff9705937ffc1647587e1b228effd30c8a0e309 (patch)
treee1c5d9397ebbd68dd593788d6b09688a1d6ee8bd
parent47096b375797c38b5b1e79f1366f1152cc292875 (diff)
downloadBobinkQtOpcUa-5ff9705937ffc1647587e1b228effd30c8a0e309.tar.gz
BobinkQtOpcUa-5ff9705937ffc1647587e1b228effd30c8a0e309.zip
Refactor Main.qml into separate components and add comments
Extract ConnectionPage, CertTrustDialog, and DebugConsole from Main.qml into their own QML files. Add inline comments to OpcUaMonitoredNode::handleValueUpdated explaining the range write guard, value store, and timestamp update.
-rw-r--r--demo/CMakeLists.txt3
-rw-r--r--demo/CertTrustDialog.qml24
-rw-r--r--demo/ConnectionPage.qml400
-rw-r--r--demo/DebugConsole.qml34
-rw-r--r--demo/Main.qml446
-rw-r--r--src/OpcUaMonitoredNode.cpp10
6 files changed, 474 insertions, 443 deletions
diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt
index dd208e8..96dade8 100644
--- a/demo/CMakeLists.txt
+++ b/demo/CMakeLists.txt
@@ -8,6 +8,9 @@ qt_add_qml_module(
1.0
QML_FILES
Main.qml
+ CertTrustDialog.qml
+ ConnectionPage.qml
+ DebugConsole.qml
NodePage.qml
IMPORT_PATH
"${PROJECT_BINARY_DIR}/qml")
diff --git a/demo/CertTrustDialog.qml b/demo/CertTrustDialog.qml
new file mode 100644
index 0000000..877d68f
--- /dev/null
+++ b/demo/CertTrustDialog.qml
@@ -0,0 +1,24 @@
+// CertTrustDialog.qml — Modal dialog for OPC UA certificate trust prompts.
+
+import QtQuick.Controls
+import Bobink
+
+Dialog {
+ id: certTrustDialog
+
+ property string certInfo
+
+ anchors.centerIn: parent
+ implicitWidth: 400
+ modal: true
+ standardButtons: Dialog.Yes | Dialog.No
+ title: "Certificate Trust"
+
+ onAccepted: Bobink.acceptCertificate()
+ onRejected: Bobink.rejectCertificate()
+
+ Label {
+ text: certTrustDialog.certInfo
+ wrapMode: Text.Wrap
+ }
+}
diff --git a/demo/ConnectionPage.qml b/demo/ConnectionPage.qml
new file mode 100644
index 0000000..ba41246
--- /dev/null
+++ b/demo/ConnectionPage.qml
@@ -0,0 +1,400 @@
+// ConnectionPage.qml — Discovery, PKI, auth, and connection UI.
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Dialogs
+import Bobink
+
+Page {
+ id: connectionPage
+
+ property bool autoConnectFailed: false
+ property bool showPkiSettings: false
+
+ Component.onCompleted: {
+ Bobink.discoveryUrl = discoveryUrlField.text;
+ Bobink.startDiscovery();
+ }
+
+ OpcUaAuth {
+ id: auth
+
+ certPath: Bobink.certFile
+ keyPath: Bobink.keyFile
+ mode: authModeCombo.currentValue
+ password: passwordField.text
+ username: usernameField.text
+ }
+
+ FileDialog {
+ id: certFileDialog
+
+ currentFolder: "file://" + trustFolderField.text
+ nameFilters: ["DER certificates (*.der)", "All files (*)"]
+ title: "Select Certificate"
+
+ onAccepted: certFileField.text = selectedFile.toString().replace(
+ "file://", "")
+ }
+
+ FileDialog {
+ id: keyFileDialog
+
+ currentFolder: "file://" + trustFolderField.text
+ nameFilters: ["Key files (*.pem *.crt)", "All files (*)"]
+ title: "Select Private Key"
+
+ onAccepted: keyFileField.text = selectedFile.toString().replace(
+ "file://", "")
+ }
+
+ FolderDialog {
+ id: trustFolderDialog
+
+ currentFolder: "file://" + trustFolderField.text
+ title: "Select Trust Folder"
+
+ onAccepted: trustFolderField.text = selectedFolder.toString().replace(
+ "file://", "")
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.margins: 20
+ spacing: 12
+
+ Label {
+ font.bold: true
+ text: "Discovery URL"
+ }
+
+ RowLayout {
+ TextField {
+ id: discoveryUrlField
+
+ Layout.fillWidth: true
+ text: "opc.tcp://localhost:4840"
+ }
+
+ Button {
+ text: Bobink.discovering ? "Stop" : "Discover"
+
+ onClicked: {
+ if (Bobink.discovering) {
+ Bobink.stopDiscovery();
+ } else {
+ Bobink.discoveryUrl = discoveryUrlField.text;
+ Bobink.startDiscovery();
+ }
+ }
+ }
+ }
+
+ Label {
+ font.italic: true
+ text: Bobink.discovering ? "Discovering... ("
+ + Bobink.servers.length + " found)" :
+ Bobink.servers.length + " server(s)"
+ }
+
+ ListView {
+ id: serverListView
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 100
+ clip: true
+ model: Bobink.servers
+
+ ScrollBar.vertical: ScrollBar {
+ policy: ScrollBar.AsNeeded
+ }
+ delegate: ItemDelegate {
+ id: serverDelegate
+
+ required property var modelData
+
+ width: ListView.view.width
+
+ contentItem: ColumnLayout {
+ spacing: 2
+
+ Label {
+ text: serverDelegate.modelData.serverName
+ }
+
+ Label {
+ Layout.fillWidth: true
+ color: "gray"
+ elide: Text.ElideRight
+ font.italic: true
+ font.pointSize: 8
+ text: serverDelegate.modelData.applicationUri
+ }
+ }
+
+ onClicked: {
+ if (modelData.discoveryUrls.length > 0)
+ serverUrlField.text = modelData.discoveryUrls[0];
+ }
+ }
+ }
+
+ RowLayout {
+ Label {
+ font.bold: true
+ text: "PKI"
+ }
+
+ Label {
+ color: Bobink.certFile ? "green" : "gray"
+ font.italic: true
+ text: Bobink.certFile ? " (" + Bobink.certFile.split("/").pop(
+ ) + ")" : " (no certificate found)"
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Button {
+ text: connectionPage.showPkiSettings ? "Hide" : "Configure"
+
+ onClicked: connectionPage.showPkiSettings =
+ !connectionPage.showPkiSettings
+ }
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 3
+ visible: connectionPage.showPkiSettings
+
+ Label {
+ text: "Certificate:"
+ }
+
+ TextField {
+ id: certFileField
+
+ Layout.fillWidth: true
+ placeholderText: "Client certificate (.der)"
+ text: Bobink.certFile
+ }
+
+ Button {
+ text: "Browse"
+
+ onClicked: certFileDialog.open()
+ }
+
+ Label {
+ text: "Private key:"
+ }
+
+ TextField {
+ id: keyFileField
+
+ Layout.fillWidth: true
+ placeholderText: "Private key (.pem, .crt)"
+ text: Bobink.keyFile
+ }
+
+ Button {
+ text: "Browse"
+
+ onClicked: keyFileDialog.open()
+ }
+
+ Label {
+ text: "Trust folder:"
+ }
+
+ TextField {
+ id: trustFolderField
+
+ Layout.fillWidth: true
+ text: Bobink.pkiDir
+ }
+
+ Button {
+ text: "Browse"
+
+ onClicked: trustFolderDialog.open()
+ }
+ }
+
+ Button {
+ Layout.fillWidth: true
+ text: "Apply PKI"
+ visible: connectionPage.showPkiSettings
+
+ onClicked: {
+ Bobink.pkiDir = trustFolderField.text;
+ Bobink.certFile = certFileField.text;
+ Bobink.keyFile = keyFileField.text;
+ Bobink.applyPki();
+ }
+ }
+
+ Label {
+ font.bold: true
+ text: "Server URL"
+ }
+
+ TextField {
+ id: serverUrlField
+
+ Layout.fillWidth: true
+ placeholderText: "opc.tcp://..."
+ }
+
+ Label {
+ font.bold: true
+ text: "Authentication"
+ }
+
+ ComboBox {
+ id: authModeCombo
+
+ Layout.fillWidth: true
+ model: [
+ {
+ text: "Anonymous",
+ mode: OpcUaAuth.Anonymous
+ },
+ {
+ text: "Username / Password",
+ mode: OpcUaAuth.UserPass
+ },
+ {
+ text: "Certificate",
+ mode: OpcUaAuth.Certificate
+ }
+ ]
+ textRole: "text"
+ valueRole: "mode"
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+ visible: authModeCombo.currentValue === OpcUaAuth.UserPass
+
+ Label {
+ text: "Username:"
+ }
+
+ TextField {
+ id: usernameField
+
+ Layout.fillWidth: true
+ }
+
+ Label {
+ text: "Password:"
+ }
+
+ TextField {
+ id: passwordField
+
+ Layout.fillWidth: true
+ echoMode: TextInput.Password
+ }
+ }
+
+ Button {
+ Layout.fillWidth: true
+ text: "Connect"
+
+ onClicked: {
+ connectionPage.autoConnectFailed = false;
+ Bobink.auth = auth;
+ Bobink.serverUrl = serverUrlField.text;
+ Bobink.connectToServer();
+ }
+ }
+
+ Label {
+ font.bold: true
+ text: "Direct Connect"
+ visible: connectionPage.autoConnectFailed
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+ visible: connectionPage.autoConnectFailed
+
+ Label {
+ text: "Security policy:"
+ }
+
+ ComboBox {
+ id: securityPolicyCombo
+
+ Layout.fillWidth: true
+ model: [
+ {
+ text: "Basic256Sha256",
+ policy: Bobink.Basic256Sha256
+ },
+ {
+ text: "Aes128-Sha256-RsaOaep",
+ policy: Bobink.Aes128_Sha256_RsaOaep
+ },
+ {
+ text: "Aes256-Sha256-RsaPss",
+ policy: Bobink.Aes256_Sha256_RsaPss
+ }
+ ]
+ textRole: "text"
+ valueRole: "policy"
+ }
+
+ Label {
+ text: "Security mode:"
+ }
+
+ ComboBox {
+ id: securityModeCombo
+
+ Layout.fillWidth: true
+ model: [
+ {
+ text: "Sign & Encrypt",
+ mode: Bobink.SignAndEncrypt
+ },
+ {
+ text: "Sign",
+ mode: Bobink.Sign
+ },
+ {
+ text: "None",
+ mode: Bobink.None
+ }
+ ]
+ textRole: "text"
+ valueRole: "mode"
+ }
+ }
+
+ Button {
+ Layout.fillWidth: true
+ text: "Direct Connect"
+ visible: connectionPage.autoConnectFailed
+
+ onClicked: {
+ Bobink.auth = auth;
+ Bobink.serverUrl = serverUrlField.text;
+ Bobink.connectDirect(securityPolicyCombo.currentValue,
+ securityModeCombo.currentValue);
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+}
diff --git a/demo/DebugConsole.qml b/demo/DebugConsole.qml
new file mode 100644
index 0000000..94202fb
--- /dev/null
+++ b/demo/DebugConsole.qml
@@ -0,0 +1,34 @@
+// DebugConsole.qml — Timestamped log output panel.
+
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ id: debugConsole
+
+ function appendLog(msg) {
+ let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss");
+ debugLog.text += "[" + ts + "] " + msg + "\n";
+ debugLog.cursorPosition = debugLog.text.length;
+ }
+
+ border.color: "#444"
+ color: "#1e1e1e"
+ radius: 4
+
+ ScrollView {
+ anchors.fill: parent
+ anchors.margins: 4
+
+ TextArea {
+ id: debugLog
+
+ background: null
+ color: "#cccccc"
+ font.family: "monospace"
+ font.pointSize: 9
+ readOnly: true
+ wrapMode: TextEdit.Wrap
+ }
+ }
+}
diff --git a/demo/Main.qml b/demo/Main.qml
index 28d59bc..908dcbd 100644
--- a/demo/Main.qml
+++ b/demo/Main.qml
@@ -5,15 +5,11 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
-import QtQuick.Dialogs
import Bobink
ApplicationWindow {
id: root
- property bool autoConnectFailed: false
- property bool showPkiSettings: false
-
height: 900
title: "Bobink Demo"
visible: true
@@ -28,7 +24,7 @@ ApplicationWindow {
function onConnectedChanged() {
debugConsole.appendLog("Connected: " + Bobink.connected);
if (Bobink.connected) {
- root.autoConnectFailed = false;
+ connectionPage.autoConnectFailed = false;
Bobink.stopDiscovery();
stack.push("NodePage.qml", {
stackRef: stack,
@@ -41,7 +37,7 @@ ApplicationWindow {
function onConnectionError(message) {
debugConsole.appendLog("Connection error: " + message);
- root.autoConnectFailed = true;
+ connectionPage.autoConnectFailed = true;
}
function onDiscoveringChanged() {
@@ -59,24 +55,9 @@ ApplicationWindow {
target: Bobink
}
- Dialog {
+ CertTrustDialog {
id: certTrustDialog
- property string certInfo
-
- anchors.centerIn: parent
- implicitWidth: 400
- modal: true
- standardButtons: Dialog.Yes | Dialog.No
- title: "Certificate Trust"
-
- onAccepted: Bobink.acceptCertificate()
- onRejected: Bobink.rejectCertificate()
-
- Label {
- text: certTrustDialog.certInfo
- wrapMode: Text.Wrap
- }
}
ColumnLayout {
@@ -89,401 +70,9 @@ ApplicationWindow {
Layout.fillHeight: true
Layout.fillWidth: true
- initialItem: Page {
+ initialItem: ConnectionPage {
id: connectionPage
- Component.onCompleted: {
- Bobink.discoveryUrl = discoveryUrlField.text;
- Bobink.startDiscovery();
- }
-
- OpcUaAuth {
- id: auth
-
- certPath: Bobink.certFile
- keyPath: Bobink.keyFile
- mode: authModeCombo.currentValue
- password: passwordField.text
- username: usernameField.text
- }
-
- FileDialog {
- id: certFileDialog
-
- currentFolder: "file://" + trustFolderField.text
- nameFilters: ["DER certificates (*.der)", "All files (*)"]
- title: "Select Certificate"
-
- onAccepted: certFileField.text = selectedFile.toString(
- ).replace("file://", "")
- }
-
- FileDialog {
- id: keyFileDialog
-
- currentFolder: "file://" + trustFolderField.text
- nameFilters: ["Key files (*.pem *.crt)", "All files (*)"]
- title: "Select Private Key"
-
- onAccepted: keyFileField.text = selectedFile.toString(
- ).replace("file://", "")
- }
-
- FolderDialog {
- id: trustFolderDialog
-
- currentFolder: "file://" + trustFolderField.text
- title: "Select Trust Folder"
-
- onAccepted: trustFolderField.text = selectedFolder.toString(
- ).replace("file://", "")
- }
-
- ColumnLayout {
- anchors.fill: parent
- anchors.margins: 20
- spacing: 12
-
- Label {
- font.bold: true
- text: "Discovery URL"
- }
-
- RowLayout {
- TextField {
- id: discoveryUrlField
-
- Layout.fillWidth: true
- text: "opc.tcp://localhost:4840"
- }
-
- Button {
- text: Bobink.discovering ? "Stop" : "Discover"
-
- onClicked: {
- if (Bobink.discovering) {
- Bobink.stopDiscovery();
- } else {
- Bobink.discoveryUrl
- = discoveryUrlField.text;
- Bobink.startDiscovery();
- }
- }
- }
- }
-
- Label {
- font.italic: true
- text: Bobink.discovering ? "Discovering... ("
- + Bobink.servers.length
- + " found)" :
- Bobink.servers.length
- + " server(s)"
- }
-
- ListView {
- id: serverListView
-
- Layout.fillWidth: true
- Layout.preferredHeight: 100
- clip: true
- model: Bobink.servers
-
- ScrollBar.vertical: ScrollBar {
- policy: ScrollBar.AsNeeded
- }
- delegate: ItemDelegate {
- id: serverDelegate
-
- required property var modelData
-
- width: ListView.view.width
-
- contentItem: ColumnLayout {
- spacing: 2
-
- Label {
- text: serverDelegate.modelData.serverName
- }
-
- Label {
- Layout.fillWidth: true
- color: "gray"
- elide: Text.ElideRight
- font.italic: true
- font.pointSize: 8
- text: serverDelegate.modelData.applicationUri
- }
- }
-
- onClicked: {
- if (modelData.discoveryUrls.length > 0)
- serverUrlField.text
- = modelData.discoveryUrls[0];
- }
- }
- }
-
- RowLayout {
- Label {
- font.bold: true
- text: "PKI"
- }
-
- Label {
- color: Bobink.certFile ? "green" : "gray"
- font.italic: true
- text: Bobink.certFile ? " ("
- + Bobink.certFile.split(
- "/").pop() + ")" :
- " (no certificate found)"
- }
-
- Item {
- Layout.fillWidth: true
- }
-
- Button {
- text: root.showPkiSettings ? "Hide" : "Configure"
-
- onClicked: root.showPkiSettings =
- !root.showPkiSettings
- }
- }
-
- GridLayout {
- Layout.fillWidth: true
- columns: 3
- visible: root.showPkiSettings
-
- Label {
- text: "Certificate:"
- }
-
- TextField {
- id: certFileField
-
- Layout.fillWidth: true
- placeholderText: "Client certificate (.der)"
- text: Bobink.certFile
- }
-
- Button {
- text: "Browse"
-
- onClicked: certFileDialog.open()
- }
-
- Label {
- text: "Private key:"
- }
-
- TextField {
- id: keyFileField
-
- Layout.fillWidth: true
- placeholderText: "Private key (.pem, .crt)"
- text: Bobink.keyFile
- }
-
- Button {
- text: "Browse"
-
- onClicked: keyFileDialog.open()
- }
-
- Label {
- text: "Trust folder:"
- }
-
- TextField {
- id: trustFolderField
-
- Layout.fillWidth: true
- text: Bobink.pkiDir
- }
-
- Button {
- text: "Browse"
-
- onClicked: trustFolderDialog.open()
- }
- }
-
- Button {
- Layout.fillWidth: true
- text: "Apply PKI"
- visible: root.showPkiSettings
-
- onClicked: {
- Bobink.pkiDir = trustFolderField.text;
- Bobink.certFile = certFileField.text;
- Bobink.keyFile = keyFileField.text;
- Bobink.applyPki();
- }
- }
-
- Label {
- font.bold: true
- text: "Server URL"
- }
-
- TextField {
- id: serverUrlField
-
- Layout.fillWidth: true
- placeholderText: "opc.tcp://..."
- }
-
- Label {
- font.bold: true
- text: "Authentication"
- }
-
- ComboBox {
- id: authModeCombo
-
- Layout.fillWidth: true
- model: [
- {
- text: "Anonymous",
- mode: OpcUaAuth.Anonymous
- },
- {
- text: "Username / Password",
- mode: OpcUaAuth.UserPass
- },
- {
- text: "Certificate",
- mode: OpcUaAuth.Certificate
- }
- ]
- textRole: "text"
- valueRole: "mode"
- }
-
- GridLayout {
- Layout.fillWidth: true
- columns: 2
- visible: authModeCombo.currentValue
- === OpcUaAuth.UserPass
-
- Label {
- text: "Username:"
- }
-
- TextField {
- id: usernameField
-
- Layout.fillWidth: true
- }
-
- Label {
- text: "Password:"
- }
-
- TextField {
- id: passwordField
-
- Layout.fillWidth: true
- echoMode: TextInput.Password
- }
- }
-
- Button {
- Layout.fillWidth: true
- text: "Connect"
-
- onClicked: {
- root.autoConnectFailed = false;
- Bobink.auth = auth;
- Bobink.serverUrl = serverUrlField.text;
- Bobink.connectToServer();
- }
- }
-
- Label {
- font.bold: true
- text: "Direct Connect"
- visible: root.autoConnectFailed
- }
-
- GridLayout {
- Layout.fillWidth: true
- columns: 2
- visible: root.autoConnectFailed
-
- Label {
- text: "Security policy:"
- }
-
- ComboBox {
- id: securityPolicyCombo
-
- Layout.fillWidth: true
- model: [
- {
- text: "Basic256Sha256",
- policy: Bobink.Basic256Sha256
- },
- {
- text: "Aes128-Sha256-RsaOaep",
- policy: Bobink.Aes128_Sha256_RsaOaep
- },
- {
- text: "Aes256-Sha256-RsaPss",
- policy: Bobink.Aes256_Sha256_RsaPss
- }
- ]
- textRole: "text"
- valueRole: "policy"
- }
-
- Label {
- text: "Security mode:"
- }
-
- ComboBox {
- id: securityModeCombo
-
- Layout.fillWidth: true
- model: [
- {
- text: "Sign & Encrypt",
- mode: Bobink.SignAndEncrypt
- },
- {
- text: "Sign",
- mode: Bobink.Sign
- },
- {
- text: "None",
- mode: Bobink.None
- }
- ]
- textRole: "text"
- valueRole: "mode"
- }
- }
-
- Button {
- Layout.fillWidth: true
- text: "Direct Connect"
- visible: root.autoConnectFailed
-
- onClicked: {
- Bobink.auth = auth;
- Bobink.serverUrl = serverUrlField.text;
- Bobink.connectDirect(
- securityPolicyCombo.currentValue,
- securityModeCombo.currentValue);
- }
- }
-
- Item {
- Layout.fillHeight: true
- }
- }
}
Connections {
@@ -496,36 +85,11 @@ ApplicationWindow {
}
}
- Rectangle {
+ DebugConsole {
id: debugConsole
- function appendLog(msg) {
- let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss");
- debugLog.text += "[" + ts + "] " + msg + "\n";
- debugLog.cursorPosition = debugLog.text.length;
- }
-
Layout.fillWidth: true
Layout.preferredHeight: 120
- border.color: "#444"
- color: "#1e1e1e"
- radius: 4
-
- ScrollView {
- anchors.fill: parent
- anchors.margins: 4
-
- TextArea {
- id: debugLog
-
- background: null
- color: "#cccccc"
- font.family: "monospace"
- font.pointSize: 9
- readOnly: true
- wrapMode: TextEdit.Wrap
- }
- }
}
}
}
diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp
index 9155838..290a1c2 100644
--- a/src/OpcUaMonitoredNode.cpp
+++ b/src/OpcUaMonitoredNode.cpp
@@ -264,8 +264,8 @@ OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
m_info.valueRank = QStringLiteral ("TwoDimensions");
break;
default:
- m_info.valueRank = QString::number (vr)
- + QStringLiteral ("Dimensions");
+ m_info.valueRank
+ = QString::number (vr) + QStringLiteral ("Dimensions");
break;
}
}
@@ -312,12 +312,18 @@ OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
void
OpcUaMonitoredNode::handleValueUpdated (const QVariant &value)
{
+ // After a range write, Qt emits valueAttributeUpdated with only the
+ // partial written value instead of the full array. Ignore this
+ // update and let the monitored item deliver the correct full array
+ // on the next data change notification.
if (m_pendingRangeWrite)
return;
+ // Store the new value pushed by the OPC UA monitored item.
m_value = value;
emit valueChanged ();
+ // Update status and timestamps from the node's cached attributes.
m_info.status = QOpcUa::statusToString (m_node->valueAttributeError ());
m_info.sourceTimestamp
= m_node->sourceTimestamp (QOpcUa::NodeAttribute::Value);