From 5ff9705937ffc1647587e1b228effd30c8a0e309 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Mon, 23 Mar 2026 14:55:32 +0100 Subject: 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. --- demo/CMakeLists.txt | 3 + demo/CertTrustDialog.qml | 24 +++ demo/ConnectionPage.qml | 400 ++++++++++++++++++++++++++++++++++++++++ demo/DebugConsole.qml | 34 ++++ demo/Main.qml | 446 +-------------------------------------------- src/OpcUaMonitoredNode.cpp | 10 +- 6 files changed, 474 insertions(+), 443 deletions(-) create mode 100644 demo/CertTrustDialog.qml create mode 100644 demo/ConnectionPage.qml create mode 100644 demo/DebugConsole.qml 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); -- cgit v1.2.3