summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-18 00:18:33 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-18 00:18:33 +0100
commit1a79ab468d8cc23cfdf28ddfa85d3e03ffddf44c (patch)
treee19d73f0aa00958dc65d19cb2dfc607f2469089b
parent343169dff6b062074fd3c4a5e240b449ffc4a449 (diff)
downloadBobinkQtOpcUa-1a79ab468d8cc23cfdf28ddfa85d3e03ffddf44c.tar.gz
BobinkQtOpcUa-1a79ab468d8cc23cfdf28ddfa85d3e03ffddf44c.zip
Refactor and document: fix cert filenames, add Doxygen, improve demo
Refactoring (issues 1,3,4,5,7,8,9,10 from review): - Replace hardcoded cert filenames with certFile/keyFile properties - Add 30s timeout to certificate trust QEventLoop - Cache servers() QVariantList instead of rebuilding per call - Validate URLs before passing to QtOpcUa - Use ComboBox valueRole for robust enum mapping - Add certificate trust dialog to demo - Remove unnecessary RowLayout wrapper - Remove debug output from BuildDeps.cmake Documentation: - Add Doxygen file blocks to all C++ files - Document AuthMode enum, toAuthenticationInformation(), key Q_INVOKABLE methods, certificateTrustRequested signal contract - Convert section banners to standard format - Add file/target comments to CMake and QML files
-rw-r--r--cmake/BuildDeps.cmake20
-rw-r--r--demo/Main.qml87
-rw-r--r--demo/main.cpp5
-rw-r--r--src/BobinkAuth.cpp4
-rw-r--r--src/BobinkAuth.h65
-rw-r--r--src/BobinkClient.cpp89
-rw-r--r--src/BobinkClient.h45
-rw-r--r--src/CMakeLists.txt1
8 files changed, 241 insertions, 75 deletions
diff --git a/cmake/BuildDeps.cmake b/cmake/BuildDeps.cmake
index 8a1fa89..d105419 100644
--- a/cmake/BuildDeps.cmake
+++ b/cmake/BuildDeps.cmake
@@ -1,3 +1,13 @@
+# ======================================
+# BuildDeps.cmake
+#
+# Configure, build, and install open62541 and QtOpcUa from
+# git submodules under deps/.
+#
+# Skip detection: if the built .so already exists, the
+# corresponding dep is skipped. Delete the file to rebuild.
+# ======================================
+
set(OPEN62541_SOURCE_DIR "${CMAKE_SOURCE_DIR}/deps/open62541")
set(OPEN62541_BUILD_DIR "${CMAKE_BINARY_DIR}/deps/open62541-build")
set(OPEN62541_INSTALL_DIR "${CMAKE_BINARY_DIR}/deps/open62541-install")
@@ -18,7 +28,9 @@ if(NOT EXISTS "${QTOPCUA_SOURCE_DIR}/CMakeLists.txt")
)
endif()
-# --- open62541 ---
+# ======================================
+# open62541
+# ======================================
if(NOT EXISTS "${OPEN62541_INSTALL_DIR}/lib/libopen62541.so")
message(STATUS "Configuring open62541 in ${OPEN62541_BUILD_DIR}...")
@@ -60,9 +72,10 @@ else()
message(STATUS "open62541 already built, skipping")
endif()
-# --- qtopcua ---
+# ======================================
+# QtOpcUa
+# ======================================
-message(STATUS "CMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH}")
find_program(QT_CMAKE_COMMAND bin/qt-cmake REQUIRED)
if(NOT EXISTS "${QTOPCUA_BUILD_DIR}/lib/libQt6OpcUa.so")
@@ -76,6 +89,7 @@ if(NOT EXISTS "${QTOPCUA_BUILD_DIR}/lib/libQt6OpcUa.so")
"${QTOPCUA_BUILD_DIR}"
-G
Ninja
+ # Use our locally-built open62541, not the bundled copy
-DINPUT_open62541=system
-DCMAKE_PREFIX_PATH=${OPEN62541_INSTALL_DIR})
execute_process(COMMAND ${_cmd} RESULT_VARIABLE _result)
diff --git a/demo/Main.qml b/demo/Main.qml
index 6d1327d..5609c12 100644
--- a/demo/Main.qml
+++ b/demo/Main.qml
@@ -1,3 +1,5 @@
+// Main.qml — Demo app for testing BobinkClient connection flow.
+
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -23,6 +25,22 @@ ApplicationWindow {
function onDiscoveringChanged() {
console.log("Discovering:", BobinkClient.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: BobinkClient.acceptCertificate()
+ onRejected: BobinkClient.rejectCertificate()
}
ColumnLayout {
@@ -74,47 +92,58 @@ ApplicationWindow {
ScrollBar.vertical: ScrollBar {}
}
- Label { text: "PKI Directory"; font.bold: true }
+ Label { text: "PKI"; font.bold: true }
- RowLayout {
+ GridLayout {
+ columns: 2
+ Layout.fillWidth: true
+
+ Label { text: "PKI directory:" }
TextField {
id: pkiDirField
Layout.fillWidth: true
text: BobinkClient.pkiDir
onEditingFinished: BobinkClient.pkiDir = text
}
- Button {
- text: "Apply PKI"
- onClicked: {
- BobinkClient.pkiDir = pkiDirField.text
- BobinkClient.applyPki()
- console.log("PKI applied:", BobinkClient.pkiDir)
- }
+ Label { text: "Certificate:" }
+ TextField {
+ id: certFileField
+ Layout.fillWidth: true
+ placeholderText: "/path/to/cert.der"
+ }
+ Label { text: "Private key:" }
+ TextField {
+ id: keyFileField
+ Layout.fillWidth: true
+ placeholderText: "/path/to/key.pem"
+ }
+ }
+
+ Button {
+ text: "Apply PKI"
+ Layout.fillWidth: true
+ onClicked: {
+ BobinkClient.pkiDir = pkiDirField.text
+ BobinkClient.certFile = certFileField.text
+ BobinkClient.keyFile = keyFileField.text
+ BobinkClient.applyPki()
+ console.log("PKI applied:", BobinkClient.pkiDir)
}
}
Label { text: "Server URL"; font.bold: true }
- RowLayout {
- TextField {
- id: serverUrlField
- Layout.fillWidth: true
- placeholderText: "opc.tcp://..."
- }
+ TextField {
+ id: serverUrlField
+ Layout.fillWidth: true
+ placeholderText: "opc.tcp://..."
}
Label { text: "Authentication"; font.bold: true }
BobinkAuth {
id: auth
- mode: {
- switch (authModeCombo.currentIndex) {
- case 0: return BobinkAuth.Anonymous
- case 1: return BobinkAuth.UserPass
- case 2: return BobinkAuth.Certificate
- default: return BobinkAuth.Anonymous
- }
- }
+ mode: authModeCombo.currentValue
username: usernameField.text
password: passwordField.text
certPath: certPathField.text
@@ -124,12 +153,18 @@ ApplicationWindow {
ComboBox {
id: authModeCombo
Layout.fillWidth: true
- model: ["Anonymous", "Username / Password", "Certificate"]
+ textRole: "text"
+ valueRole: "mode"
+ model: [
+ { text: "Anonymous", mode: BobinkAuth.Anonymous },
+ { text: "Username / Password", mode: BobinkAuth.UserPass },
+ { text: "Certificate", mode: BobinkAuth.Certificate }
+ ]
}
GridLayout {
columns: 2
- visible: authModeCombo.currentIndex === 1
+ visible: authModeCombo.currentValue === BobinkAuth.UserPass
Layout.fillWidth: true
Label { text: "Username:" }
@@ -147,7 +182,7 @@ ApplicationWindow {
GridLayout {
columns: 2
- visible: authModeCombo.currentIndex === 2
+ visible: authModeCombo.currentValue === BobinkAuth.Certificate
Layout.fillWidth: true
Label { text: "Certificate:" }
diff --git a/demo/main.cpp b/demo/main.cpp
index de3c2fc..b52df9e 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -1,8 +1,13 @@
+/**
+ * @file main.cpp
+ * @brief Entry point for the Bobink demo application.
+ */
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
+ // Load the locally-built OpcUa backend plugin (open62541).
QCoreApplication::addLibraryPath(QStringLiteral(QTOPCUA_PLUGIN_PATH));
QGuiApplication app(argc, argv);
diff --git a/src/BobinkAuth.cpp b/src/BobinkAuth.cpp
index fed1da2..03ae2cf 100644
--- a/src/BobinkAuth.cpp
+++ b/src/BobinkAuth.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file BobinkAuth.cpp
+ * @brief BobinkAuth implementation.
+ */
#include "BobinkAuth.h"
BobinkAuth::BobinkAuth(QObject *parent)
diff --git a/src/BobinkAuth.h b/src/BobinkAuth.h
index 2e3ea6a..2bd3c05 100644
--- a/src/BobinkAuth.h
+++ b/src/BobinkAuth.h
@@ -1,3 +1,7 @@
+/**
+ * @file BobinkAuth.h
+ * @brief QML component for OPC UA authentication configuration.
+ */
#ifndef BOBINKAUTH_H
#define BOBINKAUTH_H
@@ -5,49 +9,60 @@
#include <QOpcUaAuthenticationInformation>
#include <QQmlEngine>
-class BobinkAuth : public QObject {
+class BobinkAuth : public QObject
+{
Q_OBJECT
QML_ELEMENT
- Q_PROPERTY(AuthMode mode READ mode WRITE setMode NOTIFY modeChanged)
- Q_PROPERTY(
+ Q_PROPERTY (AuthMode mode READ mode WRITE setMode NOTIFY modeChanged)
+ Q_PROPERTY (
QString username READ username WRITE setUsername NOTIFY usernameChanged)
- Q_PROPERTY(
+ Q_PROPERTY (
QString password READ password WRITE setPassword NOTIFY passwordChanged)
- Q_PROPERTY(
+ Q_PROPERTY (
QString certPath READ certPath WRITE setCertPath NOTIFY certPathChanged)
- Q_PROPERTY(
+ Q_PROPERTY (
QString keyPath READ keyPath WRITE setKeyPath NOTIFY keyPathChanged)
public:
- enum AuthMode { Anonymous, UserPass, Certificate };
- Q_ENUM(AuthMode)
+ /// Authentication modes supported by OPC UA.
+ enum AuthMode
+ {
+ Anonymous,
+ UserPass,
+ Certificate
+ };
+ Q_ENUM (AuthMode)
- explicit BobinkAuth(QObject *parent = nullptr);
+ explicit BobinkAuth (QObject *parent = nullptr);
- AuthMode mode() const;
- void setMode(AuthMode mode);
+ AuthMode mode () const;
+ void setMode (AuthMode mode);
- QString username() const;
- void setUsername(const QString &username);
+ QString username () const;
+ void setUsername (const QString &username);
- QString password() const;
- void setPassword(const QString &password);
+ QString password () const;
+ void setPassword (const QString &password);
- QString certPath() const;
- void setCertPath(const QString &path);
+ QString certPath () const;
+ void setCertPath (const QString &path);
- QString keyPath() const;
- void setKeyPath(const QString &path);
+ QString keyPath () const;
+ void setKeyPath (const QString &path);
- QOpcUaAuthenticationInformation toAuthenticationInformation() const;
+ /**
+ * @brief Build a QOpcUaAuthenticationInformation from the
+ * current mode and credentials.
+ */
+ QOpcUaAuthenticationInformation toAuthenticationInformation () const;
signals:
- void modeChanged();
- void usernameChanged();
- void passwordChanged();
- void certPathChanged();
- void keyPathChanged();
+ void modeChanged ();
+ void usernameChanged ();
+ void passwordChanged ();
+ void certPathChanged ();
+ void keyPathChanged ();
private:
AuthMode m_mode = Anonymous;
diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp
index ea61583..a067489 100644
--- a/src/BobinkClient.cpp
+++ b/src/BobinkClient.cpp
@@ -1,3 +1,7 @@
+/**
+ * @file BobinkClient.cpp
+ * @brief BobinkClient implementation.
+ */
#include "BobinkClient.h"
#include "BobinkAuth.h"
@@ -12,6 +16,7 @@ static QString defaultPkiDir()
+ QStringLiteral("/pki");
}
+/** @brief Create the standard OPC UA PKI directory tree. */
static void ensurePkiDirs(const QString &base)
{
for (const auto *sub : {"own/certs", "own/private",
@@ -69,7 +74,9 @@ void BobinkClient::setupClient()
this, &BobinkClient::handleFindServersFinished);
}
-// --- Connection properties ---
+/* ======================================
+ * Connection properties
+ * ====================================== */
bool BobinkClient::connected() const { return m_connected; }
@@ -95,7 +102,9 @@ void BobinkClient::setAuth(BobinkAuth *auth)
QOpcUaClient *BobinkClient::opcuaClient() const { return m_client; }
-// --- Connection methods ---
+/* ======================================
+ * Connection methods
+ * ====================================== */
void BobinkClient::connectToServer()
{
@@ -112,7 +121,12 @@ void BobinkClient::connectToServer()
return;
}
- m_client->requestEndpoints(QUrl(m_serverUrl));
+ QUrl url(m_serverUrl);
+ if (!url.isValid()) {
+ emit connectionError(QStringLiteral("Invalid server URL: %1").arg(m_serverUrl));
+ return;
+ }
+ m_client->requestEndpoints(url);
}
void BobinkClient::disconnectFromServer()
@@ -135,7 +149,9 @@ void BobinkClient::rejectCertificate()
m_certLoop->quit();
}
-// --- Discovery properties ---
+/* ======================================
+ * Discovery properties
+ * ====================================== */
QString BobinkClient::discoveryUrl() const { return m_discoveryUrl; }
@@ -169,18 +185,12 @@ const QList<QOpcUaApplicationDescription> &BobinkClient::discoveredServers() con
QVariantList BobinkClient::servers() const
{
- QVariantList list;
- 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());
- list.append(entry);
- }
- return list;
+ return m_serversCache;
}
-// --- PKI ---
+/* ======================================
+ * PKI
+ * ====================================== */
QString BobinkClient::pkiDir() const { return m_pkiDir; }
@@ -193,14 +203,36 @@ void BobinkClient::setPkiDir(const QString &path)
emit pkiDirChanged();
}
+QString BobinkClient::certFile() const { return m_certFile; }
+
+void BobinkClient::setCertFile(const QString &path)
+{
+ if (m_certFile == path)
+ return;
+ m_certFile = path;
+ emit certFileChanged();
+}
+
+QString BobinkClient::keyFile() const { return m_keyFile; }
+
+void BobinkClient::setKeyFile(const QString &path)
+{
+ if (m_keyFile == path)
+ return;
+ m_keyFile = path;
+ emit keyFileChanged();
+}
+
void BobinkClient::applyPki()
{
if (!m_client || m_pkiDir.isEmpty())
return;
QOpcUaPkiConfiguration pki;
- pki.setClientCertificateFile(m_pkiDir + QStringLiteral("/own/certs/BobinkDemo_cert.der"));
- pki.setPrivateKeyFile(m_pkiDir + QStringLiteral("/own/private/BobinkDemo_key.pem"));
+ 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"));
@@ -212,7 +244,9 @@ void BobinkClient::applyPki()
m_client->setApplicationIdentity(pki.applicationIdentity());
}
-// --- Discovery methods ---
+/* ======================================
+ * Discovery methods
+ * ====================================== */
void BobinkClient::startDiscovery()
{
@@ -240,11 +274,17 @@ void BobinkClient::stopDiscovery()
void BobinkClient::doDiscovery()
{
- if (m_client && !m_discoveryUrl.isEmpty())
- m_client->findServers(QUrl(m_discoveryUrl));
+ if (!m_client || m_discoveryUrl.isEmpty())
+ return;
+ QUrl url(m_discoveryUrl);
+ if (!url.isValid())
+ return;
+ m_client->findServers(url);
}
-// --- Private slots ---
+/* ======================================
+ * Private slots
+ * ====================================== */
void BobinkClient::handleStateChanged(QOpcUaClient::ClientState state)
{
@@ -291,6 +331,7 @@ void BobinkClient::handleConnectError(QOpcUaErrorState *errorState)
QEventLoop loop;
m_certLoop = &loop;
+ QTimer::singleShot(30000, &loop, &QEventLoop::quit);
loop.exec();
m_certLoop = nullptr;
@@ -311,5 +352,13 @@ void BobinkClient::handleFindServersFinished(
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();
}
diff --git a/src/BobinkClient.h b/src/BobinkClient.h
index b8e5624..f95ab02 100644
--- a/src/BobinkClient.h
+++ b/src/BobinkClient.h
@@ -1,3 +1,11 @@
+/**
+ * @file BobinkClient.h
+ * @brief QML singleton managing the OPC UA connection lifecycle.
+ *
+ * Wraps QOpcUaClient into a declarative interface: LDS discovery,
+ * endpoint selection, PKI, and certificate trust flow.
+ * Single connection at a time (app-wide singleton).
+ */
#ifndef BOBINKCLIENT_H
#define BOBINKCLIENT_H
@@ -33,12 +41,21 @@ class BobinkClient : public QObject
Q_PROPERTY (QVariantList servers READ servers NOTIFY serversChanged)
Q_PROPERTY (QString pkiDir READ pkiDir WRITE setPkiDir NOTIFY pkiDirChanged)
+ Q_PROPERTY (
+ QString certFile READ certFile WRITE setCertFile NOTIFY certFileChanged)
+ Q_PROPERTY (
+ QString keyFile READ keyFile WRITE setKeyFile NOTIFY keyFileChanged)
public:
explicit BobinkClient (QObject *parent = nullptr);
~BobinkClient () override;
static BobinkClient *instance ();
+
+ /**
+ * @brief QML singleton factory.
+ * @note CppOwnership — lives for the process lifetime.
+ */
static BobinkClient *create (QQmlEngine *qmlEngine, QJSEngine *jsEngine);
bool connected () const;
@@ -65,19 +82,38 @@ public:
QString pkiDir () const;
void setPkiDir (const QString &path);
+ QString certFile () const;
+ void setCertFile (const QString &path);
+
+ QString keyFile () const;
+ void setKeyFile (const QString &path);
+
+ /** @brief Discover endpoints, pick the most secure, connect. */
Q_INVOKABLE void connectToServer ();
Q_INVOKABLE void disconnectFromServer ();
+
+ /** @brief Accept the pending server certificate. */
Q_INVOKABLE void acceptCertificate ();
+ /** @brief Reject the pending server certificate. */
Q_INVOKABLE void rejectCertificate ();
Q_INVOKABLE void startDiscovery ();
Q_INVOKABLE void stopDiscovery ();
+
+ /** @brief Apply PKI dirs and cert/key. Call before connecting. */
Q_INVOKABLE void applyPki ();
signals:
void connectedChanged ();
void serverUrlChanged ();
void authChanged ();
+
+ /**
+ * @brief Emitted when the server presents an untrusted cert.
+ *
+ * The connection blocks until acceptCertificate() or
+ * rejectCertificate() is called (30 s timeout, auto-rejects).
+ */
void certificateTrustRequested (const QString &certInfo);
void connectionError (const QString &message);
@@ -86,6 +122,8 @@ signals:
void discoveringChanged ();
void serversChanged ();
void pkiDirChanged ();
+ void certFileChanged ();
+ void keyFileChanged ();
private slots:
void handleStateChanged (QOpcUaClient::ClientState state);
@@ -108,16 +146,21 @@ private:
BobinkAuth *m_auth = nullptr;
QString m_serverUrl;
bool m_connected = false;
+
+ // Certificate trust event loop — see handleConnectError().
QEventLoop *m_certLoop = nullptr;
bool m_certAccepted = false;
QString m_discoveryUrl;
- int m_discoveryInterval = 10000;
+ int m_discoveryInterval = 10000; // ms
QTimer m_discoveryTimer;
bool m_discovering = false;
QList<QOpcUaApplicationDescription> m_discoveredServers;
QString m_pkiDir;
+ QString m_certFile;
+ QString m_keyFile;
+ QVariantList m_serversCache;
};
#endif // BOBINKCLIENT_H
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 9b7cac2..2f10c7a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,3 +1,4 @@
+# bobink — QML module wrapping QtOpcUa for declarative use.
qt_add_qml_module(bobink
URI Bobink
VERSION 1.0