diff options
Diffstat (limited to 'demo')
| -rw-r--r-- | demo/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | demo/Main.qml | 222 | ||||
| -rw-r--r-- | demo/NodePage.qml | 339 |
3 files changed, 331 insertions, 234 deletions
diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index d2158cb..dd208e8 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -8,7 +8,9 @@ qt_add_qml_module( 1.0 QML_FILES Main.qml - NodePage.qml) + NodePage.qml + IMPORT_PATH + "${PROJECT_BINARY_DIR}/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 361e3bd..2cfa577 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -9,61 +9,72 @@ import Bobink ApplicationWindow { id: root - width: 800 - height: 900 - visible: true - title: "Bobink Demo" property bool autoConnectFailed: false property bool showPkiSettings: false + height: 900 + title: "Bobink Demo" + visible: true + width: 800 + Connections { - target: Bobink - function onServersChanged() { - debugConsole.appendLog("Discovered server list updated"); + 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, - logFunction: debugConsole.appendLog - }); + stackRef: stack, + pageNumber: 1, + logFunction: debugConsole.appendLog + }); } else { stack.pop(null); } } + function onConnectionError(message) { debugConsole.appendLog("Connection error: " + message); root.autoConnectFailed = true; } - function onStatusMessage(message) { - debugConsole.appendLog(message); - } + function onDiscoveringChanged() { debugConsole.appendLog("Discovering: " + Bobink.discovering); } - function onCertificateTrustRequested(certInfo) { - certTrustDialog.certInfo = certInfo; - certTrustDialog.open(); + + 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 - title: "Certificate Trust" modal: true standardButtons: Dialog.Yes | Dialog.No + title: "Certificate Trust" + + onAccepted: Bobink.acceptCertificate() + onRejected: Bobink.rejectCertificate() + Label { text: certTrustDialog.certInfo } - onAccepted: Bobink.acceptCertificate() - onRejected: Bobink.rejectCertificate() } ColumnLayout { @@ -72,11 +83,13 @@ ApplicationWindow { StackView { id: stack - Layout.fillWidth: true + Layout.fillHeight: true + Layout.fillWidth: true initialItem: Page { id: connectionPage + Component.onCompleted: { Bobink.discoveryUrl = discoveryUrlField.text; Bobink.startDiscovery(); @@ -84,32 +97,44 @@ ApplicationWindow { OpcUaAuth { id: auth - mode: authModeCombo.currentValue - username: usernameField.text - password: passwordField.text + certPath: Bobink.certFile keyPath: Bobink.keyFile + mode: authModeCombo.currentValue + password: passwordField.text + username: usernameField.text } FileDialog { id: certFileDialog - title: "Select Certificate" + currentFolder: "file://" + trustFolderField.text nameFilters: ["DER certificates (*.der)", "All files (*)"] - onAccepted: certFileField.text = selectedFile.toString().replace("file://", "") + title: "Select Certificate" + + 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://", "") + title: "Select Private Key" + + 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://", "") + title: "Select Trust Folder" + + onAccepted: trustFolderField.text = selectedFolder.toString( + ).replace("file://", "") } ColumnLayout { @@ -118,23 +143,27 @@ ApplicationWindow { spacing: 12 Label { - text: "Discovery URL" 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.discoveryUrl + = discoveryUrlField.text; Bobink.startDiscovery(); } } @@ -142,114 +171,148 @@ ApplicationWindow { } Label { - text: Bobink.discovering ? "Discovering... (" + Bobink.servers.length + " found)" : Bobink.servers.length + " server(s)" 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 { - text: serverDelegate.modelData.applicationUri + Layout.fillWidth: true color: "gray" + elide: Text.ElideRight font.italic: true font.pointSize: 8 - elide: Text.ElideRight - Layout.fillWidth: true + text: serverDelegate.modelData.applicationUri } } + onClicked: { if (modelData.discoveryUrls.length > 0) - serverUrlField.text = modelData.discoveryUrls[0]; + serverUrlField.text + = modelData.discoveryUrls[0]; } } - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AsNeeded - } } RowLayout { Label { - text: "PKI" font.bold: true + text: "PKI" } + Label { - text: Bobink.certFile ? " (" + Bobink.certFile.split("/").pop() + ")" : " (no certificate found)" - font.italic: true 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 + + onClicked: root.showPkiSettings = + !root.showPkiSettings } } GridLayout { - columns: 3 Layout.fillWidth: true + columns: 3 visible: root.showPkiSettings Label { text: "Certificate:" } + TextField { id: certFileField + Layout.fillWidth: true - text: Bobink.certFile placeholderText: "Client certificate (.der)" + text: Bobink.certFile } + Button { text: "Browse" + onClicked: certFileDialog.open() } Label { text: "Private key:" } + TextField { id: keyFileField + Layout.fillWidth: true - text: Bobink.keyFile 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 { - text: "Apply PKI" Layout.fillWidth: true + text: "Apply PKI" visible: root.showPkiSettings + onClicked: { Bobink.pkiDir = trustFolderField.text; Bobink.certFile = certFileField.text; @@ -259,26 +322,26 @@ ApplicationWindow { } Label { - text: "Server URL" font.bold: true + text: "Server URL" } TextField { id: serverUrlField + Layout.fillWidth: true placeholderText: "opc.tcp://..." } Label { - text: "Authentication" font.bold: true + text: "Authentication" } ComboBox { id: authModeCombo + Layout.fillWidth: true - textRole: "text" - valueRole: "mode" model: [ { text: "Anonymous", @@ -293,33 +356,42 @@ ApplicationWindow { mode: OpcUaAuth.Certificate } ] + textRole: "text" + valueRole: "mode" } GridLayout { - columns: 2 - visible: authModeCombo.currentValue === OpcUaAuth.UserPass 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 { - text: "Connect" Layout.fillWidth: true + text: "Connect" + onClicked: { root.autoConnectFailed = false; Bobink.auth = auth; @@ -329,24 +401,24 @@ ApplicationWindow { } Label { - text: "Direct Connect" font.bold: true + text: "Direct Connect" visible: root.autoConnectFailed } GridLayout { - columns: 2 Layout.fillWidth: true + columns: 2 visible: root.autoConnectFailed Label { text: "Security policy:" } + ComboBox { id: securityPolicyCombo + Layout.fillWidth: true - textRole: "text" - valueRole: "policy" model: [ { text: "Basic256Sha256", @@ -361,16 +433,18 @@ ApplicationWindow { policy: Bobink.Aes256_Sha256_RsaPss } ] + textRole: "text" + valueRole: "policy" } Label { text: "Security mode:" } + ComboBox { id: securityModeCombo + Layout.fillWidth: true - textRole: "text" - valueRole: "mode" model: [ { text: "Sign & Encrypt", @@ -385,17 +459,22 @@ ApplicationWindow { mode: Bobink.None } ] + textRole: "text" + valueRole: "mode" } } Button { - text: "Direct Connect" Layout.fillWidth: true + text: "Direct Connect" visible: root.autoConnectFailed + onClicked: { Bobink.auth = auth; Bobink.serverUrl = serverUrlField.text; - Bobink.connectDirect(securityPolicyCombo.currentValue, securityModeCombo.currentValue); + Bobink.connectDirect( + securityPolicyCombo.currentValue, + securityModeCombo.currentValue); } } @@ -408,11 +487,6 @@ ApplicationWindow { Rectangle { id: debugConsole - Layout.fillWidth: true - Layout.preferredHeight: 120 - color: "#1e1e1e" - border.color: "#444" - radius: 4 function appendLog(msg) { let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss"); @@ -420,17 +494,25 @@ ApplicationWindow { 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 - readOnly: true + + background: null color: "#cccccc" font.family: "monospace" font.pointSize: 9 + readOnly: true wrapMode: TextEdit.Wrap - background: null } } } diff --git a/demo/NodePage.qml b/demo/NodePage.qml index e089f0e..97c99ef 100644 --- a/demo/NodePage.qml +++ b/demo/NodePage.qml @@ -1,5 +1,4 @@ // NodePage.qml — Displays 10 OPC UA nodes per page with tooltips. - import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -8,126 +7,90 @@ import Bobink Page { id: nodePage - required property StackView stackRef - required property int pageNumber + readonly property var currentPage: pages[pageNumber - 1] required property var logFunction - + required property int pageNumber readonly property var pages: [ { - title: "Server Info", - description: "Standard OPC UA server nodes (namespace 0)." - + " CurrentTime updates live via monitoring.", - nodes: [ - "ns=0;i=2258", // CurrentTime - "ns=0;i=2257", // StartTime - "ns=0;i=2259", // State - "ns=0;i=2261", // ProductName - "ns=0;i=2264" // SoftwareVersion + "title": "Server Info", + "description": "Standard OPC UA server nodes (namespace 0)." + + " CurrentTime updates live via monitoring.", + "nodes": ["ns=0;i=2258" // CurrentTime + , "ns=0;i=2257" // StartTime + , "ns=0;i=2259" // State + , "ns=0;i=2261" // ProductName + , "ns=0;i=2264" // SoftwareVersion ] }, { - title: "Read-Write Scalars", - description: "Single-value nodes with read and write access.", - nodes: [ - "ns=1;s=bool_rw_scalar", - "ns=1;s=int16_rw_scalar", - "ns=1;s=uint16_rw_scalar", - "ns=1;s=int32_rw_scalar", - "ns=1;s=uint32_rw_scalar", - "ns=1;s=int64_rw_scalar", - "ns=1;s=uint64_rw_scalar", - "ns=1;s=float_rw_scalar", - "ns=1;s=double_rw_scalar", - "ns=1;s=string_rw_scalar", - "ns=1;s=sbyte_rw_scalar", - "ns=1;s=byte_rw_scalar", - "ns=1;s=datetime_rw_scalar", - "ns=1;s=guid_rw_scalar", - "ns=1;s=bytestring_rw_scalar" - ] + "title": "Read-Write Scalars", + "description": "Single-value nodes with read and write access.", + "nodes": ["ns=1;s=bool_rw_scalar", "ns=1;s=int16_rw_scalar", + "ns=1;s=uint16_rw_scalar", "ns=1;s=int32_rw_scalar", + "ns=1;s=uint32_rw_scalar", "ns=1;s=int64_rw_scalar", + "ns=1;s=uint64_rw_scalar", "ns=1;s=float_rw_scalar", + "ns=1;s=double_rw_scalar", "ns=1;s=string_rw_scalar", + "ns=1;s=sbyte_rw_scalar", "ns=1;s=byte_rw_scalar", + "ns=1;s=datetime_rw_scalar", "ns=1;s=guid_rw_scalar", + "ns=1;s=bytestring_rw_scalar"] }, { - title: "Read-Only Scalars", - description: "Single-value nodes with read-only access.", - nodes: [ - "ns=1;s=bool_ro_scalar", - "ns=1;s=int16_ro_scalar", - "ns=1;s=uint16_ro_scalar", - "ns=1;s=int32_ro_scalar", - "ns=1;s=uint32_ro_scalar", - "ns=1;s=int64_ro_scalar", - "ns=1;s=uint64_ro_scalar", - "ns=1;s=float_ro_scalar", - "ns=1;s=double_ro_scalar", - "ns=1;s=string_ro_scalar", - "ns=1;s=sbyte_ro_scalar", - "ns=1;s=byte_ro_scalar", - "ns=1;s=datetime_ro_scalar", - "ns=1;s=guid_ro_scalar", - "ns=1;s=bytestring_ro_scalar" - ] + "title": "Read-Only Scalars", + "description": "Single-value nodes with read-only access.", + "nodes": ["ns=1;s=bool_ro_scalar", "ns=1;s=int16_ro_scalar", + "ns=1;s=uint16_ro_scalar", "ns=1;s=int32_ro_scalar", + "ns=1;s=uint32_ro_scalar", "ns=1;s=int64_ro_scalar", + "ns=1;s=uint64_ro_scalar", "ns=1;s=float_ro_scalar", + "ns=1;s=double_ro_scalar", "ns=1;s=string_ro_scalar", + "ns=1;s=sbyte_ro_scalar", "ns=1;s=byte_ro_scalar", + "ns=1;s=datetime_ro_scalar", "ns=1;s=guid_ro_scalar", + "ns=1;s=bytestring_ro_scalar"] }, { - title: "Read-Write Arrays", - description: "Array nodes. Values are displayed comma-separated." - + " To write, enter comma-separated values (e.g. \"1, 2, 3\")." - + " Commas cannot appear inside individual values.", - nodes: [ - "ns=1;s=bool_rw_array", - "ns=1;s=int16_rw_array", - "ns=1;s=uint16_rw_array", - "ns=1;s=int32_rw_array", - "ns=1;s=uint32_rw_array", - "ns=1;s=int64_rw_array", - "ns=1;s=uint64_rw_array", - "ns=1;s=float_rw_array", - "ns=1;s=double_rw_array", - "ns=1;s=string_rw_array", - "ns=1;s=sbyte_rw_array", - "ns=1;s=byte_rw_array", - "ns=1;s=datetime_rw_array", - "ns=1;s=guid_rw_array", - "ns=1;s=bytestring_rw_array" - ] + "title": "Read-Write Arrays", + "description": "Array nodes. Values are displayed comma-separated." + + " To write, enter comma-separated values (e.g. \"1, 2, 3\")." + + " Commas cannot appear inside individual values.", + "nodes": ["ns=1;s=bool_rw_array", "ns=1;s=int16_rw_array", + "ns=1;s=uint16_rw_array", "ns=1;s=int32_rw_array", + "ns=1;s=uint32_rw_array", "ns=1;s=int64_rw_array", + "ns=1;s=uint64_rw_array", "ns=1;s=float_rw_array", + "ns=1;s=double_rw_array", "ns=1;s=string_rw_array", + "ns=1;s=sbyte_rw_array", "ns=1;s=byte_rw_array", + "ns=1;s=datetime_rw_array", "ns=1;s=guid_rw_array", + "ns=1;s=bytestring_rw_array"] }, { - title: "Index Range Write", - description: "Write to specific array elements using OPC UA index" - + " range syntax. Examples: \"0\" = first element," - + " \"0:2\" = elements 0–2. Enter the range and the value(s)" - + " to write (comma-separated for multi-element ranges).", - indexRange: true, - nodes: [ - "ns=1;s=int32_rw_array", - "ns=1;s=float_rw_array", - "ns=1;s=string_rw_array" - ] + "title": "Index Range Write", + "description": + "Write to specific array elements using OPC UA index" + + " range syntax. Examples: \"0\" = first element," + + " \"0:2\" = elements 0–2. Enter the range and the value(s)" + + " to write (comma-separated for multi-element ranges).", + "indexRange": true, + "nodes": ["ns=1;s=int32_rw_array", "ns=1;s=float_rw_array", + "ns=1;s=string_rw_array"] }, { - title: "Non-Existent Nodes", - description: "These node IDs do not exist on the server." - + " The row should show no value and no metadata in the tooltip.", - nodes: [ - "ns=1;s=does_not_exist", - "ns=99;i=12345", - "ns=1;s=also_missing" - ] + "title": "Non-Existent Nodes", + "description": "These node IDs do not exist on the server." + + " The row should show no value and no metadata in the tooltip.", + "nodes": ["ns=1;s=does_not_exist", "ns=99;i=12345", + "ns=1;s=also_missing"] }, { - title: "Empty (Monitoring Test)", - description: "No nodes on this page. All previous pages are inactive," - + " so the server log should show zero active monitored items.", - nodes: [] + "title": "Empty (Monitoring Test)", + "description": + "No nodes on this page. All previous pages are inactive," + + " so the server log should show zero active monitored items.", + "nodes": [] } ] - readonly property var currentPage: pages[pageNumber - 1] - // OPC UA ServerState enum (Part 4, Table 120). - readonly property var serverStates: [ - "Running", "Failed", "NoConfiguration", "Suspended", - "Shutdown", "Test", "CommunicationFault", "Unknown" - ] + readonly property var serverStates: ["Running", "Failed", "NoConfiguration", + "Suspended", "Shutdown", "Test", "CommunicationFault", "Unknown"] + required property StackView stackRef function formatValue(node) { var v = node.value; @@ -139,8 +102,10 @@ Page { return String(v); } - Component.onCompleted: nodePage.logFunction( - currentPage.title + " page loaded (" + currentPage.nodes.length + " nodes)") + Component.onCompleted: nodePage.logFunction(currentPage.title + + " page loaded (" + + currentPage.nodes.length + + " nodes)") ColumnLayout { anchors.fill: parent @@ -150,29 +115,37 @@ Page { // Header RowLayout { Label { - text: currentPage.title font.bold: true font.pointSize: 14 + text: currentPage.title } + Label { - text: "(" + nodePage.pageNumber + "/" + nodePage.pages.length + ")" color: "gray" + text: "(" + nodePage.pageNumber + "/" + nodePage.pages.length + + ")" + } + + Item { + Layout.fillWidth: true } - Item { Layout.fillWidth: true } + Button { text: "Disconnect" + onClicked: Bobink.disconnectFromServer() } } Label { id: pageDescription - visible: currentPage.description !== undefined - text: currentPage.description || "" - wrapMode: Text.WordWrap + + Layout.fillWidth: true color: "gray" font.italic: true - Layout.fillWidth: true + text: currentPage.description || "" + visible: currentPage.description !== undefined + wrapMode: Text.WordWrap } // Column headers @@ -181,28 +154,32 @@ Page { Layout.leftMargin: 12 Layout.rightMargin: 12 spacing: 12 + Label { - text: "Identifier" - font.bold: true Layout.preferredWidth: 160 + font.bold: true + text: "Identifier" } + Label { - text: "Value" - font.bold: true Layout.preferredWidth: 300 + font.bold: true + text: "Value" } + Label { - text: "Write" - font.bold: true Layout.fillWidth: true + font.bold: true + text: "Write" } } Rectangle { id: separator + Layout.fillWidth: true - height: 1 color: "#ccc" + height: 1 } // Nodes @@ -211,48 +188,57 @@ Page { ItemDelegate { id: delegate - required property string modelData + required property int index + required property string modelData + Layout.fillWidth: true - padding: 4 + ToolTip.delay: 400 + ToolTip.text: "Display Name: " + (node.info.displayName || "—") + + "\nDescription: " + (node.info.description + || "—") + "\nNode Class: " + + (node.info.nodeClass || "—") + "\nData Type: " + + (node.info.dataType || "—") + "\nValue Rank: " + + (node.info.valueRank || "—") + + "\nArray Dimensions: " + ( + node.info.arrayDimensions || "—") + + "\nAccess Level: " + node.info.accessLevel + + "\nStatus: " + (node.info.status || "—") + + "\nSource Time: " + ( + node.info.sourceTimestamp.toLocaleString() + || "—") + "\nServer Time: " + ( + node.info.serverTimestamp.toLocaleString() + || "—") + ToolTip.visible: hovered leftPadding: 12 + padding: 4 rightPadding: 12 - OpcUaMonitoredNode { - id: node - nodeId: delegate.modelData - monitored: nodePage.StackView.status === StackView.Active - onWriteCompleted: (success, message) => { - var short_id = node.nodeId.substring(node.nodeId.indexOf(";s=") + 3); - nodePage.logFunction(short_id + ": " + message); - } - } - background: Rectangle { color: delegate.index % 2 === 0 ? "#f8f8f8" : "transparent" radius: 2 } - contentItem: RowLayout { spacing: 12 // Column 1: Display name if available, otherwise short ID. Label { + Layout.preferredWidth: 160 + elide: Text.ElideRight text: { if (node.info.displayName) return node.info.displayName; var idx = node.nodeId.indexOf(";s="); - return idx >= 0 ? node.nodeId.substring(idx + 3) : node.nodeId; + return idx >= 0 ? node.nodeId.substring(idx + 3) : + node.nodeId; } - Layout.preferredWidth: 160 - elide: Text.ElideRight } // Column 2: Live value (always visible) Label { - text: nodePage.formatValue(node) Layout.preferredWidth: 300 elide: Text.ElideRight + text: nodePage.formatValue(node) } // Column 3: Edit area (writable) or READ-ONLY label @@ -260,81 +246,108 @@ Page { // Normal write controls (non-index-range pages) TextField { id: editField - visible: node.writable && !currentPage.indexRange + Layout.fillWidth: true placeholderText: "Enter value..." + visible: node.writable && !currentPage.indexRange + onAccepted: node.writeValue(text) } + Button { - visible: node.writable && !currentPage.indexRange text: "Write" + visible: node.writable && !currentPage.indexRange + onClicked: node.writeValue(editField.text) } // Index range write controls TextField { id: rangeField - visible: node.writable && currentPage.indexRange === true + Layout.preferredWidth: 60 placeholderText: "Range" + visible: node.writable && currentPage.indexRange + === true } + TextField { id: rangeValueField - visible: node.writable && currentPage.indexRange === true + Layout.fillWidth: true placeholderText: "Value(s)..." - onAccepted: node.writeValueAtRange(text, rangeField.text) + visible: node.writable && currentPage.indexRange + === true + + onAccepted: node.writeValueAtRange(text, + rangeField.text) } + Button { - visible: node.writable && currentPage.indexRange === true text: "Write Range" - onClicked: node.writeValueAtRange(rangeValueField.text, rangeField.text) + visible: node.writable && currentPage.indexRange + === true + + onClicked: node.writeValueAtRange(rangeValueField.text, + rangeField.text) } Label { - visible: !node.writable - text: "(READ-ONLY)" + Layout.fillWidth: true color: "gray" font.italic: true - Layout.fillWidth: true + text: "(READ-ONLY)" + visible: !node.writable } } - ToolTip.visible: hovered - ToolTip.delay: 400 - ToolTip.text: - "Display Name: " + (node.info.displayName || "—") - + "\nDescription: " + (node.info.description || "—") - + "\nNode Class: " + (node.info.nodeClass || "—") - + "\nData Type: " + (node.info.dataType || "—") - + "\nValue Rank: " + (node.info.valueRank || "—") - + "\nArray Dimensions: " + (node.info.arrayDimensions || "—") - + "\nAccess Level: " + node.info.accessLevel - + "\nStatus: " + (node.info.status || "—") - + "\nSource Time: " + (node.info.sourceTimestamp.toLocaleString() || "—") - + "\nServer Time: " + (node.info.serverTimestamp.toLocaleString() || "—") + OpcUaMonitoredNode { + id: node + + monitored: nodePage.StackView.status === StackView.Active + nodeId: delegate.modelData + + onWriteCompleted: (success, message) => { + var short_id = node.nodeId.substring( + node.nodeId.indexOf(";s=") + 3); + nodePage.logFunction(short_id + ": " + + message); + } + } } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } // Navigation RowLayout { Layout.fillWidth: true + Button { text: "← Previous" visible: nodePage.pageNumber > 1 + onClicked: nodePage.stackRef.pop() } - Item { Layout.fillWidth: true } + + Item { + Layout.fillWidth: true + } + Button { text: "Next →" visible: nodePage.pageNumber < nodePage.pages.length + onClicked: nodePage.stackRef.push("NodePage.qml", { - stackRef: nodePage.stackRef, - pageNumber: nodePage.pageNumber + 1, - logFunction: nodePage.logFunction - }) + "stackRef": + nodePage.stackRef, + "pageNumber": + nodePage.pageNumber + 1, + "logFunction": + nodePage.logFunction + }) } } } |
