summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt23
-rw-r--r--demo/CMakeLists.txt30
-rw-r--r--demo/Main.qml243
-rw-r--r--src/BobinkClient.cpp685
-rw-r--r--src/BobinkClient.h9
-rw-r--r--src/CMakeLists.txt27
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)