diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 05:13:42 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-19 05:38:24 +0100 |
| commit | a0c7f2a7ef04dbe2e7491eabf828e26423d1bd10 (patch) | |
| tree | 50491c80f28ad5f2789a3777bec74489327c4d7f | |
| parent | 23916cbb98e952aab752a647ac96020aab709bb6 (diff) | |
| download | BobinkQtOpcUa-a0c7f2a7ef04dbe2e7491eabf828e26423d1bd10.tar.gz BobinkQtOpcUa-a0c7f2a7ef04dbe2e7491eabf828e26423d1bd10.zip | |
Rename targets, route messages to debug console, clean up
- Rename CMake project and library target to BobinkQtOpcUa (URI stays Bobink)
- Rename demo target to BobinkDemo, output binary to build/bin/
- Route all QML status/error messages to the in-app debug console
- Remove discoveryInterval QML property, default to 30s internally
- Add errorChanged handler, PKI file validation in applyPki()
| -rw-r--r-- | CMakeLists.txt | 23 | ||||
| -rw-r--r-- | demo/CMakeLists.txt | 30 | ||||
| -rw-r--r-- | demo/Main.qml | 243 | ||||
| -rw-r--r-- | src/BobinkClient.cpp | 685 | ||||
| -rw-r--r-- | src/BobinkClient.h | 9 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 27 |
6 files changed, 622 insertions, 395 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 73ab94d..2607734 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(Bobink LANGUAGES CXX) +project(BobinkQtOpcUa LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -8,19 +8,26 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Build external dependencies (open62541, qtopcua) if not already built include(cmake/BuildDeps.cmake) -# Local QtOpcUa must come before system Qt so find_package picks up our -# build instead of any system-installed QtOpcUa. +# Local QtOpcUa must come before system Qt so find_package picks up our build +# instead of any system-installed QtOpcUa. list(PREPEND CMAKE_PREFIX_PATH "${QTOPCUA_BUILD_DIR}") list(PREPEND CMAKE_PREFIX_PATH "${OPEN62541_INSTALL_DIR}") find_package(Qt6 6.10.2 REQUIRED COMPONENTS Core Qml Quick OpcUa) qt_standard_project_setup(REQUIRES 6.10.2) -# Ensure the local QtOpcUa and open62541 libs are findable at runtime -# (needed because the Qt plugin loader dlopen's the open62541 backend). -set(CMAKE_BUILD_RPATH - "${QTOPCUA_BUILD_DIR}/lib" - "${OPEN62541_INSTALL_DIR}/lib") +# Set path for qmllint +set(QML_IMPORT_PATH + "${CMAKE_CURRENT_BINARY_DIR}/qml" + CACHE STRING "Path to locally built qml") +# Generate .qmlls.ini for QML Language Server. Useful once QtOpcUa is installed +# globally (qt-cmake --install build/deps/qtopcua-build) so qmlls can resolve +# all Qt QML imports without extra importPaths. set(QT_QML_GENERATE_QMLLS_INI ON +# CACHE BOOL "") + +# Ensure the local QtOpcUa and open62541 libs are findable at runtime (needed +# because the Qt plugin loader dlopen's the open62541 backend). +set(CMAKE_BUILD_RPATH "${QTOPCUA_BUILD_DIR}/lib" "${OPEN62541_INSTALL_DIR}/lib") add_subdirectory(src) add_subdirectory(demo) diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index a12e090..9fb3093 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -1,18 +1,20 @@ -qt_add_executable(bobink-demo main.cpp) +qt_add_executable(BobinkDemo main.cpp) -qt_add_qml_module(bobink-demo - URI BobinkDemo - VERSION 1.0 - QML_FILES - Main.qml - # LoginPage.qml - # NodePage.qml - NO_RESOURCE_TARGET_PATH -) +qt_add_qml_module( + BobinkDemo + URI + BobinkDemo + VERSION + 1.0 + QML_FILES + Main.qml) -target_link_libraries(bobink-demo PRIVATE Qt6::Quick bobink) +# Executable goes to bin/ to avoid clashing with the QML module directory +set_target_properties(BobinkDemo PROPERTIES RUNTIME_OUTPUT_DIRECTORY + "${CMAKE_BINARY_DIR}/bin") + +target_link_libraries(BobinkDemo PRIVATE Qt6::Quick BobinkQtOpcUa) # Tell the demo where to find the locally-built OpcUa plugin at runtime -target_compile_definitions(bobink-demo PRIVATE - QTOPCUA_PLUGIN_PATH="${QTOPCUA_BUILD_DIR}/plugins" -) +target_compile_definitions( + BobinkDemo PRIVATE QTOPCUA_PLUGIN_PATH="${QTOPCUA_BUILD_DIR}/plugins") diff --git a/demo/Main.qml b/demo/Main.qml index fe4f369..5bdf369 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -7,8 +7,9 @@ import QtQuick.Dialogs import Bobink ApplicationWindow { - width: 600 - height: 800 + id: root + width: 800 + height: 900 visible: true title: "Bobink Demo" @@ -16,30 +17,33 @@ ApplicationWindow { property bool showPkiSettings: false Component.onCompleted: { - BobinkClient.discoveryUrl = discoveryUrlField.text - BobinkClient.startDiscovery() + BobinkClient.discoveryUrl = discoveryUrlField.text; + BobinkClient.startDiscovery(); } Connections { target: BobinkClient function onServersChanged() { - console.log("Discovered server list updated") + debugConsole.appendLog("Discovered server list updated"); } function onConnectedChanged() { - console.log("Connected:", BobinkClient.connected) + debugConsole.appendLog("Connected: " + BobinkClient.connected); if (BobinkClient.connected) - autoConnectFailed = false + root.autoConnectFailed = false; } function onConnectionError(message) { - console.log("Connection error:", message) - autoConnectFailed = true + debugConsole.appendLog("ERROR: " + message); + root.autoConnectFailed = true; + } + function onStatusMessage(message) { + debugConsole.appendLog(message); } function onDiscoveringChanged() { - console.log("Discovering:", BobinkClient.discovering) + debugConsole.appendLog("Discovering: " + BobinkClient.discovering); } function onCertificateTrustRequested(certInfo) { - certTrustDialog.certInfo = certInfo - certTrustDialog.open() + certTrustDialog.certInfo = certInfo; + certTrustDialog.open(); } } @@ -50,7 +54,9 @@ 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() } @@ -60,7 +66,10 @@ ApplicationWindow { anchors.margins: 20 spacing: 12 - Label { text: "Discovery URL"; font.bold: true } + Label { + text: "Discovery URL" + font.bold: true + } RowLayout { TextField { @@ -72,19 +81,17 @@ ApplicationWindow { text: BobinkClient.discovering ? "Stop" : "Discover" onClicked: { if (BobinkClient.discovering) { - BobinkClient.stopDiscovery() + BobinkClient.stopDiscovery(); } else { - BobinkClient.discoveryUrl = discoveryUrlField.text - BobinkClient.startDiscovery() + BobinkClient.discoveryUrl = discoveryUrlField.text; + BobinkClient.startDiscovery(); } } } } Label { - text: BobinkClient.discovering - ? "Discovering... (" + BobinkClient.servers.length + " found)" - : BobinkClient.servers.length + " server(s)" + text: BobinkClient.discovering ? "Discovering... (" + BobinkClient.servers.length + " found)" : BobinkClient.servers.length + " server(s)" font.italic: true } @@ -94,30 +101,34 @@ ApplicationWindow { 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] + serverUrlField.text = modelData.discoveryUrls[0]; } } ScrollBar.vertical: ScrollBar {} } RowLayout { - Label { text: "PKI"; font.bold: true } Label { - text: BobinkClient.certFile - ? " (" + BobinkClient.certFile.split("/").pop() + ")" - : " (no certificate found)" + 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 } + Item { + Layout.fillWidth: true + } Button { - text: showPkiSettings ? "Hide" : "Configure..." + text: root.showPkiSettings ? "Hide" : "Configure..." flat: true - onClicked: showPkiSettings = !showPkiSettings + onClicked: root.showPkiSettings = !root.showPkiSettings } } @@ -147,9 +158,11 @@ ApplicationWindow { GridLayout { columns: 3 Layout.fillWidth: true - visible: showPkiSettings + visible: root.showPkiSettings - Label { text: "Certificate:" } + Label { + text: "Certificate:" + } TextField { id: certFileField Layout.fillWidth: true @@ -161,7 +174,9 @@ ApplicationWindow { onClicked: certFileDialog.open() } - Label { text: "Private key:" } + Label { + text: "Private key:" + } TextField { id: keyFileField Layout.fillWidth: true @@ -173,7 +188,9 @@ ApplicationWindow { onClicked: keyFileDialog.open() } - Label { text: "Trust folder:" } + Label { + text: "Trust folder:" + } TextField { id: trustFolderField Layout.fillWidth: true @@ -188,16 +205,19 @@ ApplicationWindow { Button { text: "Apply PKI" Layout.fillWidth: true - visible: showPkiSettings + visible: root.showPkiSettings onClicked: { - BobinkClient.pkiDir = trustFolderField.text - BobinkClient.certFile = certFileField.text - BobinkClient.keyFile = keyFileField.text - BobinkClient.applyPki() + BobinkClient.pkiDir = trustFolderField.text; + BobinkClient.certFile = certFileField.text; + BobinkClient.keyFile = keyFileField.text; + BobinkClient.applyPki(); } } - Label { text: "Server URL"; font.bold: true } + Label { + text: "Server URL" + font.bold: true + } TextField { id: serverUrlField @@ -205,15 +225,18 @@ ApplicationWindow { placeholderText: "opc.tcp://..." } - Label { text: "Authentication"; font.bold: true } + Label { + text: "Authentication" + font.bold: true + } BobinkAuth { id: auth mode: authModeCombo.currentValue username: usernameField.text password: passwordField.text - certPath: certPathField.text - keyPath: keyPathField.text + certPath: BobinkClient.certFile + keyPath: BobinkClient.keyFile } ComboBox { @@ -222,9 +245,18 @@ ApplicationWindow { textRole: "text" valueRole: "mode" model: [ - { text: "Anonymous", mode: BobinkAuth.Anonymous }, - { text: "Username / Password", mode: BobinkAuth.UserPass }, - { text: "Certificate", mode: BobinkAuth.Certificate } + { + text: "Anonymous", + mode: BobinkAuth.Anonymous + }, + { + text: "Username / Password", + mode: BobinkAuth.UserPass + }, + { + text: "Certificate", + mode: BobinkAuth.Certificate + } ] } @@ -233,12 +265,16 @@ ApplicationWindow { visible: authModeCombo.currentValue === BobinkAuth.UserPass Layout.fillWidth: true - Label { text: "Username:" } + Label { + text: "Username:" + } TextField { id: usernameField Layout.fillWidth: true } - Label { text: "Password:" } + Label { + text: "Password:" + } TextField { id: passwordField Layout.fillWidth: true @@ -246,36 +282,17 @@ ApplicationWindow { } } - GridLayout { - columns: 2 - visible: authModeCombo.currentValue === BobinkAuth.Certificate - Layout.fillWidth: true - - Label { text: "Certificate:" } - TextField { - id: certPathField - Layout.fillWidth: true - placeholderText: "/path/to/cert.pem" - } - Label { text: "Private key:" } - TextField { - id: keyPathField - Layout.fillWidth: true - placeholderText: "/path/to/key.pem" - } - } - Button { text: BobinkClient.connected ? "Disconnect" : "Connect" Layout.fillWidth: true onClicked: { if (BobinkClient.connected) { - BobinkClient.disconnectFromServer() + BobinkClient.disconnectFromServer(); } else { - autoConnectFailed = false - BobinkClient.auth = auth - BobinkClient.serverUrl = serverUrlField.text - BobinkClient.connectToServer() + root.autoConnectFailed = false; + BobinkClient.auth = auth; + BobinkClient.serverUrl = serverUrlField.text; + BobinkClient.connectToServer(); } } } @@ -283,37 +300,59 @@ ApplicationWindow { Label { text: "Direct Connect" font.bold: true - visible: autoConnectFailed && !BobinkClient.connected + visible: root.autoConnectFailed && !BobinkClient.connected } GridLayout { columns: 2 Layout.fillWidth: true - visible: autoConnectFailed && !BobinkClient.connected + visible: root.autoConnectFailed && !BobinkClient.connected - Label { text: "Security policy:" } + 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 } + { + 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:" } + 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 } + { + text: "Sign & Encrypt", + mode: BobinkClient.SignAndEncrypt + }, + { + text: "Sign", + mode: BobinkClient.Sign + }, + { + text: "None", + mode: BobinkClient.None + } ] } } @@ -321,12 +360,11 @@ ApplicationWindow { Button { text: "Direct Connect" Layout.fillWidth: true - visible: autoConnectFailed && !BobinkClient.connected + visible: root.autoConnectFailed && !BobinkClient.connected onClicked: { - BobinkClient.auth = auth - BobinkClient.serverUrl = serverUrlField.text - BobinkClient.connectDirect(securityPolicyCombo.currentValue, - securityModeCombo.currentValue) + BobinkClient.auth = auth; + BobinkClient.serverUrl = serverUrlField.text; + BobinkClient.connectDirect(securityPolicyCombo.currentValue, securityModeCombo.currentValue); } } @@ -335,6 +373,39 @@ ApplicationWindow { color: BobinkClient.connected ? "green" : "red" } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } + + Rectangle { + id: debugConsole + Layout.fillWidth: true + Layout.preferredHeight: 100 + color: "#1e1e1e" + border.color: "#444" + radius: 4 + + function appendLog(msg) { + let ts = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss"); + statusLog.text += "[" + ts + "] " + msg + "\n"; + statusLog.cursorPosition = statusLog.text.length; + } + + ScrollView { + anchors.fill: parent + anchors.margins: 4 + + TextArea { + id: statusLog + readOnly: true + color: "#cccccc" + font.family: "monospace" + font.pointSize: 9 + wrapMode: TextEdit.Wrap + background: null + } + } + } } + } diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp index 565868c..e7f6d0d 100644 --- a/src/BobinkClient.cpp +++ b/src/BobinkClient.cpp @@ -11,433 +11,582 @@ BobinkClient *BobinkClient::s_instance = nullptr; -static QString defaultPkiDir() +static QString +defaultPkiDir () { - return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) - + QStringLiteral("/pki"); + return QStandardPaths::writableLocation (QStandardPaths::AppDataLocation) + + QStringLiteral ("/pki"); } /** @brief Create the standard OPC UA PKI directory tree. */ -static void ensurePkiDirs(const QString &base) +static void +ensurePkiDirs (const QString &base) { - for (const auto *sub : {"own/certs", "own/private", - "trusted/certs", "trusted/crl", - "issuers/certs", "issuers/crl"}) { - QDir().mkpath(base + QLatin1Char('/') + QLatin1String(sub)); + for (const auto *sub : { "own/certs", "own/private", "trusted/certs", + "trusted/crl", "issuers/certs", "issuers/crl" }) + { + QDir ().mkpath (base + QLatin1Char ('/') + QLatin1String (sub)); } } -BobinkClient::BobinkClient(QObject *parent) - : QObject(parent) - , m_provider(new QOpcUaProvider(this)) - , m_pkiDir(defaultPkiDir()) +BobinkClient::BobinkClient (QObject *parent) + : QObject (parent), m_provider (new QOpcUaProvider (this)), + m_pkiDir (defaultPkiDir ()) { - ensurePkiDirs(m_pkiDir); - setupClient(); - autoDetectPki(); - applyPki(); - connect(&m_discoveryTimer, &QTimer::timeout, this, &BobinkClient::doDiscovery); + ensurePkiDirs (m_pkiDir); + setupClient (); + autoDetectPki (); + applyPki (); + connect (&m_discoveryTimer, &QTimer::timeout, this, + &BobinkClient::doDiscovery); } -BobinkClient::~BobinkClient() +BobinkClient::~BobinkClient () { - if (s_instance == this) - s_instance = nullptr; + if (s_instance == this) + s_instance = nullptr; } -BobinkClient *BobinkClient::instance() +BobinkClient * +BobinkClient::instance () { - return s_instance; + return s_instance; } -BobinkClient *BobinkClient::create(QQmlEngine *, QJSEngine *) +BobinkClient * +BobinkClient::create (QQmlEngine *, QJSEngine *) { - if (!s_instance) { - s_instance = new BobinkClient; - QJSEngine::setObjectOwnership(s_instance, QJSEngine::CppOwnership); + if (!s_instance) + { + s_instance = new BobinkClient; + QJSEngine::setObjectOwnership (s_instance, QJSEngine::CppOwnership); } - return s_instance; + return s_instance; } -void BobinkClient::setupClient() +void +BobinkClient::setupClient () { - m_client = m_provider->createClient(QStringLiteral("open62541")); - if (!m_client) { - qWarning() << "BobinkClient: failed to create open62541 backend"; - return; + m_client = m_provider->createClient (QStringLiteral ("open62541")); + if (!m_client) + { + qWarning () << "BobinkClient: failed to create open62541 backend"; + return; } - connect(m_client, &QOpcUaClient::stateChanged, - this, &BobinkClient::handleStateChanged); - connect(m_client, &QOpcUaClient::endpointsRequestFinished, - this, &BobinkClient::handleEndpointsReceived); - connect(m_client, &QOpcUaClient::connectError, - this, &BobinkClient::handleConnectError); - connect(m_client, &QOpcUaClient::findServersFinished, - this, &BobinkClient::handleFindServersFinished); + connect (m_client, &QOpcUaClient::stateChanged, this, + &BobinkClient::handleStateChanged); + connect (m_client, &QOpcUaClient::endpointsRequestFinished, this, + &BobinkClient::handleEndpointsReceived); + connect (m_client, &QOpcUaClient::connectError, this, + &BobinkClient::handleConnectError); + connect (m_client, &QOpcUaClient::findServersFinished, this, + &BobinkClient::handleFindServersFinished); + connect (m_client, &QOpcUaClient::errorChanged, this, + [this] (QOpcUaClient::ClientError error) + { + static const char *names[] + = { "NoError", + "InvalidUrl", + "AccessDenied", + "ConnectionError", + "UnknownError", + "UnsupportedAuthenticationInformation", + "InvalidAuthenticationInformation", + "InvalidEndpointDescription", + "NoMatchingUserIdentityTokenFound", + "UnsupportedSecurityPolicy", + "InvalidPki" }; + int idx = static_cast<int> (error); + if (idx > 0 && idx <= 10) + emit connectionError ( + QStringLiteral ("Client error: %1").arg (names[idx])); + }); } /* ====================================== * Connection properties * ====================================== */ -bool BobinkClient::connected() const { return m_connected; } +bool +BobinkClient::connected () const +{ + return m_connected; +} -QString BobinkClient::serverUrl() const { return m_serverUrl; } +QString +BobinkClient::serverUrl () const +{ + return m_serverUrl; +} -void BobinkClient::setServerUrl(const QString &url) +void +BobinkClient::setServerUrl (const QString &url) { - if (m_serverUrl == url) - return; - m_serverUrl = url; - emit serverUrlChanged(); + if (m_serverUrl == url) + return; + m_serverUrl = url; + emit serverUrlChanged (); } -BobinkAuth *BobinkClient::auth() const { return m_auth; } +BobinkAuth * +BobinkClient::auth () const +{ + return m_auth; +} -void BobinkClient::setAuth(BobinkAuth *auth) +void +BobinkClient::setAuth (BobinkAuth *auth) { - if (m_auth == auth) - return; - m_auth = auth; - emit authChanged(); + if (m_auth == auth) + return; + m_auth = auth; + emit authChanged (); } -QOpcUaClient *BobinkClient::opcuaClient() const { return m_client; } +QOpcUaClient * +BobinkClient::opcuaClient () const +{ + return m_client; +} /* ====================================== * Connection methods * ====================================== */ -void BobinkClient::connectToServer() +void +BobinkClient::connectToServer () { - if (!m_client) { - emit connectionError(QStringLiteral("OPC UA backend not available")); - return; + if (!m_client) + { + emit connectionError (QStringLiteral ("OPC UA backend not available")); + return; } - if (m_serverUrl.isEmpty()) { - emit connectionError(QStringLiteral("No server URL set")); - return; + if (m_serverUrl.isEmpty ()) + { + emit connectionError (QStringLiteral ("No server URL set")); + return; } - if (m_client->state() != QOpcUaClient::Disconnected) { - emit connectionError(QStringLiteral("Already connected or connecting")); - return; + if (m_client->state () != QOpcUaClient::Disconnected) + { + emit connectionError ( + QStringLiteral ("Already connected or connecting")); + return; } - QUrl url(m_serverUrl); - if (!url.isValid()) { - emit connectionError(QStringLiteral("Invalid server URL: %1").arg(m_serverUrl)); - return; + QUrl url (m_serverUrl); + if (!url.isValid ()) + { + emit connectionError ( + QStringLiteral ("Invalid server URL: %1").arg (m_serverUrl)); + return; } - m_client->requestEndpoints(url); + m_client->requestEndpoints (url); } -static QString securityPolicyUri(BobinkClient::SecurityPolicy policy) +static QString +securityPolicyUri (BobinkClient::SecurityPolicy policy) { - switch (policy) { + switch (policy) + { case BobinkClient::Basic256Sha256: - return QStringLiteral( - "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); case BobinkClient::Aes128_Sha256_RsaOaep: - return QStringLiteral( - "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"); + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"); case BobinkClient::Aes256_Sha256_RsaPss: - return QStringLiteral( - "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss"); + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss"); } - return {}; + return {}; } -void BobinkClient::connectDirect(SecurityPolicy policy, SecurityMode mode) +void +BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode) { - if (!m_client) { - emit connectionError(QStringLiteral("OPC UA backend not available")); - return; + if (!m_client) + { + emit connectionError (QStringLiteral ("OPC UA backend not available")); + return; } - if (m_serverUrl.isEmpty()) { - emit connectionError(QStringLiteral("No server URL set")); - return; + if (m_serverUrl.isEmpty ()) + { + emit connectionError (QStringLiteral ("No server URL set")); + return; } - if (m_client->state() != QOpcUaClient::Disconnected) { - emit connectionError(QStringLiteral("Already connected or connecting")); - return; + if (m_client->state () != QOpcUaClient::Disconnected) + { + emit connectionError ( + QStringLiteral ("Already connected or connecting")); + return; } - QOpcUaEndpointDescription endpoint; - endpoint.setEndpointUrl(m_serverUrl); - endpoint.setSecurityPolicy(securityPolicyUri(policy)); - endpoint.setSecurityMode( - static_cast<QOpcUaEndpointDescription::MessageSecurityMode>(mode)); - - QOpcUaUserTokenPolicy tokenPolicy; - if (m_auth) { - switch (m_auth->mode()) { + QOpcUaEndpointDescription endpoint; + endpoint.setEndpointUrl (m_serverUrl); + endpoint.setSecurityPolicy (securityPolicyUri (policy)); + endpoint.setSecurityMode ( + static_cast<QOpcUaEndpointDescription::MessageSecurityMode> (mode)); + + QOpcUaUserTokenPolicy tokenPolicy; + if (m_auth) + { + switch (m_auth->mode ()) + { case BobinkAuth::Anonymous: - tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Anonymous); - break; + tokenPolicy.setTokenType ( + QOpcUaUserTokenPolicy::TokenType::Anonymous); + break; case BobinkAuth::UserPass: - tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Username); - break; + tokenPolicy.setTokenType ( + QOpcUaUserTokenPolicy::TokenType::Username); + break; case BobinkAuth::Certificate: - tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Certificate); - break; + tokenPolicy.setTokenType ( + QOpcUaUserTokenPolicy::TokenType::Certificate); + break; } - m_client->setAuthenticationInformation(m_auth->toAuthenticationInformation()); - } else { - tokenPolicy.setTokenType(QOpcUaUserTokenPolicy::TokenType::Anonymous); + m_client->setAuthenticationInformation ( + m_auth->toAuthenticationInformation ()); + } + else + { + tokenPolicy.setTokenType (QOpcUaUserTokenPolicy::TokenType::Anonymous); } - endpoint.setUserIdentityTokens({tokenPolicy}); + endpoint.setUserIdentityTokens ({ tokenPolicy }); - m_client->connectToEndpoint(endpoint); + m_client->connectToEndpoint (endpoint); } -void BobinkClient::disconnectFromServer() +void +BobinkClient::disconnectFromServer () { - if (m_client) - m_client->disconnectFromEndpoint(); + if (m_client) + m_client->disconnectFromEndpoint (); } -void BobinkClient::acceptCertificate() +void +BobinkClient::acceptCertificate () { - m_certAccepted = true; - if (m_certLoop) - m_certLoop->quit(); + m_certAccepted = true; + if (m_certLoop) + m_certLoop->quit (); } -void BobinkClient::rejectCertificate() +void +BobinkClient::rejectCertificate () { - m_certAccepted = false; - if (m_certLoop) - m_certLoop->quit(); + m_certAccepted = false; + if (m_certLoop) + m_certLoop->quit (); } /* ====================================== * Discovery properties * ====================================== */ -QString BobinkClient::discoveryUrl() const { return m_discoveryUrl; } - -void BobinkClient::setDiscoveryUrl(const QString &url) +QString +BobinkClient::discoveryUrl () const { - if (m_discoveryUrl == url) - return; - m_discoveryUrl = url; - emit discoveryUrlChanged(); + return m_discoveryUrl; } -int BobinkClient::discoveryInterval() const { return m_discoveryInterval; } - -void BobinkClient::setDiscoveryInterval(int ms) +void +BobinkClient::setDiscoveryUrl (const QString &url) { - if (m_discoveryInterval == ms) - return; - m_discoveryInterval = ms; - emit discoveryIntervalChanged(); - - if (m_discoveryTimer.isActive()) - m_discoveryTimer.setInterval(ms); + if (m_discoveryUrl == url) + return; + m_discoveryUrl = url; + emit discoveryUrlChanged (); } -bool BobinkClient::discovering() const { return m_discovering; } +bool +BobinkClient::discovering () const +{ + return m_discovering; +} -const QList<QOpcUaApplicationDescription> &BobinkClient::discoveredServers() const +const QList<QOpcUaApplicationDescription> & +BobinkClient::discoveredServers () const { - return m_discoveredServers; + return m_discoveredServers; } -QVariantList BobinkClient::servers() const +QVariantList +BobinkClient::servers () const { - return m_serversCache; + return m_serversCache; } /* ====================================== * PKI * ====================================== */ -QString BobinkClient::pkiDir() const { return m_pkiDir; } - -void BobinkClient::setPkiDir(const QString &path) +QString +BobinkClient::pkiDir () const { - if (m_pkiDir == path) - return; - m_pkiDir = path; - ensurePkiDirs(m_pkiDir); - emit pkiDirChanged(); + return m_pkiDir; } -QString BobinkClient::certFile() const { return m_certFile; } - -void BobinkClient::setCertFile(const QString &path) +void +BobinkClient::setPkiDir (const QString &path) { - if (m_certFile == path) - return; - m_certFile = path; - emit certFileChanged(); + if (m_pkiDir == path) + return; + m_pkiDir = path; + ensurePkiDirs (m_pkiDir); + emit pkiDirChanged (); } -QString BobinkClient::keyFile() const { return m_keyFile; } - -void BobinkClient::setKeyFile(const QString &path) +QString +BobinkClient::certFile () const { - if (m_keyFile == path) - return; - m_keyFile = path; - emit keyFileChanged(); + return m_certFile; } -void BobinkClient::autoDetectPki() +void +BobinkClient::setCertFile (const QString &path) { - if (m_pkiDir.isEmpty()) - return; - - QDir certDir(m_pkiDir + QStringLiteral("/own/certs")); - QStringList certs = certDir.entryList({QStringLiteral("*.der")}, QDir::Files); - if (!certs.isEmpty()) - setCertFile(certDir.filePath(certs.first())); + if (m_certFile == path) + return; + m_certFile = path; + emit certFileChanged (); +} - QDir keyDir(m_pkiDir + QStringLiteral("/own/private")); - QStringList keys = keyDir.entryList( - {QStringLiteral("*.pem"), QStringLiteral("*.crt")}, QDir::Files); - if (!keys.isEmpty()) - setKeyFile(keyDir.filePath(keys.first())); +QString +BobinkClient::keyFile () const +{ + return m_keyFile; } -void BobinkClient::applyPki() +void +BobinkClient::setKeyFile (const QString &path) { - if (!m_client || m_pkiDir.isEmpty()) - return; + if (m_keyFile == path) + return; + m_keyFile = path; + emit keyFileChanged (); +} - QOpcUaPkiConfiguration pki; - if (!m_certFile.isEmpty()) - pki.setClientCertificateFile(m_certFile); - if (!m_keyFile.isEmpty()) - pki.setPrivateKeyFile(m_keyFile); - pki.setTrustListDirectory(m_pkiDir + QStringLiteral("/trusted/certs")); - pki.setRevocationListDirectory(m_pkiDir + QStringLiteral("/trusted/crl")); - pki.setIssuerListDirectory(m_pkiDir + QStringLiteral("/issuers/certs")); - pki.setIssuerRevocationListDirectory(m_pkiDir + QStringLiteral("/issuers/crl")); +void +BobinkClient::autoDetectPki () +{ + if (m_pkiDir.isEmpty ()) + return; + + QDir certDir (m_pkiDir + QStringLiteral ("/own/certs")); + QStringList certs + = certDir.entryList ({ QStringLiteral ("*.der") }, QDir::Files); + if (!certs.isEmpty ()) + setCertFile (certDir.filePath (certs.first ())); + + QDir keyDir (m_pkiDir + QStringLiteral ("/own/private")); + QStringList keys = keyDir.entryList ( + { QStringLiteral ("*.pem"), QStringLiteral ("*.crt") }, QDir::Files); + if (!keys.isEmpty ()) + setKeyFile (keyDir.filePath (keys.first ())); +} - m_client->setPkiConfiguration(pki); +void +BobinkClient::applyPki () +{ + if (!m_client || m_pkiDir.isEmpty ()) + return; + + if (!m_certFile.isEmpty () && !QFile::exists (m_certFile)) + { + emit statusMessage ( + QStringLiteral ("PKI error: certificate not found: %1") + .arg (m_certFile)); + return; + } + if (!m_keyFile.isEmpty () && !QFile::exists (m_keyFile)) + { + emit statusMessage ( + QStringLiteral ("PKI error: private key not found: %1") + .arg (m_keyFile)); + return; + } + if (!m_certFile.isEmpty () && m_keyFile.isEmpty ()) + { + emit statusMessage ( + QStringLiteral ("PKI error: certificate set but no private key")); + return; + } + if (m_certFile.isEmpty () && !m_keyFile.isEmpty ()) + { + emit statusMessage ( + QStringLiteral ("PKI error: private key set but no certificate")); + return; + } - if (pki.isKeyAndCertificateFileSet()) - m_client->setApplicationIdentity(pki.applicationIdentity()); + QOpcUaPkiConfiguration pki; + if (!m_certFile.isEmpty ()) + pki.setClientCertificateFile (m_certFile); + if (!m_keyFile.isEmpty ()) + pki.setPrivateKeyFile (m_keyFile); + pki.setTrustListDirectory (m_pkiDir + QStringLiteral ("/trusted/certs")); + pki.setRevocationListDirectory (m_pkiDir + QStringLiteral ("/trusted/crl")); + pki.setIssuerListDirectory (m_pkiDir + QStringLiteral ("/issuers/certs")); + pki.setIssuerRevocationListDirectory (m_pkiDir + + QStringLiteral ("/issuers/crl")); + + m_client->setPkiConfiguration (pki); + + if (pki.isKeyAndCertificateFileSet ()) + { + auto identity = pki.applicationIdentity (); + if (!identity.isValid ()) + { + emit statusMessage (QStringLiteral ( + "PKI error: certificate could not be parsed (invalid DER?)")); + return; + } + m_client->setApplicationIdentity (identity); + emit statusMessage (QStringLiteral ("PKI applied: %1") + .arg (m_certFile.split ('/').last ())); + } + else + { + emit statusMessage ( + QStringLiteral ("PKI applied (no client certificate)")); + } } /* ====================================== * Discovery methods * ====================================== */ -void BobinkClient::startDiscovery() +void +BobinkClient::startDiscovery () { - if (m_discoveryUrl.isEmpty() || !m_client) - return; + if (m_discoveryUrl.isEmpty () || !m_client) + return; - doDiscovery(); - m_discoveryTimer.start(m_discoveryInterval); + doDiscovery (); + m_discoveryTimer.start (m_discoveryInterval); - if (!m_discovering) { - m_discovering = true; - emit discoveringChanged(); + if (!m_discovering) + { + m_discovering = true; + emit discoveringChanged (); } } -void BobinkClient::stopDiscovery() +void +BobinkClient::stopDiscovery () { - m_discoveryTimer.stop(); + m_discoveryTimer.stop (); - if (m_discovering) { - m_discovering = false; - emit discoveringChanged(); + if (m_discovering) + { + m_discovering = false; + emit discoveringChanged (); } } -void BobinkClient::doDiscovery() +void +BobinkClient::doDiscovery () { - if (!m_client || m_discoveryUrl.isEmpty()) - return; - QUrl url(m_discoveryUrl); - if (!url.isValid()) - return; - m_client->findServers(url); + if (!m_client || m_discoveryUrl.isEmpty ()) + return; + QUrl url (m_discoveryUrl); + if (!url.isValid ()) + return; + m_client->findServers (url); } /* ====================================== * Private slots * ====================================== */ -void BobinkClient::handleStateChanged(QOpcUaClient::ClientState state) +void +BobinkClient::handleStateChanged (QOpcUaClient::ClientState state) { - bool nowConnected = (state == QOpcUaClient::Connected); - if (m_connected != nowConnected) { - m_connected = nowConnected; - emit connectedChanged(); + bool nowConnected = (state == QOpcUaClient::Connected); + if (m_connected != nowConnected) + { + m_connected = nowConnected; + emit connectedChanged (); } } -void BobinkClient::handleEndpointsReceived( +void +BobinkClient::handleEndpointsReceived ( const QList<QOpcUaEndpointDescription> &endpoints, QOpcUa::UaStatusCode statusCode, const QUrl &) { - if (statusCode != QOpcUa::Good || endpoints.isEmpty()) { - emit connectionError(QStringLiteral("Failed to retrieve endpoints")); - return; + if (statusCode != QOpcUa::Good || endpoints.isEmpty ()) + { + emit connectionError (QStringLiteral ("Failed to retrieve endpoints")); + return; } - QOpcUaEndpointDescription best = endpoints.first(); - for (const auto &ep : endpoints) { - if (ep.securityLevel() > best.securityLevel()) - best = ep; + QOpcUaEndpointDescription best = endpoints.first (); + for (const auto &ep : endpoints) + { + if (ep.securityLevel () > best.securityLevel ()) + best = ep; } - if (m_auth) - m_client->setAuthenticationInformation(m_auth->toAuthenticationInformation()); - - m_client->connectToEndpoint(best); -} - -void BobinkClient::handleConnectError(QOpcUaErrorState *errorState) -{ - if (errorState->connectionStep() == - QOpcUaErrorState::ConnectionStep::CertificateValidation) { - // connectError uses BlockingQueuedConnection — the backend thread is - // blocked waiting for us to return. The errorState pointer is stack- - // allocated in the backend, so it is only valid during this call. - // Spin a local event loop so QML can show a dialog and call - // acceptCertificate() / rejectCertificate() while we stay in scope. - m_certAccepted = false; - emit certificateTrustRequested( - QStringLiteral("The server certificate is not trusted. Accept?")); - - QEventLoop loop; - m_certLoop = &loop; - QTimer::singleShot(30000, &loop, &QEventLoop::quit); - loop.exec(); - m_certLoop = nullptr; - - errorState->setIgnoreError(m_certAccepted); - } else { - emit connectionError( - QStringLiteral("Connection error at step %1, code 0x%2") - .arg(static_cast<int>(errorState->connectionStep())) - .arg(static_cast<uint>(errorState->errorCode()), 8, 16, QLatin1Char('0'))); + if (m_auth) + m_client->setAuthenticationInformation ( + m_auth->toAuthenticationInformation ()); + + m_client->connectToEndpoint (best); +} + +void +BobinkClient::handleConnectError (QOpcUaErrorState *errorState) +{ + if (errorState->connectionStep () + == QOpcUaErrorState::ConnectionStep::CertificateValidation) + { + // connectError uses BlockingQueuedConnection — the backend thread is + // blocked waiting for us to return. The errorState pointer is stack- + // allocated in the backend, so it is only valid during this call. + // Spin a local event loop so QML can show a dialog and call + // acceptCertificate() / rejectCertificate() while we stay in scope. + m_certAccepted = false; + emit certificateTrustRequested ( + QStringLiteral ("The server certificate is not trusted. Accept?")); + + QEventLoop loop; + m_certLoop = &loop; + QTimer::singleShot (30000, &loop, &QEventLoop::quit); + loop.exec (); + m_certLoop = nullptr; + + errorState->setIgnoreError (m_certAccepted); + } + else + { + emit connectionError ( + QStringLiteral ("Connection error at step %1, code 0x%2") + .arg (static_cast<int> (errorState->connectionStep ())) + .arg (static_cast<uint> (errorState->errorCode ()), 8, 16, + QLatin1Char ('0'))); } } -void BobinkClient::handleFindServersFinished( +void +BobinkClient::handleFindServersFinished ( const QList<QOpcUaApplicationDescription> &servers, QOpcUa::UaStatusCode statusCode, const QUrl &) { - if (statusCode != QOpcUa::Good) - return; - - m_discoveredServers = servers; - m_serversCache.clear(); - for (const auto &s : m_discoveredServers) { - QVariantMap entry; - entry[QStringLiteral("serverName")] = s.applicationName().text(); - entry[QStringLiteral("applicationUri")] = s.applicationUri(); - entry[QStringLiteral("discoveryUrls")] = QVariant::fromValue(s.discoveryUrls()); - m_serversCache.append(entry); + if (statusCode != QOpcUa::Good) + return; + + m_discoveredServers = servers; + m_serversCache.clear (); + for (const auto &s : m_discoveredServers) + { + QVariantMap entry; + entry[QStringLiteral ("serverName")] = s.applicationName ().text (); + entry[QStringLiteral ("applicationUri")] = s.applicationUri (); + entry[QStringLiteral ("discoveryUrls")] + = QVariant::fromValue (s.discoveryUrls ()); + m_serversCache.append (entry); } - emit serversChanged(); + emit serversChanged (); } diff --git a/src/BobinkClient.h b/src/BobinkClient.h index 43eda65..c86cfaa 100644 --- a/src/BobinkClient.h +++ b/src/BobinkClient.h @@ -35,8 +35,6 @@ class BobinkClient : public QObject Q_PROPERTY (QString discoveryUrl READ discoveryUrl WRITE setDiscoveryUrl NOTIFY discoveryUrlChanged) - Q_PROPERTY (int discoveryInterval READ discoveryInterval WRITE - setDiscoveryInterval NOTIFY discoveryIntervalChanged) Q_PROPERTY (bool discovering READ discovering NOTIFY discoveringChanged) Q_PROPERTY (QVariantList servers READ servers NOTIFY serversChanged) @@ -71,9 +69,6 @@ public: QString discoveryUrl () const; void setDiscoveryUrl (const QString &url); - int discoveryInterval () const; - void setDiscoveryInterval (int ms); - bool discovering () const; const QList<QOpcUaApplicationDescription> &discoveredServers () const; @@ -136,9 +131,9 @@ signals: */ void certificateTrustRequested (const QString &certInfo); void connectionError (const QString &message); + void statusMessage (const QString &message); void discoveryUrlChanged (); - void discoveryIntervalChanged (); void discoveringChanged (); void serversChanged (); void pkiDirChanged (); @@ -172,7 +167,7 @@ private: bool m_certAccepted = false; QString m_discoveryUrl; - int m_discoveryInterval = 10000; // ms + int m_discoveryInterval = 30000; // ms QTimer m_discoveryTimer; bool m_discovering = false; QList<QOpcUaApplicationDescription> m_discoveredServers; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f10c7a..615e90c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,16 @@ -# bobink — QML module wrapping QtOpcUa for declarative use. -qt_add_qml_module(bobink - URI Bobink - VERSION 1.0 - SOURCES - BobinkAuth.h BobinkAuth.cpp - BobinkClient.h BobinkClient.cpp - # BobinkServerDiscovery.h BobinkServerDiscovery.cpp - # BobinkNode.h BobinkNode.cpp - OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml/Bobink" -) +# BobinkQtOpcUa — QML module wrapping QtOpcUa for declarative use. +qt_add_qml_module( + BobinkQtOpcUa + URI + Bobink + VERSION + 1.0 + SOURCES + BobinkAuth.h + BobinkAuth.cpp + BobinkClient.h + BobinkClient.cpp + OUTPUT_DIRECTORY + "${CMAKE_BINARY_DIR}/qml/Bobink") -target_link_libraries(bobink PRIVATE Qt6::Core Qt6::Quick Qt6::OpcUa) +target_link_libraries(BobinkQtOpcUa PRIVATE Qt6::Core Qt6::Quick Qt6::OpcUa) |
