// Main.qml — Demo app for Bobink library. // Connects to an OPC UA server, then pushes NodePage for node interaction. 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 width: 800 Connections { function onCertificateTrustRequested(certInfo) { certTrustDialog.certInfo = certInfo; certTrustDialog.open(); } function onConnectedChanged() { debugConsole.appendLog("Connected: " + Bobink.connected); if (Bobink.connected) { root.autoConnectFailed = false; Bobink.stopDiscovery(); stack.push("NodePage.qml", { stackRef: stack, pageNumber: 1 }); } else { stack.pop(null); } } function onConnectionError(message) { debugConsole.appendLog("Connection error: " + message); root.autoConnectFailed = true; } function onDiscoveringChanged() { debugConsole.appendLog("Discovering: " + Bobink.discovering); } function onServersChanged() { debugConsole.appendLog("Discovered server list updated"); } function onStatusMessage(message) { debugConsole.appendLog(message); } target: 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 } } ColumnLayout { anchors.fill: parent spacing: 0 StackView { id: stack Layout.fillHeight: true Layout.fillWidth: true initialItem: Page { 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 { function onLogRequested(message) { debugConsole.appendLog(message); } ignoreUnknownSignals: true target: stack.currentItem } } 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; } 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 } } } } }