diff options
| -rw-r--r-- | cmake/BuildDeps.cmake | 20 | ||||
| -rw-r--r-- | demo/Main.qml | 87 | ||||
| -rw-r--r-- | demo/main.cpp | 5 | ||||
| -rw-r--r-- | src/BobinkAuth.cpp | 4 | ||||
| -rw-r--r-- | src/BobinkAuth.h | 65 | ||||
| -rw-r--r-- | src/BobinkClient.cpp | 89 | ||||
| -rw-r--r-- | src/BobinkClient.h | 45 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 1 |
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 |
