diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | CMakeLists.txt | 66 | ||||
| -rw-r--r-- | Main.qml | 432 | ||||
| -rw-r--r-- | NodePage.qml | 341 | ||||
| m--------- | deps/BobinkQtOpcUa | 0 |
5 files changed, 50 insertions, 790 deletions
@@ -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") @@ -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 |
