summaryrefslogtreecommitdiffstats
path: root/demo
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-19 06:18:29 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-19 06:18:29 +0100
commit0c1df583acba434e2d7f6905a30fdefe288d0f9d (patch)
treee485fb1510ce2441622c4b29b8762633849f6fd2 /demo
parenta0c7f2a7ef04dbe2e7491eabf828e26423d1bd10 (diff)
downloadBobinkQtOpcUa-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.txt3
-rw-r--r--demo/Main.qml545
-rw-r--r--demo/NodePage.qml164
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();
+ }
+ }
+ }
+}