From 364430e417600c68133673254b58b4d35608777a Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Thu, 19 Feb 2026 22:31:16 +0100 Subject: Reorganize BobinkClient by domain and make s_instance private Group header and source by domain (Connection, Discovery, PKI) so properties, methods, and signal handlers live together. Move enums before constructor, move s_instance to private, and add a public instance() accessor used by BobinkNode. --- demo/Main.qml | 2 +- src/BobinkClient.cpp | 325 +++++++++++++++++++++++++-------------------------- src/BobinkClient.h | 98 +++++++++------- src/BobinkNode.cpp | 34 +++--- 4 files changed, 233 insertions(+), 226 deletions(-) diff --git a/demo/Main.qml b/demo/Main.qml index ef09b7d..dae415f 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -37,7 +37,7 @@ ApplicationWindow { } } function onConnectionError(message) { - debugConsole.appendLog("ERROR: " + message); + debugConsole.appendLog("Connection error: " + message); root.autoConnectFailed = true; } function onStatusMessage(message) { diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp index 01f9912..6d1c608 100644 --- a/src/BobinkClient.cpp +++ b/src/BobinkClient.cpp @@ -30,12 +30,33 @@ ensurePkiDirs (const QString &base) } } +static QString +securityPolicyUri (BobinkClient::SecurityPolicy policy) +{ + switch (policy) + { + case BobinkClient::Basic256Sha256: + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); + case BobinkClient::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 {}; +} + +/* ====================================== + * Construction + * ====================================== */ + BobinkClient::BobinkClient (QObject *parent) : QObject (parent), m_provider (new QOpcUaProvider (this)), m_pkiDir (defaultPkiDir ()) { - // Singleton pattern: construct only once - Q_ASSERT(!s_instance); + Q_ASSERT (!s_instance); ensurePkiDirs (m_pkiDir); setupClient (); autoDetectPki (); @@ -87,9 +108,15 @@ BobinkClient::setupClient () } /* ====================================== - * Connection properties + * Connection * ====================================== */ +BobinkClient * +BobinkClient::instance () +{ + return s_instance; +} + bool BobinkClient::connected () const { @@ -132,10 +159,6 @@ BobinkClient::opcuaClient () const return m_client; } -/* ====================================== - * Connection methods - * ====================================== */ - void BobinkClient::connectToServer () { @@ -166,24 +189,6 @@ BobinkClient::connectToServer () m_client->requestEndpoints (url); } -static QString -securityPolicyUri (BobinkClient::SecurityPolicy policy) -{ - switch (policy) - { - case BobinkClient::Basic256Sha256: - return QStringLiteral ( - "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); - case BobinkClient::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 {}; -} - void BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode) { @@ -263,8 +268,77 @@ BobinkClient::rejectCertificate () m_certLoop->quit (); } +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 &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; + 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 (errorState->connectionStep ())) + .arg (static_cast (errorState->errorCode ()), 8, 16, + QLatin1Char ('0'))); + } +} + /* ====================================== - * Discovery properties + * Discovery * ====================================== */ QString @@ -300,6 +374,67 @@ BobinkClient::servers () const return m_serversCache; } +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 ()) + return; + QUrl url (m_discoveryUrl); + if (!url.isValid ()) + return; + m_client->findServers (url); +} + +void +BobinkClient::handleFindServersFinished ( + const QList &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); + } + emit serversChanged (); +} + /* ====================================== * PKI * ====================================== */ @@ -434,141 +569,3 @@ BobinkClient::applyPki () QStringLiteral ("PKI applied (no client certificate)")); } } - -/* ====================================== - * 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 ()) - return; - QUrl url (m_discoveryUrl); - if (!url.isValid ()) - return; - m_client->findServers (url); -} - -/* ====================================== - * 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 &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; - 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 (errorState->connectionStep ())) - .arg (static_cast (errorState->errorCode ()), 8, 16, - QLatin1Char ('0'))); - } -} - -void -BobinkClient::handleFindServersFinished ( - const QList &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); - } - emit serversChanged (); -} diff --git a/src/BobinkClient.h b/src/BobinkClient.h index 3f36e75..2b41e05 100644 --- a/src/BobinkClient.h +++ b/src/BobinkClient.h @@ -28,16 +28,19 @@ class BobinkClient : public QObject QML_SINGLETON QML_NAMED_ELEMENT (Bobink) + /* -- Connection -- */ 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) + /* -- Discovery -- */ Q_PROPERTY (QString discoveryUrl READ discoveryUrl WRITE setDiscoveryUrl NOTIFY discoveryUrlChanged) Q_PROPERTY (bool discovering READ discovering NOTIFY discoveringChanged) Q_PROPERTY (QVariantList servers READ servers NOTIFY serversChanged) + /* -- PKI -- */ Q_PROPERTY (QString pkiDir READ pkiDir WRITE setPkiDir NOTIFY pkiDirChanged) Q_PROPERTY ( QString certFile READ certFile WRITE setCertFile NOTIFY certFileChanged) @@ -45,46 +48,6 @@ class BobinkClient : public QObject QString keyFile READ keyFile WRITE setKeyFile NOTIFY keyFileChanged) public: - BobinkClient (QObject *parent = nullptr); - // ~BobinkClient () override; - - static inline BobinkClient *s_instance = nullptr; - - // static BobinkClient *instance (); - - /** - * @brief QML singleton factory. - * @note CppOwnership — lives for the process lifetime. - */ - // 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); - - bool discovering () const; - - const QList &discoveredServers () const; - QVariantList servers () const; - - QString pkiDir () const; - void setPkiDir (const QString &path); - - QString certFile () const; - void setCertFile (const QString &path); - - QString keyFile () const; - void setKeyFile (const QString &path); - enum SecurityMode { SignAndEncrypt = 3, @@ -101,6 +64,18 @@ public: }; Q_ENUM (SecurityPolicy) + BobinkClient (QObject *parent = nullptr); + + /* -- Connection -- */ + + bool connected () const; + + QString serverUrl () const; + void setServerUrl (const QString &url); + + BobinkAuth *auth () const; + void setAuth (BobinkAuth *auth); + /** @brief Discover endpoints, pick the most secure, connect. */ Q_INVOKABLE void connectToServer (); /** @brief Connect directly without endpoint discovery. */ @@ -112,19 +87,45 @@ public: /** @brief Reject the pending server certificate. */ Q_INVOKABLE void rejectCertificate (); + /* -- Discovery -- */ + + QString discoveryUrl () const; + void setDiscoveryUrl (const QString &url); + + bool discovering () const; + + const QList &discoveredServers () const; + QVariantList servers () const; + Q_INVOKABLE void startDiscovery (); Q_INVOKABLE void stopDiscovery (); + /* -- PKI -- */ + + 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 Auto-detect cert/key from the PKI directory and apply. */ Q_INVOKABLE void autoDetectPki (); /** @brief Apply PKI dirs and cert/key. Call before connecting. */ Q_INVOKABLE void applyPki (); + /* -- C++ only -- */ + + static BobinkClient *instance (); + QOpcUaClient *opcuaClient () const; + signals: + /* -- Connection -- */ void connectedChanged (); void serverUrlChanged (); void authChanged (); - /** * @brief Emitted when the server presents an untrusted cert. * @@ -135,20 +136,26 @@ signals: void connectionError (const QString &message); void statusMessage (const QString &message); + /* -- Discovery -- */ void discoveryUrlChanged (); void discoveringChanged (); void serversChanged (); + + /* -- PKI -- */ void pkiDirChanged (); void certFileChanged (); void keyFileChanged (); private slots: + /* -- Connection -- */ void handleStateChanged (QOpcUaClient::ClientState state); void handleEndpointsReceived (const QList &endpoints, QOpcUa::UaStatusCode statusCode, const QUrl &requestUrl); void handleConnectError (QOpcUaErrorState *errorState); + + /* -- Discovery -- */ void handleFindServersFinished ( const QList &servers, QOpcUa::UaStatusCode statusCode, const QUrl &requestUrl); @@ -157,26 +164,29 @@ private slots: private: void setupClient (); + /* -- Connection -- */ QOpcUaProvider *m_provider = nullptr; QOpcUaClient *m_client = nullptr; 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; + /* -- Discovery -- */ QString m_discoveryUrl; qint32 m_discoveryInterval = 30000; // ms QTimer m_discoveryTimer; bool m_discovering = false; QList m_discoveredServers; + QVariantList m_serversCache; + /* -- PKI -- */ QString m_pkiDir; QString m_certFile; QString m_keyFile; - QVariantList m_serversCache; + + static inline BobinkClient *s_instance = nullptr; }; #endif // BOBINKCLIENT_H diff --git a/src/BobinkNode.cpp b/src/BobinkNode.cpp index fed87bc..55e3b75 100644 --- a/src/BobinkNode.cpp +++ b/src/BobinkNode.cpp @@ -104,7 +104,7 @@ BobinkNode::componentComplete () QQuickItem::componentComplete (); m_componentComplete = true; - auto *client = BobinkClient::s_instance; + auto *client = BobinkClient::instance (); if (client) connect (client, &BobinkClient::connectedChanged, this, &BobinkNode::handleClientConnectedChanged); @@ -137,7 +137,7 @@ BobinkNode::startMonitoring () if (m_opcuaNode || m_nodeId.isEmpty ()) return; - auto *client = BobinkClient::s_instance; + auto *client = BobinkClient::instance (); if (!client || !client->connected ()) return; @@ -245,10 +245,10 @@ BobinkNode::handleAttributeWritten (QOpcUa::NodeAttribute attr, void BobinkNode::handleClientConnectedChanged () { - if (!BobinkClient::s_instance) + if (!BobinkClient::instance ()) return; - if (BobinkClient::s_instance->connected ()) + if (BobinkClient::instance ()->connected ()) { if (m_componentComplete && isVisible ()) startMonitoring (); @@ -262,7 +262,7 @@ BobinkNode::handleClientConnectedChanged () void BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs) { - if (!BobinkClient::s_instance || !m_opcuaNode) + if (!BobinkClient::instance () || !m_opcuaNode) return; for (int bit = 0; bit < 27; ++bit) @@ -274,12 +274,12 @@ BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs) auto sc = m_opcuaNode->attributeError (attr); QLatin1StringView name = nameFromAttribute (attr); if (sc == QOpcUa::UaStatusCode::Good) - emit BobinkClient::s_instance->statusMessage ( + emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Read %1.%2 = %3") .arg (m_nodeId, name, m_opcuaNode->attribute (attr).toString ())); else - emit BobinkClient::s_instance->statusMessage ( + emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Read %1.%2 failed: 0x%3") .arg (m_nodeId, name) .arg (static_cast (sc), 8, 16, QLatin1Char ('0'))); @@ -290,32 +290,32 @@ void BobinkNode::handleEnableMonitoringFinished (QOpcUa::NodeAttribute, QOpcUa::UaStatusCode statusCode) { - if (!BobinkClient::s_instance) + if (!BobinkClient::instance ()) return; if (statusCode == QOpcUa::Good) - emit BobinkClient::s_instance->statusMessage ( + emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Monitoring started: %1").arg (m_nodeId)); else - emit BobinkClient::s_instance->statusMessage ( - QStringLiteral ("Monitoring failed for %1: 0x%2") - .arg (m_nodeId) - .arg (static_cast (statusCode), 8, 16, - QLatin1Char ('0'))); + emit BobinkClient::instance () + -> statusMessage (QStringLiteral ("Monitoring failed for %1: 0x%2") + .arg (m_nodeId) + .arg (static_cast (statusCode), 8, 16, + QLatin1Char ('0'))); } void BobinkNode::handleDisableMonitoringFinished (QOpcUa::NodeAttribute, QOpcUa::UaStatusCode statusCode) { - if (!BobinkClient::s_instance) + if (!BobinkClient::instance ()) return; if (statusCode == QOpcUa::Good) - emit BobinkClient::s_instance->statusMessage ( + emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Monitoring stopped: %1").arg (m_nodeId)); else - emit BobinkClient::s_instance->statusMessage ( + emit BobinkClient::instance () -> statusMessage ( QStringLiteral ("Stop monitoring failed for %1: 0x%2") .arg (m_nodeId) .arg (static_cast (statusCode), 8, 16, -- cgit v1.2.3