diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 23:58:08 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 23:58:08 +0100 |
| commit | 343169dff6b062074fd3c4a5e240b449ffc4a449 (patch) | |
| tree | a2ef1edc8dd1b1c1dbac757192fa681d8ec76717 /src | |
| download | BobinkQtOpcUa-343169dff6b062074fd3c4a5e240b449ffc4a449.tar.gz BobinkQtOpcUa-343169dff6b062074fd3c4a5e240b449ffc4a449.zip | |
Initial Bobink library: BobinkAuth, BobinkClient, and demo app
Implements the core OPC UA wrapper library with:
- Build system with automatic dep building (open62541, QtOpcUa)
- BobinkAuth: QML auth component (anonymous/userpass/certificate)
- BobinkClient: QML singleton managing connection, LDS discovery,
PKI configuration, endpoint selection, and certificate trust flow
- Demo app for manual testing of the full connection flow
Diffstat (limited to 'src')
| -rw-r--r-- | src/BobinkAuth.cpp | 73 | ||||
| -rw-r--r-- | src/BobinkAuth.h | 60 | ||||
| -rw-r--r-- | src/BobinkClient.cpp | 315 | ||||
| -rw-r--r-- | src/BobinkClient.h | 123 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 12 |
5 files changed, 583 insertions, 0 deletions
diff --git a/src/BobinkAuth.cpp b/src/BobinkAuth.cpp new file mode 100644 index 0000000..fed1da2 --- /dev/null +++ b/src/BobinkAuth.cpp @@ -0,0 +1,73 @@ +#include "BobinkAuth.h" + +BobinkAuth::BobinkAuth(QObject *parent) + : QObject(parent) +{ +} + +BobinkAuth::AuthMode BobinkAuth::mode() const { return m_mode; } + +void BobinkAuth::setMode(AuthMode mode) +{ + if (m_mode == mode) + return; + m_mode = mode; + emit modeChanged(); +} + +QString BobinkAuth::username() const { return m_username; } + +void BobinkAuth::setUsername(const QString &username) +{ + if (m_username == username) + return; + m_username = username; + emit usernameChanged(); +} + +QString BobinkAuth::password() const { return m_password; } + +void BobinkAuth::setPassword(const QString &password) +{ + if (m_password == password) + return; + m_password = password; + emit passwordChanged(); +} + +QString BobinkAuth::certPath() const { return m_certPath; } + +void BobinkAuth::setCertPath(const QString &path) +{ + if (m_certPath == path) + return; + m_certPath = path; + emit certPathChanged(); +} + +QString BobinkAuth::keyPath() const { return m_keyPath; } + +void BobinkAuth::setKeyPath(const QString &path) +{ + if (m_keyPath == path) + return; + m_keyPath = path; + emit keyPathChanged(); +} + +QOpcUaAuthenticationInformation BobinkAuth::toAuthenticationInformation() const +{ + QOpcUaAuthenticationInformation info; + switch (m_mode) { + case Anonymous: + info.setAnonymousAuthentication(); + break; + case UserPass: + info.setUsernameAuthentication(m_username, m_password); + break; + case Certificate: + info.setCertificateAuthentication(m_certPath, m_keyPath); + break; + } + return info; +} diff --git a/src/BobinkAuth.h b/src/BobinkAuth.h new file mode 100644 index 0000000..2e3ea6a --- /dev/null +++ b/src/BobinkAuth.h @@ -0,0 +1,60 @@ +#ifndef BOBINKAUTH_H +#define BOBINKAUTH_H + +#include <QObject> +#include <QOpcUaAuthenticationInformation> +#include <QQmlEngine> + +class BobinkAuth : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(AuthMode mode READ mode WRITE setMode NOTIFY modeChanged) + Q_PROPERTY( + QString username READ username WRITE setUsername NOTIFY usernameChanged) + Q_PROPERTY( + QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY( + QString certPath READ certPath WRITE setCertPath NOTIFY certPathChanged) + Q_PROPERTY( + QString keyPath READ keyPath WRITE setKeyPath NOTIFY keyPathChanged) + +public: + enum AuthMode { Anonymous, UserPass, Certificate }; + Q_ENUM(AuthMode) + + explicit BobinkAuth(QObject *parent = nullptr); + + AuthMode mode() const; + void setMode(AuthMode mode); + + QString username() const; + void setUsername(const QString &username); + + QString password() const; + void setPassword(const QString &password); + + QString certPath() const; + void setCertPath(const QString &path); + + QString keyPath() const; + void setKeyPath(const QString &path); + + QOpcUaAuthenticationInformation toAuthenticationInformation() const; + +signals: + void modeChanged(); + void usernameChanged(); + void passwordChanged(); + void certPathChanged(); + void keyPathChanged(); + +private: + AuthMode m_mode = Anonymous; + QString m_username; + QString m_password; + QString m_certPath; + QString m_keyPath; +}; + +#endif // BOBINKAUTH_H diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp new file mode 100644 index 0000000..ea61583 --- /dev/null +++ b/src/BobinkClient.cpp @@ -0,0 +1,315 @@ +#include "BobinkClient.h" +#include "BobinkAuth.h" + +#include <QDir> +#include <QStandardPaths> + +BobinkClient *BobinkClient::s_instance = nullptr; + +static QString defaultPkiDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + QStringLiteral("/pki"); +} + +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)); + } +} + +BobinkClient::BobinkClient(QObject *parent) + : QObject(parent) + , m_provider(new QOpcUaProvider(this)) + , m_pkiDir(defaultPkiDir()) +{ + ensurePkiDirs(m_pkiDir); + setupClient(); + connect(&m_discoveryTimer, &QTimer::timeout, this, &BobinkClient::doDiscovery); +} + +BobinkClient::~BobinkClient() +{ + if (s_instance == this) + s_instance = nullptr; +} + +BobinkClient *BobinkClient::instance() +{ + return s_instance; +} + +BobinkClient *BobinkClient::create(QQmlEngine *, QJSEngine *) +{ + if (!s_instance) { + s_instance = new BobinkClient; + QJSEngine::setObjectOwnership(s_instance, QJSEngine::CppOwnership); + } + return s_instance; +} + +void BobinkClient::setupClient() +{ + 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); +} + +// --- Connection properties --- + +bool BobinkClient::connected() const { return m_connected; } + +QString BobinkClient::serverUrl() const { return m_serverUrl; } + +void BobinkClient::setServerUrl(const QString &url) +{ + if (m_serverUrl == url) + return; + m_serverUrl = url; + emit serverUrlChanged(); +} + +BobinkAuth *BobinkClient::auth() const { return m_auth; } + +void BobinkClient::setAuth(BobinkAuth *auth) +{ + if (m_auth == auth) + return; + m_auth = auth; + emit authChanged(); +} + +QOpcUaClient *BobinkClient::opcuaClient() const { return m_client; } + +// --- Connection methods --- + +void BobinkClient::connectToServer() +{ + 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_client->state() != QOpcUaClient::Disconnected) { + emit connectionError(QStringLiteral("Already connected or connecting")); + return; + } + + m_client->requestEndpoints(QUrl(m_serverUrl)); +} + +void BobinkClient::disconnectFromServer() +{ + if (m_client) + m_client->disconnectFromEndpoint(); +} + +void BobinkClient::acceptCertificate() +{ + m_certAccepted = true; + if (m_certLoop) + m_certLoop->quit(); +} + +void BobinkClient::rejectCertificate() +{ + m_certAccepted = false; + if (m_certLoop) + m_certLoop->quit(); +} + +// --- Discovery properties --- + +QString BobinkClient::discoveryUrl() const { return m_discoveryUrl; } + +void BobinkClient::setDiscoveryUrl(const QString &url) +{ + if (m_discoveryUrl == url) + return; + m_discoveryUrl = url; + emit discoveryUrlChanged(); +} + +int BobinkClient::discoveryInterval() const { return m_discoveryInterval; } + +void BobinkClient::setDiscoveryInterval(int ms) +{ + if (m_discoveryInterval == ms) + return; + m_discoveryInterval = ms; + emit discoveryIntervalChanged(); + + if (m_discoveryTimer.isActive()) + m_discoveryTimer.setInterval(ms); +} + +bool BobinkClient::discovering() const { return m_discovering; } + +const QList<QOpcUaApplicationDescription> &BobinkClient::discoveredServers() const +{ + return m_discoveredServers; +} + +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; +} + +// --- PKI --- + +QString BobinkClient::pkiDir() const { return m_pkiDir; } + +void BobinkClient::setPkiDir(const QString &path) +{ + if (m_pkiDir == path) + return; + m_pkiDir = path; + ensurePkiDirs(m_pkiDir); + emit pkiDirChanged(); +} + +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")); + 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()) + m_client->setApplicationIdentity(pki.applicationIdentity()); +} + +// --- Discovery methods --- + +void BobinkClient::startDiscovery() +{ + if (m_discoveryUrl.isEmpty() || !m_client) + return; + + doDiscovery(); + m_discoveryTimer.start(m_discoveryInterval); + + if (!m_discovering) { + m_discovering = true; + emit discoveringChanged(); + } +} + +void BobinkClient::stopDiscovery() +{ + m_discoveryTimer.stop(); + + if (m_discovering) { + m_discovering = false; + emit discoveringChanged(); + } +} + +void BobinkClient::doDiscovery() +{ + if (m_client && !m_discoveryUrl.isEmpty()) + m_client->findServers(QUrl(m_discoveryUrl)); +} + +// --- Private slots --- + +void BobinkClient::handleStateChanged(QOpcUaClient::ClientState state) +{ + bool nowConnected = (state == QOpcUaClient::Connected); + if (m_connected != nowConnected) { + m_connected = nowConnected; + emit connectedChanged(); + } +} + +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; + } + + 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; + 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( + const QList<QOpcUaApplicationDescription> &servers, + QOpcUa::UaStatusCode statusCode, const QUrl &) +{ + if (statusCode != QOpcUa::Good) + return; + + m_discoveredServers = servers; + emit serversChanged(); +} diff --git a/src/BobinkClient.h b/src/BobinkClient.h new file mode 100644 index 0000000..b8e5624 --- /dev/null +++ b/src/BobinkClient.h @@ -0,0 +1,123 @@ +#ifndef BOBINKCLIENT_H +#define BOBINKCLIENT_H + +#include <QEventLoop> +#include <QObject> +#include <QOpcUaApplicationDescription> +#include <QOpcUaClient> +#include <QOpcUaEndpointDescription> +#include <QOpcUaErrorState> +#include <QOpcUaPkiConfiguration> +#include <QOpcUaProvider> +#include <QQmlEngine> +#include <QTimer> + +class BobinkAuth; + +class BobinkClient : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY (bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY (QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY + serverUrlChanged) + Q_PROPERTY (BobinkAuth *auth READ auth WRITE setAuth NOTIFY authChanged) + + 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) + + Q_PROPERTY (QString pkiDir READ pkiDir WRITE setPkiDir NOTIFY pkiDirChanged) + +public: + explicit BobinkClient (QObject *parent = nullptr); + ~BobinkClient () override; + + static BobinkClient *instance (); + static BobinkClient *create (QQmlEngine *qmlEngine, QJSEngine *jsEngine); + + bool connected () const; + + QString serverUrl () const; + void setServerUrl (const QString &url); + + BobinkAuth *auth () const; + void setAuth (BobinkAuth *auth); + + QOpcUaClient *opcuaClient () const; + + QString discoveryUrl () const; + void setDiscoveryUrl (const QString &url); + + int discoveryInterval () const; + void setDiscoveryInterval (int ms); + + bool discovering () const; + + const QList<QOpcUaApplicationDescription> &discoveredServers () const; + QVariantList servers () const; + + QString pkiDir () const; + void setPkiDir (const QString &path); + + Q_INVOKABLE void connectToServer (); + Q_INVOKABLE void disconnectFromServer (); + Q_INVOKABLE void acceptCertificate (); + Q_INVOKABLE void rejectCertificate (); + + Q_INVOKABLE void startDiscovery (); + Q_INVOKABLE void stopDiscovery (); + Q_INVOKABLE void applyPki (); + +signals: + void connectedChanged (); + void serverUrlChanged (); + void authChanged (); + void certificateTrustRequested (const QString &certInfo); + void connectionError (const QString &message); + + void discoveryUrlChanged (); + void discoveryIntervalChanged (); + void discoveringChanged (); + void serversChanged (); + void pkiDirChanged (); + +private slots: + void handleStateChanged (QOpcUaClient::ClientState state); + void + handleEndpointsReceived (const QList<QOpcUaEndpointDescription> &endpoints, + QOpcUa::UaStatusCode statusCode, + const QUrl &requestUrl); + void handleConnectError (QOpcUaErrorState *errorState); + void handleFindServersFinished ( + const QList<QOpcUaApplicationDescription> &servers, + QOpcUa::UaStatusCode statusCode, const QUrl &requestUrl); + void doDiscovery (); + +private: + void setupClient (); + + static BobinkClient *s_instance; + QOpcUaProvider *m_provider = nullptr; + QOpcUaClient *m_client = nullptr; + BobinkAuth *m_auth = nullptr; + QString m_serverUrl; + bool m_connected = false; + QEventLoop *m_certLoop = nullptr; + bool m_certAccepted = false; + + QString m_discoveryUrl; + int m_discoveryInterval = 10000; + QTimer m_discoveryTimer; + bool m_discovering = false; + QList<QOpcUaApplicationDescription> m_discoveredServers; + + QString m_pkiDir; +}; + +#endif // BOBINKCLIENT_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..9b7cac2 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,12 @@ +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" +) + +target_link_libraries(bobink PRIVATE Qt6::Core Qt6::Quick Qt6::OpcUa) |
