aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--CMakeLists.txt66
-rw-r--r--Main.qml432
-rw-r--r--NodePage.qml341
m---------deps/BobinkQtOpcUa0
5 files changed, 50 insertions, 790 deletions
diff --git a/.gitignore b/.gitignore
index 0e87258..6f639c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.qtcreator/
build/
+.qmlls.ini
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b464cce..4b0403c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,40 +8,60 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 6.10.2 REQUIRED COMPONENTS Quick)
qt_standard_project_setup(REQUIRES 6.10.2)
+# ── QML tooling ────────────────────────────────────────────────────────────
+# Generate .qmlls.ini for QML Language Server
+set(QT_QML_GENERATE_QMLLS_INI
+ ON
+ CACHE BOOL "")
+
# ── BobinkQtOpcUa ──────────────────────────────────────────────────────────
-# Builds open62541 and QtOpcUa from submodules automatically on first
-# configure. Subsequent configures skip the dep builds (delete
-# build/deps/*-install/ to force a rebuild).
+# Builds open62541 and QtOpcUa from submodules automatically on first configure.
+# Subsequent configures skip the dep builds (delete build/deps/*-install/ to
+# force a rebuild).
#
-# Prerequisites: Qt 6.10.2+ with qt-cmake in PATH, OpenSSL dev headers,
-# Ninja, and initialized submodules:
-# git submodule update --init --recursive
+# Prerequisites: Qt 6.10.2+ with qt-cmake in PATH, OpenSSL dev headers, Ninja,
+# and initialized submodules: git submodule update --init --recursive
add_subdirectory(deps/BobinkQtOpcUa)
+# Ensure the qml/ import directory exists before qmlimportscanner runs
+file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
+
qt_add_executable(BobinkQtOpcUaAppTemplate main.cpp)
-qt_add_qml_module(BobinkQtOpcUaAppTemplate URI BobinkQtOpcUaAppTemplate VERSION 1.0 QML_FILES Main.qml NodePage.qml)
+qt_add_qml_module(
+ BobinkQtOpcUaAppTemplate
+ URI
+ BobinkQtOpcUaAppTemplate
+ VERSION
+ 1.0
+ QML_FILES
+ Main.qml
+ IMPORT_PATH
+ "${CMAKE_BINARY_DIR}/deps/BobinkQtOpcUa/qml")
# Executable goes to bin/ to avoid clashing with the QML module directory
-set_target_properties(BobinkQtOpcUaAppTemplate PROPERTIES
- RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
+set_target_properties(
+ BobinkQtOpcUaAppTemplate PROPERTIES RUNTIME_OUTPUT_DIRECTORY
+ "${CMAKE_BINARY_DIR}/bin")
-# Link against BobinkQtOpcUaplugin (not BobinkQtOpcUa). The "plugin"
-# target includes the QML type registration needed for `import Bobink`.
-target_link_libraries(BobinkQtOpcUaAppTemplate PRIVATE Qt6::Quick BobinkQtOpcUaplugin)
+# Link against BobinkQtOpcUaplugin (not BobinkQtOpcUa). The "plugin" target
+# includes the QML type registration needed for `import Bobink`.
+target_link_libraries(BobinkQtOpcUaAppTemplate PRIVATE Qt6::Quick
+ BobinkQtOpcUaplugin)
# ── Runtime quirks ──────────────────────────────────────────────────────────
-# BobinkQtOpcUa builds open62541 as a shared library. The QtOpcUa backend
-# plugin (loaded at runtime via dlopen) needs to find it. These two lines
-# make that work during development:
+# BobinkQtOpcUa builds open62541 as a shared library. The QtOpcUa backend plugin
+# (loaded at runtime via dlopen) needs to find it. These two lines make that
+# work during development:
-# 1. RPATH: embed the dep lib dirs so the dynamic linker finds libopen62541
-# and libQt6OpcUa at runtime without installing them system-wide.
+# RPATH: embed the dep lib dirs so the dynamic linker finds libopen62541 and
+# libQt6OpcUa at runtime without installing them system-wide.
set(CMAKE_BUILD_RPATH "${QTOPCUA_INSTALL_DIR}/lib"
"${OPEN62541_INSTALL_DIR}/lib")
-# 2. Plugin path: Qt's plugin loader doesn't know about our locally-built
-# QtOpcUa backend. Pass the path as a compile definition and call
-# QCoreApplication::addLibraryPath(QTOPCUA_PLUGIN_PATH) in main.cpp
-# *before* creating the QGuiApplication.
-target_compile_definitions(BobinkQtOpcUaAppTemplate PRIVATE
- QTOPCUA_PLUGIN_PATH="${QTOPCUA_INSTALL_DIR}/plugins")
+# Plugin path: Qt's plugin loader doesn't know about our locally-built QtOpcUa
+# backend. Pass the path as a compile definition and call
+# QCoreApplication::addLibraryPath(QTOPCUA_PLUGIN_PATH) in main.cpp *before*
+# creating the QGuiApplication.
+target_compile_definitions(
+ BobinkQtOpcUaAppTemplate
+ PRIVATE QTOPCUA_PLUGIN_PATH="${QTOPCUA_INSTALL_DIR}/plugins")
diff --git a/Main.qml b/Main.qml
index 361e3bd..1598bb8 100644
--- a/Main.qml
+++ b/Main.qml
@@ -3,436 +3,16 @@
import QtQuick
import QtQuick.Controls
-import QtQuick.Layouts
-import QtQuick.Dialogs
-import Bobink
+import Bobink // This should properly integrate with qmlls
ApplicationWindow {
id: root
- width: 800
+
height: 900
- visible: true
title: "Bobink Demo"
+ visible: true
+ width: 800
- property bool autoConnectFailed: false
- property bool showPkiSettings: false
-
- Connections {
- target: Bobink
- function onServersChanged() {
- debugConsole.appendLog("Discovered server list updated");
- }
- 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
- });
- } 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();
- }
- }
-
- Dialog {
- id: certTrustDialog
- property string certInfo
- anchors.centerIn: parent
- title: "Certificate Trust"
- modal: true
- standardButtons: Dialog.Yes | Dialog.No
- Label {
- text: certTrustDialog.certInfo
- }
- onAccepted: Bobink.acceptCertificate()
- onRejected: Bobink.rejectCertificate()
- }
-
- ColumnLayout {
- anchors.fill: parent
- spacing: 0
-
- StackView {
- id: stack
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- initialItem: Page {
- id: connectionPage
- Component.onCompleted: {
- Bobink.discoveryUrl = discoveryUrlField.text;
- Bobink.startDiscovery();
- }
-
- OpcUaAuth {
- id: auth
- mode: authModeCombo.currentValue
- username: usernameField.text
- password: passwordField.text
- certPath: Bobink.certFile
- keyPath: Bobink.keyFile
- }
-
- 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://", "")
- }
-
- ColumnLayout {
- anchors.fill: parent
- anchors.margins: 20
- spacing: 12
-
- Label {
- text: "Discovery URL"
- font.bold: true
- }
-
- 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 {
- text: Bobink.discovering ? "Discovering... (" + Bobink.servers.length + " found)" : Bobink.servers.length + " server(s)"
- font.italic: true
- }
-
- ListView {
- id: serverListView
- Layout.fillWidth: true
- Layout.preferredHeight: 100
- clip: true
- model: Bobink.servers
- 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
- color: "gray"
- font.italic: true
- font.pointSize: 8
- elide: Text.ElideRight
- Layout.fillWidth: true
- }
- }
- onClicked: {
- if (modelData.discoveryUrls.length > 0)
- serverUrlField.text = modelData.discoveryUrls[0];
- }
- }
- ScrollBar.vertical: ScrollBar {
- policy: ScrollBar.AsNeeded
- }
- }
-
- RowLayout {
- Label {
- text: "PKI"
- font.bold: true
- }
- Label {
- text: Bobink.certFile ? " (" + Bobink.certFile.split("/").pop() + ")" : " (no certificate found)"
- font.italic: true
- color: Bobink.certFile ? "green" : "gray"
- }
- Item {
- Layout.fillWidth: true
- }
- Button {
- text: root.showPkiSettings ? "Hide" : "Configure"
- onClicked: root.showPkiSettings = !root.showPkiSettings
- }
- }
-
- GridLayout {
- columns: 3
- Layout.fillWidth: true
- visible: root.showPkiSettings
-
- Label {
- text: "Certificate:"
- }
- TextField {
- id: certFileField
- Layout.fillWidth: true
- text: Bobink.certFile
- placeholderText: "Client certificate (.der)"
- }
- Button {
- text: "Browse"
- onClicked: certFileDialog.open()
- }
-
- Label {
- text: "Private key:"
- }
- TextField {
- id: keyFileField
- Layout.fillWidth: true
- text: Bobink.keyFile
- placeholderText: "Private key (.pem, .crt)"
- }
- 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
- visible: root.showPkiSettings
- onClicked: {
- Bobink.pkiDir = trustFolderField.text;
- Bobink.certFile = certFileField.text;
- Bobink.keyFile = keyFileField.text;
- Bobink.applyPki();
- }
- }
-
- Label {
- text: "Server URL"
- font.bold: true
- }
-
- TextField {
- id: serverUrlField
- Layout.fillWidth: true
- placeholderText: "opc.tcp://..."
- }
-
- Label {
- text: "Authentication"
- font.bold: true
- }
-
- ComboBox {
- id: authModeCombo
- Layout.fillWidth: true
- textRole: "text"
- valueRole: "mode"
- model: [
- {
- text: "Anonymous",
- mode: OpcUaAuth.Anonymous
- },
- {
- text: "Username / Password",
- mode: OpcUaAuth.UserPass
- },
- {
- text: "Certificate",
- mode: OpcUaAuth.Certificate
- }
- ]
- }
-
- GridLayout {
- columns: 2
- visible: authModeCombo.currentValue === OpcUaAuth.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
- }
- }
-
- Button {
- text: "Connect"
- Layout.fillWidth: true
- onClicked: {
- root.autoConnectFailed = false;
- Bobink.auth = auth;
- Bobink.serverUrl = serverUrlField.text;
- Bobink.connectToServer();
- }
- }
-
- Label {
- text: "Direct Connect"
- font.bold: true
- visible: root.autoConnectFailed
- }
-
- 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: Bobink.Basic256Sha256
- },
- {
- text: "Aes128-Sha256-RsaOaep",
- policy: Bobink.Aes128_Sha256_RsaOaep
- },
- {
- text: "Aes256-Sha256-RsaPss",
- policy: Bobink.Aes256_Sha256_RsaPss
- }
- ]
- }
-
- Label {
- text: "Security mode:"
- }
- ComboBox {
- id: securityModeCombo
- Layout.fillWidth: true
- textRole: "text"
- valueRole: "mode"
- model: [
- {
- text: "Sign & Encrypt",
- mode: Bobink.SignAndEncrypt
- },
- {
- text: "Sign",
- mode: Bobink.Sign
- },
- {
- text: "None",
- mode: Bobink.None
- }
- ]
- }
- }
-
- Button {
- text: "Direct Connect"
- Layout.fillWidth: true
- visible: root.autoConnectFailed
- onClicked: {
- Bobink.auth = auth;
- Bobink.serverUrl = serverUrlField.text;
- Bobink.connectDirect(securityPolicyCombo.currentValue, securityModeCombo.currentValue);
- }
- }
-
- Item {
- Layout.fillHeight: true
- }
- }
- }
- }
-
- 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");
- debugLog.text += "[" + ts + "] " + msg + "\n";
- debugLog.cursorPosition = debugLog.text.length;
- }
-
- ScrollView {
- anchors.fill: parent
- anchors.margins: 4
- TextArea {
- id: debugLog
- readOnly: true
- color: "#cccccc"
- font.family: "monospace"
- font.pointSize: 9
- wrapMode: TextEdit.Wrap
- background: null
- }
- }
- }
- }
+ // Add your app here!
+ // Look at deps/BobinkQtOpcUa/demo for an example.
}
diff --git a/NodePage.qml b/NodePage.qml
deleted file mode 100644
index e089f0e..0000000
--- a/NodePage.qml
+++ /dev/null
@@ -1,341 +0,0 @@
-// NodePage.qml — Displays 10 OPC UA nodes per page with tooltips.
-
-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
-
- 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: "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-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: "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: []
- }
- ]
-
- 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"
- ]
-
- function formatValue(node) {
- var v = node.value;
- if (v === undefined || String(v) === "")
- return "—";
- // Map ServerState enum to human-readable string.
- if (node.nodeId === "ns=0;i=2259" && !isNaN(v))
- return serverStates[v] || String(v);
- return String(v);
- }
-
- Component.onCompleted: nodePage.logFunction(
- currentPage.title + " page loaded (" + currentPage.nodes.length + " nodes)")
-
- ColumnLayout {
- anchors.fill: parent
- anchors.margins: 20
- spacing: 4
-
- // Header
- RowLayout {
- Label {
- text: currentPage.title
- font.bold: true
- font.pointSize: 14
- }
- Label {
- text: "(" + nodePage.pageNumber + "/" + nodePage.pages.length + ")"
- color: "gray"
- }
- Item { Layout.fillWidth: true }
- Button {
- text: "Disconnect"
- onClicked: Bobink.disconnectFromServer()
- }
- }
-
- Label {
- id: pageDescription
- visible: currentPage.description !== undefined
- text: currentPage.description || ""
- wrapMode: Text.WordWrap
- color: "gray"
- font.italic: true
- Layout.fillWidth: true
- }
-
- // Column headers
- RowLayout {
- Layout.fillWidth: true
- Layout.leftMargin: 12
- Layout.rightMargin: 12
- spacing: 12
- Label {
- text: "Identifier"
- font.bold: true
- Layout.preferredWidth: 160
- }
- Label {
- text: "Value"
- font.bold: true
- Layout.preferredWidth: 300
- }
- Label {
- text: "Write"
- font.bold: true
- Layout.fillWidth: true
- }
- }
-
- Rectangle {
- id: separator
- Layout.fillWidth: true
- height: 1
- color: "#ccc"
- }
-
- // Nodes
- Repeater {
- model: currentPage.nodes
-
- ItemDelegate {
- id: delegate
- required property string modelData
- required property int index
- Layout.fillWidth: true
- padding: 4
- leftPadding: 12
- 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 {
- 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;
- }
- Layout.preferredWidth: 160
- elide: Text.ElideRight
- }
-
- // Column 2: Live value (always visible)
- Label {
- text: nodePage.formatValue(node)
- Layout.preferredWidth: 300
- elide: Text.ElideRight
- }
-
- // Column 3: Edit area (writable) or READ-ONLY label
-
- // Normal write controls (non-index-range pages)
- TextField {
- id: editField
- visible: node.writable && !currentPage.indexRange
- Layout.fillWidth: true
- placeholderText: "Enter value..."
- onAccepted: node.writeValue(text)
- }
- Button {
- visible: node.writable && !currentPage.indexRange
- text: "Write"
- onClicked: node.writeValue(editField.text)
- }
-
- // Index range write controls
- TextField {
- id: rangeField
- visible: node.writable && currentPage.indexRange === true
- Layout.preferredWidth: 60
- placeholderText: "Range"
- }
- TextField {
- id: rangeValueField
- visible: node.writable && currentPage.indexRange === true
- Layout.fillWidth: true
- placeholderText: "Value(s)..."
- onAccepted: node.writeValueAtRange(text, rangeField.text)
- }
- Button {
- visible: node.writable && currentPage.indexRange === true
- text: "Write Range"
- onClicked: node.writeValueAtRange(rangeValueField.text, rangeField.text)
- }
-
- Label {
- visible: !node.writable
- text: "(READ-ONLY)"
- color: "gray"
- font.italic: true
- Layout.fillWidth: true
- }
- }
-
- 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() || "—")
- }
- }
-
- Item { Layout.fillHeight: true }
-
- // Navigation
- RowLayout {
- Layout.fillWidth: true
- Button {
- text: "← Previous"
- visible: nodePage.pageNumber > 1
- onClicked: nodePage.stackRef.pop()
- }
- 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
- })
- }
- }
- }
-}
diff --git a/deps/BobinkQtOpcUa b/deps/BobinkQtOpcUa
-Subproject 9507f9779809bd077705d1ad54384f714b41c12
+Subproject 7bb49eabdf8d86567660c8825892eb323fa6e67