diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 06:18:29 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 06:18:29 +0100 |
| commit | 0c1df583acba434e2d7f6905a30fdefe288d0f9d (patch) | |
| tree | e485fb1510ce2441622c4b29b8762633849f6fd2 /demo | |
| parent | a0c7f2a7ef04dbe2e7491eabf828e26423d1bd10 (diff) | |
| download | BobinkQtOpcUa-0c1df583acba434e2d7f6905a30fdefe288d0f9d.tar.gz BobinkQtOpcUa-0c1df583acba434e2d7f6905a30fdefe288d0f9d.zip | |
Add BobinkNode QML type and two-page demo for node monitoring
BobinkNode (QQuickItem) monitors a single OPC UA node with
automatic lifecycle: monitoring starts/stops based on item
visibility (StackView page switches, Loader, etc.).
Properties: nodeId, value (r/w), status, sourceTimestamp,
serverTimestamp. On-demand readAttribute() for metadata.
writeError signal for failed writes.
Demo restructured with StackView: connection page → two node
pages demonstrating the visibility-based monitoring lifecycle.
Diffstat (limited to 'demo')
| -rw-r--r-- | demo/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | demo/Main.qml | 545 | ||||
| -rw-r--r-- | demo/NodePage.qml | 164 |
3 files changed, 412 insertions, 300 deletions
diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 9fb3093..255a9ee 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -7,7 +7,8 @@ qt_add_qml_module( VERSION 1.0 QML_FILES - Main.qml) + Main.qml + NodePage.qml) # Executable goes to bin/ to avoid clashing with the QML module directory set_target_properties(BobinkDemo PROPERTIES RUNTIME_OUTPUT_DIRECTORY diff --git a/demo/Main.qml b/demo/Main.qml index 5bdf369..ca22bb8 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -1,4 +1,5 @@ -// Main.qml — Demo app for testing BobinkClient connection flow. +// Main.qml — Demo app for Bobink library. +// Connects to an OPC UA server, then pushes NodePage for node interaction. import QtQuick import QtQuick.Controls @@ -16,11 +17,6 @@ ApplicationWindow { property bool autoConnectFailed: false property bool showPkiSettings: false - Component.onCompleted: { - BobinkClient.discoveryUrl = discoveryUrlField.text; - BobinkClient.startDiscovery(); - } - Connections { target: BobinkClient function onServersChanged() { @@ -28,8 +24,17 @@ ApplicationWindow { } function onConnectedChanged() { debugConsole.appendLog("Connected: " + BobinkClient.connected); - if (BobinkClient.connected) + if (BobinkClient.connected) { root.autoConnectFailed = false; + BobinkClient.stopDiscovery(); + stack.push("NodePage.qml", { + stackRef: stack, + pageNumber: 1, + logFunction: debugConsole.appendLog + }); + } else { + stack.pop(null); + } } function onConnectionError(message) { debugConsole.appendLog("ERROR: " + message); @@ -54,333 +59,277 @@ ApplicationWindow { title: "Certificate Trust" modal: true standardButtons: Dialog.Yes | Dialog.No - Label { - text: certTrustDialog.certInfo - } + Label { text: certTrustDialog.certInfo } onAccepted: BobinkClient.acceptCertificate() onRejected: BobinkClient.rejectCertificate() } ColumnLayout { anchors.fill: parent - anchors.margins: 20 - spacing: 12 + spacing: 0 - Label { - text: "Discovery URL" - font.bold: true - } + StackView { + id: stack + Layout.fillWidth: true + Layout.fillHeight: true - RowLayout { - TextField { - id: discoveryUrlField - Layout.fillWidth: true - text: "opc.tcp://localhost:4840" - } - Button { - text: BobinkClient.discovering ? "Stop" : "Discover" - onClicked: { - if (BobinkClient.discovering) { - BobinkClient.stopDiscovery(); - } else { - BobinkClient.discoveryUrl = discoveryUrlField.text; - BobinkClient.startDiscovery(); - } + initialItem: Page { + Component.onCompleted: { + BobinkClient.discoveryUrl = discoveryUrlField.text; + BobinkClient.startDiscovery(); } - } - } - Label { - text: BobinkClient.discovering ? "Discovering... (" + BobinkClient.servers.length + " found)" : BobinkClient.servers.length + " server(s)" - font.italic: true - } - - ListView { - Layout.fillWidth: true - Layout.preferredHeight: 100 - clip: true - model: BobinkClient.servers - delegate: ItemDelegate { - required property var modelData - width: ListView.view.width - text: modelData.serverName + " — " + modelData.applicationUri - onClicked: { - if (modelData.discoveryUrls.length > 0) - serverUrlField.text = modelData.discoveryUrls[0]; + BobinkAuth { + id: auth + mode: authModeCombo.currentValue + username: usernameField.text + password: passwordField.text + certPath: BobinkClient.certFile + keyPath: BobinkClient.keyFile } - } - ScrollBar.vertical: ScrollBar {} - } - - RowLayout { - Label { - text: "PKI" - font.bold: true - } - Label { - text: BobinkClient.certFile ? " (" + BobinkClient.certFile.split("/").pop() + ")" : " (no certificate found)" - font.italic: true - color: BobinkClient.certFile ? "green" : "gray" - } - Item { - Layout.fillWidth: true - } - Button { - text: root.showPkiSettings ? "Hide" : "Configure..." - flat: true - onClicked: root.showPkiSettings = !root.showPkiSettings - } - } - - FileDialog { - id: certFileDialog - title: "Select Certificate" - currentFolder: "file://" + trustFolderField.text - nameFilters: ["DER certificates (*.der)", "All files (*)"] - onAccepted: certFileField.text = selectedFile.toString().replace("file://", "") - } - - FileDialog { - id: keyFileDialog - title: "Select Private Key" - currentFolder: "file://" + trustFolderField.text - nameFilters: ["Key files (*.pem *.crt)", "All files (*)"] - onAccepted: keyFileField.text = selectedFile.toString().replace("file://", "") - } - FolderDialog { - id: trustFolderDialog - title: "Select Trust Folder" - currentFolder: "file://" + trustFolderField.text - onAccepted: trustFolderField.text = selectedFolder.toString().replace("file://", "") - } - - GridLayout { - columns: 3 - Layout.fillWidth: true - visible: root.showPkiSettings + FileDialog { + id: certFileDialog + title: "Select Certificate" + currentFolder: "file://" + trustFolderField.text + nameFilters: ["DER certificates (*.der)", "All files (*)"] + onAccepted: certFileField.text = selectedFile.toString().replace("file://", "") + } + FileDialog { + id: keyFileDialog + title: "Select Private Key" + currentFolder: "file://" + trustFolderField.text + nameFilters: ["Key files (*.pem *.crt)", "All files (*)"] + onAccepted: keyFileField.text = selectedFile.toString().replace("file://", "") + } + FolderDialog { + id: trustFolderDialog + title: "Select Trust Folder" + currentFolder: "file://" + trustFolderField.text + onAccepted: trustFolderField.text = selectedFolder.toString().replace("file://", "") + } - Label { - text: "Certificate:" - } - TextField { - id: certFileField - Layout.fillWidth: true - text: BobinkClient.certFile - placeholderText: "Client certificate (.der)" - } - Button { - text: "Browse..." - onClicked: certFileDialog.open() - } + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 12 - Label { - text: "Private key:" - } - TextField { - id: keyFileField - Layout.fillWidth: true - text: BobinkClient.keyFile - placeholderText: "Private key (.pem, .crt)" - } - Button { - text: "Browse..." - onClicked: keyFileDialog.open() - } + Label { + text: "Discovery URL" + font.bold: true + } - Label { - text: "Trust folder:" - } - TextField { - id: trustFolderField - Layout.fillWidth: true - text: BobinkClient.pkiDir - } - Button { - text: "Browse..." - onClicked: trustFolderDialog.open() - } - } + RowLayout { + TextField { + id: discoveryUrlField + Layout.fillWidth: true + text: "opc.tcp://localhost:4840" + } + Button { + text: BobinkClient.discovering ? "Stop" : "Discover" + onClicked: { + if (BobinkClient.discovering) { + BobinkClient.stopDiscovery(); + } else { + BobinkClient.discoveryUrl = discoveryUrlField.text; + BobinkClient.startDiscovery(); + } + } + } + } - Button { - text: "Apply PKI" - Layout.fillWidth: true - visible: root.showPkiSettings - onClicked: { - BobinkClient.pkiDir = trustFolderField.text; - BobinkClient.certFile = certFileField.text; - BobinkClient.keyFile = keyFileField.text; - BobinkClient.applyPki(); - } - } + Label { + text: BobinkClient.discovering + ? "Discovering... (" + BobinkClient.servers.length + " found)" + : BobinkClient.servers.length + " server(s)" + font.italic: true + } - Label { - text: "Server URL" - font.bold: true - } + ListView { + Layout.fillWidth: true + Layout.preferredHeight: 100 + clip: true + model: BobinkClient.servers + delegate: ItemDelegate { + required property var modelData + width: ListView.view.width + text: modelData.serverName + " — " + modelData.applicationUri + onClicked: { + if (modelData.discoveryUrls.length > 0) + serverUrlField.text = modelData.discoveryUrls[0]; + } + } + ScrollBar.vertical: ScrollBar {} + } - TextField { - id: serverUrlField - Layout.fillWidth: true - placeholderText: "opc.tcp://..." - } + RowLayout { + Label { text: "PKI"; font.bold: true } + Label { + text: BobinkClient.certFile + ? " (" + BobinkClient.certFile.split("/").pop() + ")" + : " (no certificate found)" + font.italic: true + color: BobinkClient.certFile ? "green" : "gray" + } + Item { Layout.fillWidth: true } + Button { + text: root.showPkiSettings ? "Hide" : "Configure..." + flat: true + onClicked: root.showPkiSettings = !root.showPkiSettings + } + } - Label { - text: "Authentication" - font.bold: true - } + GridLayout { + columns: 3 + Layout.fillWidth: true + visible: root.showPkiSettings + + Label { text: "Certificate:" } + TextField { + id: certFileField + Layout.fillWidth: true + text: BobinkClient.certFile + placeholderText: "Client certificate (.der)" + } + Button { text: "Browse..."; onClicked: certFileDialog.open() } + + Label { text: "Private key:" } + TextField { + id: keyFileField + Layout.fillWidth: true + text: BobinkClient.keyFile + placeholderText: "Private key (.pem, .crt)" + } + Button { text: "Browse..."; onClicked: keyFileDialog.open() } + + Label { text: "Trust folder:" } + TextField { + id: trustFolderField + Layout.fillWidth: true + text: BobinkClient.pkiDir + } + Button { text: "Browse..."; onClicked: trustFolderDialog.open() } + } - BobinkAuth { - id: auth - mode: authModeCombo.currentValue - username: usernameField.text - password: passwordField.text - certPath: BobinkClient.certFile - keyPath: BobinkClient.keyFile - } + Button { + text: "Apply PKI" + Layout.fillWidth: true + visible: root.showPkiSettings + onClicked: { + BobinkClient.pkiDir = trustFolderField.text; + BobinkClient.certFile = certFileField.text; + BobinkClient.keyFile = keyFileField.text; + BobinkClient.applyPki(); + } + } - ComboBox { - id: authModeCombo - Layout.fillWidth: true - textRole: "text" - valueRole: "mode" - model: [ - { - text: "Anonymous", - mode: BobinkAuth.Anonymous - }, - { - text: "Username / Password", - mode: BobinkAuth.UserPass - }, - { - text: "Certificate", - mode: BobinkAuth.Certificate - } - ] - } + Label { text: "Server URL"; font.bold: true } - GridLayout { - columns: 2 - visible: authModeCombo.currentValue === BobinkAuth.UserPass - Layout.fillWidth: true + TextField { + id: serverUrlField + Layout.fillWidth: true + placeholderText: "opc.tcp://..." + } - Label { - text: "Username:" - } - TextField { - id: usernameField - Layout.fillWidth: true - } - Label { - text: "Password:" - } - TextField { - id: passwordField - Layout.fillWidth: true - echoMode: TextInput.Password - } - } + Label { text: "Authentication"; font.bold: true } + + ComboBox { + id: authModeCombo + Layout.fillWidth: true + textRole: "text" + valueRole: "mode" + model: [ + { text: "Anonymous", mode: BobinkAuth.Anonymous }, + { text: "Username / Password", mode: BobinkAuth.UserPass }, + { text: "Certificate", mode: BobinkAuth.Certificate } + ] + } - Button { - text: BobinkClient.connected ? "Disconnect" : "Connect" - Layout.fillWidth: true - onClicked: { - if (BobinkClient.connected) { - BobinkClient.disconnectFromServer(); - } else { - root.autoConnectFailed = false; - BobinkClient.auth = auth; - BobinkClient.serverUrl = serverUrlField.text; - BobinkClient.connectToServer(); - } - } - } + GridLayout { + columns: 2 + visible: authModeCombo.currentValue === BobinkAuth.UserPass + Layout.fillWidth: true + + Label { text: "Username:" } + TextField { id: usernameField; Layout.fillWidth: true } + Label { text: "Password:" } + TextField { + id: passwordField + Layout.fillWidth: true + echoMode: TextInput.Password + } + } - Label { - text: "Direct Connect" - font.bold: true - visible: root.autoConnectFailed && !BobinkClient.connected - } + Button { + text: "Connect" + Layout.fillWidth: true + onClicked: { + root.autoConnectFailed = false; + BobinkClient.auth = auth; + BobinkClient.serverUrl = serverUrlField.text; + BobinkClient.connectToServer(); + } + } - GridLayout { - columns: 2 - Layout.fillWidth: true - visible: root.autoConnectFailed && !BobinkClient.connected + Label { + text: "Direct Connect" + font.bold: true + visible: root.autoConnectFailed + } - Label { - text: "Security policy:" - } - ComboBox { - id: securityPolicyCombo - Layout.fillWidth: true - textRole: "text" - valueRole: "policy" - model: [ - { - text: "Basic256Sha256", - policy: BobinkClient.Basic256Sha256 - }, - { - text: "Aes128-Sha256-RsaOaep", - policy: BobinkClient.Aes128_Sha256_RsaOaep - }, - { - text: "Aes256-Sha256-RsaPss", - policy: BobinkClient.Aes256_Sha256_RsaPss + GridLayout { + columns: 2 + Layout.fillWidth: true + visible: root.autoConnectFailed + + Label { text: "Security policy:" } + ComboBox { + id: securityPolicyCombo + Layout.fillWidth: true + textRole: "text" + valueRole: "policy" + model: [ + { text: "Basic256Sha256", policy: BobinkClient.Basic256Sha256 }, + { text: "Aes128-Sha256-RsaOaep", policy: BobinkClient.Aes128_Sha256_RsaOaep }, + { text: "Aes256-Sha256-RsaPss", policy: BobinkClient.Aes256_Sha256_RsaPss } + ] + } + + Label { text: "Security mode:" } + ComboBox { + id: securityModeCombo + Layout.fillWidth: true + textRole: "text" + valueRole: "mode" + model: [ + { text: "Sign & Encrypt", mode: BobinkClient.SignAndEncrypt }, + { text: "Sign", mode: BobinkClient.Sign }, + { text: "None", mode: BobinkClient.None } + ] + } } - ] - } - Label { - text: "Security mode:" - } - ComboBox { - id: securityModeCombo - Layout.fillWidth: true - textRole: "text" - valueRole: "mode" - model: [ - { - text: "Sign & Encrypt", - mode: BobinkClient.SignAndEncrypt - }, - { - text: "Sign", - mode: BobinkClient.Sign - }, - { - text: "None", - mode: BobinkClient.None + Button { + text: "Direct Connect" + Layout.fillWidth: true + visible: root.autoConnectFailed + onClicked: { + BobinkClient.auth = auth; + BobinkClient.serverUrl = serverUrlField.text; + BobinkClient.connectDirect(securityPolicyCombo.currentValue, + securityModeCombo.currentValue); + } } - ] - } - } - Button { - text: "Direct Connect" - Layout.fillWidth: true - visible: root.autoConnectFailed && !BobinkClient.connected - onClicked: { - BobinkClient.auth = auth; - BobinkClient.serverUrl = serverUrlField.text; - BobinkClient.connectDirect(securityPolicyCombo.currentValue, securityModeCombo.currentValue); + Item { Layout.fillHeight: true } + } } } - Label { - text: "Status: " + (BobinkClient.connected ? "Connected" : "Disconnected") - color: BobinkClient.connected ? "green" : "red" - } - - Item { - Layout.fillHeight: true - } - Rectangle { id: debugConsole Layout.fillWidth: true - Layout.preferredHeight: 100 + Layout.preferredHeight: 120 color: "#1e1e1e" border.color: "#444" radius: 4 @@ -394,7 +343,6 @@ ApplicationWindow { ScrollView { anchors.fill: parent anchors.margins: 4 - TextArea { id: statusLog readOnly: true @@ -407,5 +355,4 @@ ApplicationWindow { } } } - } diff --git a/demo/NodePage.qml b/demo/NodePage.qml new file mode 100644 index 0000000..e4862d8 --- /dev/null +++ b/demo/NodePage.qml @@ -0,0 +1,164 @@ +// NodePage.qml — Monitors a single OPC UA node. +// Two instances demonstrate visibility lifecycle: switching pages +// stops monitoring on the hidden page automatically. + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Bobink + +Page { + id: nodePage + + required property StackView stackRef + 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; + } + onWriteError: (message) => nodePage.logFunction("WRITE: " + message) + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 12 + + RowLayout { + Label { + text: "Node Page " + nodePage.pageNumber + font.bold: true + font.pointSize: 14 + } + Item { Layout.fillWidth: true } + Button { + text: "Disconnect" + onClicked: BobinkClient.disconnectFromServer() + } + } + + RowLayout { + TextField { + id: nodeIdField + Layout.fillWidth: true + placeholderText: "ns=2;s=Temperature" + enabled: !nodePage.monitoredNodeId + } + 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"); + } + } + } + } + + GroupBox { + 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 + } + + 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" + } + + Label { text: "Source TS:" } + Label { + text: node.sourceTimestamp.toLocaleString( + Qt.locale(), "HH:mm:ss.zzz") + font.family: "monospace" + } + + Label { text: "Server TS:" } + Label { + text: node.serverTimestamp.toLocaleString( + Qt.locale(), "HH:mm:ss.zzz") + font.family: "monospace" + } + } + } + + RowLayout { + visible: nodePage.monitoredNodeId.length > 0 + + 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(); + } + } + } + + Label { + visible: nodePage.monitoredNodeId.length > 0 + && nodePage.metadataLoaded && !nodePage.nodeWritable + text: "Read-only node" + font.italic: true + color: "gray" + } + + Item { Layout.fillHeight: true } + + Button { + 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(); + } + } + } +} |
