From 0012cb312e92c33f5263478d318eb82da22ee879 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Fri, 20 Feb 2026 10:41:09 +0100 Subject: Rename classes to OpcUa* prefix, replace BobinkNode with OpcUaMonitoredNode boilerplate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename BobinkAuth → OpcUaAuth, BobinkClient → OpcUaClient (C++ class names only; QML module URI and singleton name stay as Bobink). Remove BobinkNode (QQuickItem-based) and add OpcUaMonitoredNode skeleton using QObject + QQmlParserStatus, following Qt convention for non-visual QML types. --- src/BobinkAuth.cpp | 101 -------- src/BobinkAuth.h | 75 ------ src/BobinkClient.cpp | 569 --------------------------------------------- src/BobinkClient.h | 193 --------------- src/BobinkNode.cpp | 410 -------------------------------- src/BobinkNode.h | 111 --------- src/CMakeLists.txt | 12 +- src/OpcUaAuth.cpp | 101 ++++++++ src/OpcUaAuth.h | 75 ++++++ src/OpcUaClient.cpp | 569 +++++++++++++++++++++++++++++++++++++++++++++ src/OpcUaClient.h | 193 +++++++++++++++ src/OpcUaMonitoredNode.cpp | 18 ++ src/OpcUaMonitoredNode.h | 31 +++ 13 files changed, 993 insertions(+), 1465 deletions(-) delete mode 100644 src/BobinkAuth.cpp delete mode 100644 src/BobinkAuth.h delete mode 100644 src/BobinkClient.cpp delete mode 100644 src/BobinkClient.h delete mode 100644 src/BobinkNode.cpp delete mode 100644 src/BobinkNode.h create mode 100644 src/OpcUaAuth.cpp create mode 100644 src/OpcUaAuth.h create mode 100644 src/OpcUaClient.cpp create mode 100644 src/OpcUaClient.h create mode 100644 src/OpcUaMonitoredNode.cpp create mode 100644 src/OpcUaMonitoredNode.h (limited to 'src') diff --git a/src/BobinkAuth.cpp b/src/BobinkAuth.cpp deleted file mode 100644 index 132e4be..0000000 --- a/src/BobinkAuth.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @file BobinkAuth.cpp - * @brief BobinkAuth implementation. - */ -#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 deleted file mode 100644 index 2bd3c05..0000000 --- a/src/BobinkAuth.h +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @file BobinkAuth.h - * @brief QML component for OPC UA authentication configuration. - */ -#ifndef BOBINKAUTH_H -#define BOBINKAUTH_H - -#include -#include -#include - -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: - /// Authentication modes supported by OPC UA. - 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); - - /** - * @brief Build a QOpcUaAuthenticationInformation from the - * current mode and credentials. - */ - 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 deleted file mode 100644 index 41b7dbf..0000000 --- a/src/BobinkClient.cpp +++ /dev/null @@ -1,569 +0,0 @@ -/** - * @file BobinkClient.cpp - * @brief BobinkClient implementation. - */ -#include "BobinkClient.h" -#include "BobinkAuth.h" - -#include -#include -#include -#include -#include - -using namespace Qt::Literals::StringLiterals; - -static constexpr qint32 DISCOVERY_INTERVAL = 30000; // ms - -static QString -defaultPkiDir () -{ - return QStandardPaths::writableLocation (QStandardPaths::AppDataLocation) - + QStringLiteral ("/pki"); -} - -/** @brief Create the standard OPC UA PKI directory tree. */ -static void -ensurePkiDirs (const QString &base) -{ - for (const auto sub : - { u"own/certs"_s, u"own/private"_s, u"trusted/certs"_s, - u"trusted/crl"_s, u"issuers/certs"_s, u"issuers/crl"_s }) - { - QDir ().mkpath (base + QLatin1Char ('/') + sub); - } -} - -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 ()) -{ - Q_ASSERT (!s_instance); - ensurePkiDirs (m_pkiDir); - setupClient (); - autoDetectPki (); - applyPki (); - connect (&m_discoveryTimer, &QTimer::timeout, this, - &BobinkClient::doDiscovery); - s_instance = this; -} - -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); - connect (m_client, &QOpcUaClient::errorChanged, this, - &BobinkClient::handleClientError); -} - -/* ====================================== - * Connection - * ====================================== */ - -BobinkClient * -BobinkClient::instance () -{ - return s_instance; -} - -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; -} - -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; - } - - QUrl url (m_serverUrl); - if (!url.isValid ()) - { - emit connectionError ( - QStringLiteral ("Invalid server URL: %1").arg (m_serverUrl)); - return; - } - m_client->requestEndpoints (url); -} - -void -BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode) -{ - 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; - } - - QOpcUaEndpointDescription endpoint; - endpoint.setEndpointUrl (m_serverUrl); - endpoint.setSecurityPolicy (securityPolicyUri (policy)); - endpoint.setSecurityMode ( - static_cast (mode)); - - QOpcUaUserTokenPolicy tokenPolicy; - if (m_auth) - { - switch (m_auth->mode ()) - { - case BobinkAuth::Anonymous: - tokenPolicy.setTokenType ( - QOpcUaUserTokenPolicy::TokenType::Anonymous); - break; - case BobinkAuth::UserPass: - tokenPolicy.setTokenType ( - QOpcUaUserTokenPolicy::TokenType::Username); - break; - case BobinkAuth::Certificate: - tokenPolicy.setTokenType ( - QOpcUaUserTokenPolicy::TokenType::Certificate); - break; - } - m_client->setAuthenticationInformation ( - m_auth->toAuthenticationInformation ()); - } - else - { - tokenPolicy.setTokenType (QOpcUaUserTokenPolicy::TokenType::Anonymous); - } - endpoint.setUserIdentityTokens ({ tokenPolicy }); - - m_client->connectToEndpoint (endpoint); -} - -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 (); -} - -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; - } - - // Pick the endpoint with the highest securityLevel (server-assigned score). - 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::handleClientError (QOpcUaClient::ClientError error) -{ - if (error == QOpcUaClient::NoError) - return; - auto me = QMetaEnum::fromType (); - const char *key = me.valueToKey (static_cast (error)); - emit connectionError ( - QStringLiteral ("Client error: %1") - .arg (key ? QLatin1StringView (key) : "Unknown"_L1)); -} - -/* ====================================== - * Discovery - * ====================================== */ - -QString -BobinkClient::discoveryUrl () const -{ - return m_discoveryUrl; -} - -void -BobinkClient::setDiscoveryUrl (const QString &url) -{ - if (m_discoveryUrl == url) - return; - m_discoveryUrl = url; - emit discoveryUrlChanged (); -} - -bool -BobinkClient::discovering () const -{ - return m_discovering; -} - -const QList & -BobinkClient::discoveredServers () const -{ - return m_discoveredServers; -} - -QVariantList -BobinkClient::servers () const -{ - return m_serversCache; -} - -void -BobinkClient::startDiscovery () -{ - if (m_discoveryUrl.isEmpty () || !m_client) - return; - - doDiscovery (); - m_discoveryTimer.start (DISCOVERY_INTERVAL); - - 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 - * ====================================== */ - -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 (); -} - -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::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 ())); -} - -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; - } - - 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)")); - } -} diff --git a/src/BobinkClient.h b/src/BobinkClient.h deleted file mode 100644 index 7eb6c3c..0000000 --- a/src/BobinkClient.h +++ /dev/null @@ -1,193 +0,0 @@ -/** - * @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 - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -class BobinkAuth; - -class BobinkClient : public QObject -{ - Q_OBJECT - 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) - Q_PROPERTY ( - QString keyFile READ keyFile WRITE setKeyFile NOTIFY keyFileChanged) - -public: - /// OPC UA message security mode (values match MessageSecurityMode). - enum SecurityMode - { - SignAndEncrypt = 3, - Sign = 2, - None = 1, - }; - Q_ENUM (SecurityMode) - - /// OPC UA security policy for encryption algorithms. - enum SecurityPolicy - { - Basic256Sha256, - Aes128_Sha256_RsaOaep, - Aes256_Sha256_RsaPss, - }; - 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. */ - Q_INVOKABLE void connectDirect (SecurityPolicy policy, SecurityMode mode); - Q_INVOKABLE void disconnectFromServer (); - - /** @brief Accept the pending server certificate. */ - Q_INVOKABLE void acceptCertificate (); - /** @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. - * - * The connection blocks until acceptCertificate() or - * rejectCertificate() is called (30 s timeout, auto-rejects). - */ - void certificateTrustRequested (const QString &certInfo); - 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); - void handleClientError (QOpcUaClient::ClientError error); - - /* -- Discovery -- */ - void handleFindServersFinished ( - const QList &servers, - QOpcUa::UaStatusCode statusCode, const QUrl &requestUrl); - void doDiscovery (); - -private: - void setupClient (); - - /* -- Connection -- */ - 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; - - /* -- Discovery -- */ - QString m_discoveryUrl; - QTimer m_discoveryTimer; - bool m_discovering = false; - QList m_discoveredServers; - QVariantList m_serversCache; - - /* -- PKI -- */ - QString m_pkiDir; - QString m_certFile; - QString m_keyFile; - - static inline BobinkClient *s_instance = nullptr; -}; - -#endif // BOBINKCLIENT_H diff --git a/src/BobinkNode.cpp b/src/BobinkNode.cpp deleted file mode 100644 index 55e3b75..0000000 --- a/src/BobinkNode.cpp +++ /dev/null @@ -1,410 +0,0 @@ -/** - * @file BobinkNode.cpp - * @brief QML component representing a single OPC UA node. - */ -#include "BobinkNode.h" -#include "BobinkClient.h" - -#include -#include - -using namespace Qt::Literals::StringLiterals; - -static constexpr double DEFAULT_PUBLISHING_INTERVAL = 250.0; // ms - -/* ------------------------------------------------------------------ */ -/* Construction / destruction */ -/* ------------------------------------------------------------------ */ - -BobinkNode::BobinkNode (QQuickItem *parent) : QQuickItem (parent) {} - -BobinkNode::~BobinkNode () { stopMonitoring (); } - -/* ------------------------------------------------------------------ */ -/* Properties */ -/* ------------------------------------------------------------------ */ - -QString -BobinkNode::nodeId () const -{ - return m_nodeId; -} - -void -BobinkNode::setNodeId (const QString &id) -{ - if (m_nodeId == id) - return; - m_nodeId = id; - emit nodeIdChanged (); - - if (m_componentComplete && isVisible ()) - { - stopMonitoring (); - startMonitoring (); - } -} - -QVariant -BobinkNode::value () const -{ - return m_value; -} - -void -BobinkNode::setValue (const QVariant &value) -{ - if (m_opcuaNode) - m_opcuaNode->writeValueAttribute (value); -} - -BobinkNode::NodeStatus -BobinkNode::status () const -{ - return m_status; -} - -QDateTime -BobinkNode::sourceTimestamp () const -{ - return m_sourceTimestamp; -} - -QDateTime -BobinkNode::serverTimestamp () const -{ - return m_serverTimestamp; -} - -/* ------------------------------------------------------------------ */ -/* readAttribute */ -/* ------------------------------------------------------------------ */ - -void -BobinkNode::readAttribute (const QString &attributeName) -{ - if (!m_opcuaNode) - return; - - QOpcUa::NodeAttribute attr = attributeFromName (attributeName); - if (attr == QOpcUa::NodeAttribute::None) - return; - - m_pendingReads.insert (attr, attributeName); - m_opcuaNode->readAttributes (attr); -} - -/* ------------------------------------------------------------------ */ -/* QQuickItem lifecycle */ -/* ------------------------------------------------------------------ */ - -void -BobinkNode::componentComplete () -{ - QQuickItem::componentComplete (); - m_componentComplete = true; - - auto *client = BobinkClient::instance (); - if (client) - connect (client, &BobinkClient::connectedChanged, this, - &BobinkNode::handleClientConnectedChanged); - - if (isVisible ()) - startMonitoring (); -} - -void -BobinkNode::itemChange (ItemChange change, const ItemChangeData &data) -{ - QQuickItem::itemChange (change, data); - - if (change == ItemVisibleHasChanged && m_componentComplete) - { - if (data.boolValue) - startMonitoring (); - else - stopMonitoring (); - } -} - -/* ------------------------------------------------------------------ */ -/* Monitoring lifecycle */ -/* ------------------------------------------------------------------ */ - -void -BobinkNode::startMonitoring () -{ - if (m_opcuaNode || m_nodeId.isEmpty ()) - return; - - auto *client = BobinkClient::instance (); - if (!client || !client->connected ()) - return; - - QOpcUaClient *opcua = client->opcuaClient (); - if (!opcua) - return; - - m_opcuaNode = opcua->node (m_nodeId); - if (!m_opcuaNode) - return; - - connect (m_opcuaNode, &QOpcUaNode::dataChangeOccurred, this, - &BobinkNode::handleDataChange); - connect (m_opcuaNode, &QOpcUaNode::attributeUpdated, this, - &BobinkNode::handleAttributeUpdated); - connect (m_opcuaNode, &QOpcUaNode::attributeWritten, this, - &BobinkNode::handleAttributeWritten); - connect (m_opcuaNode, &QOpcUaNode::attributeRead, this, - &BobinkNode::handleAttributeReadFinished); - connect (m_opcuaNode, &QOpcUaNode::enableMonitoringFinished, this, - &BobinkNode::handleEnableMonitoringFinished); - connect (m_opcuaNode, &QOpcUaNode::disableMonitoringFinished, this, - &BobinkNode::handleDisableMonitoringFinished); - - QOpcUaMonitoringParameters params (DEFAULT_PUBLISHING_INTERVAL); - m_opcuaNode->enableMonitoring (QOpcUa::NodeAttribute::Value, params); -} - -void -BobinkNode::stopMonitoring () -{ - if (!m_opcuaNode) - return; - - m_pendingReads.clear (); - delete m_opcuaNode; - m_opcuaNode = nullptr; -} - -/* ------------------------------------------------------------------ */ -/* Signal handlers */ -/* ------------------------------------------------------------------ */ - -void -BobinkNode::handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val) -{ - if (attr != QOpcUa::NodeAttribute::Value) - return; - - if (m_value != val) - { - m_value = val; - emit valueChanged (); - } - - NodeStatus newStatus = statusFromCode (m_opcuaNode->valueAttributeError ()); - if (m_status != newStatus) - { - m_status = newStatus; - emit statusChanged (); - } - - QDateTime srcTs - = m_opcuaNode->sourceTimestamp (QOpcUa::NodeAttribute::Value); - if (m_sourceTimestamp != srcTs) - { - m_sourceTimestamp = srcTs; - emit sourceTimestampChanged (); - } - - QDateTime srvTs - = m_opcuaNode->serverTimestamp (QOpcUa::NodeAttribute::Value); - if (m_serverTimestamp != srvTs) - { - m_serverTimestamp = srvTs; - emit serverTimestampChanged (); - } -} - -void -BobinkNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr, - const QVariant &val) -{ - auto it = m_pendingReads.find (attr); - if (it != m_pendingReads.end ()) - { - emit attributeRead (it.value (), val); - m_pendingReads.erase (it); - } -} - -void -BobinkNode::handleAttributeWritten (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode) -{ - if (attr != QOpcUa::NodeAttribute::Value) - return; - - if (statusCode != QOpcUa::UaStatusCode::Good) - emit writeError (QStringLiteral ("Write failed: 0x%1") - .arg (static_cast (statusCode), 8, 16, - QLatin1Char ('0'))); -} - -void -BobinkNode::handleClientConnectedChanged () -{ - if (!BobinkClient::instance ()) - return; - - if (BobinkClient::instance ()->connected ()) - { - if (m_componentComplete && isVisible ()) - startMonitoring (); - } - else - { - stopMonitoring (); - } -} - -void -BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs) -{ - if (!BobinkClient::instance () || !m_opcuaNode) - return; - - for (int bit = 0; bit < 27; ++bit) - { - auto attr = static_cast (1 << bit); - if (!attrs.testFlag (attr)) - continue; - - auto sc = m_opcuaNode->attributeError (attr); - QLatin1StringView name = nameFromAttribute (attr); - if (sc == QOpcUa::UaStatusCode::Good) - emit BobinkClient::instance () -> statusMessage ( - QStringLiteral ("Read %1.%2 = %3") - .arg (m_nodeId, name, - m_opcuaNode->attribute (attr).toString ())); - else - emit BobinkClient::instance () -> statusMessage ( - QStringLiteral ("Read %1.%2 failed: 0x%3") - .arg (m_nodeId, name) - .arg (static_cast (sc), 8, 16, QLatin1Char ('0'))); - } -} - -void -BobinkNode::handleEnableMonitoringFinished (QOpcUa::NodeAttribute, - QOpcUa::UaStatusCode statusCode) -{ - if (!BobinkClient::instance ()) - return; - - if (statusCode == QOpcUa::Good) - emit BobinkClient::instance () -> statusMessage ( - QStringLiteral ("Monitoring started: %1").arg (m_nodeId)); - else - 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::instance ()) - return; - - if (statusCode == QOpcUa::Good) - emit BobinkClient::instance () -> statusMessage ( - QStringLiteral ("Monitoring stopped: %1").arg (m_nodeId)); - else - emit BobinkClient::instance () -> statusMessage ( - QStringLiteral ("Stop monitoring failed for %1: 0x%2") - .arg (m_nodeId) - .arg (static_cast (statusCode), 8, 16, - QLatin1Char ('0'))); -} - -/* ------------------------------------------------------------------ */ -/* Helpers */ -/* ------------------------------------------------------------------ */ - -BobinkNode::NodeStatus -BobinkNode::statusFromCode (QOpcUa::UaStatusCode code) -{ - quint32 severity = static_cast (code) >> 30; - switch (severity) - { - case 0: - return Good; - case 1: - return Uncertain; - default: - return Bad; - } -} - -QOpcUa::NodeAttribute -BobinkNode::attributeFromName (const QString &name) -{ - static const QHash map = { - { QStringLiteral ("NodeId"), QOpcUa::NodeAttribute::NodeId }, - { QStringLiteral ("NodeClass"), QOpcUa::NodeAttribute::NodeClass }, - { QStringLiteral ("BrowseName"), QOpcUa::NodeAttribute::BrowseName }, - { QStringLiteral ("DisplayName"), QOpcUa::NodeAttribute::DisplayName }, - { QStringLiteral ("Description"), QOpcUa::NodeAttribute::Description }, - { QStringLiteral ("Value"), QOpcUa::NodeAttribute::Value }, - { QStringLiteral ("DataType"), QOpcUa::NodeAttribute::DataType }, - { QStringLiteral ("ValueRank"), QOpcUa::NodeAttribute::ValueRank }, - { QStringLiteral ("ArrayDimensions"), - QOpcUa::NodeAttribute::ArrayDimensions }, - { QStringLiteral ("AccessLevel"), QOpcUa::NodeAttribute::AccessLevel }, - { QStringLiteral ("UserAccessLevel"), - QOpcUa::NodeAttribute::UserAccessLevel }, - { QStringLiteral ("MinimumSamplingInterval"), - QOpcUa::NodeAttribute::MinimumSamplingInterval }, - { QStringLiteral ("Historizing"), QOpcUa::NodeAttribute::Historizing }, - { QStringLiteral ("Executable"), QOpcUa::NodeAttribute::Executable }, - { QStringLiteral ("UserExecutable"), - QOpcUa::NodeAttribute::UserExecutable }, - }; - - return map.value (name, QOpcUa::NodeAttribute::None); -} - -QLatin1StringView -BobinkNode::nameFromAttribute (QOpcUa::NodeAttribute attr) -{ - switch (attr) - { - case QOpcUa::NodeAttribute::NodeId: - return "NodeId"_L1; - case QOpcUa::NodeAttribute::NodeClass: - return "NodeClass"_L1; - case QOpcUa::NodeAttribute::BrowseName: - return "BrowseName"_L1; - case QOpcUa::NodeAttribute::DisplayName: - return "DisplayName"_L1; - case QOpcUa::NodeAttribute::Description: - return "Description"_L1; - case QOpcUa::NodeAttribute::Value: - return "Value"_L1; - case QOpcUa::NodeAttribute::DataType: - return "DataType"_L1; - case QOpcUa::NodeAttribute::ValueRank: - return "ValueRank"_L1; - case QOpcUa::NodeAttribute::ArrayDimensions: - return "ArrayDimensions"_L1; - case QOpcUa::NodeAttribute::AccessLevel: - return "AccessLevel"_L1; - case QOpcUa::NodeAttribute::UserAccessLevel: - return "UserAccessLevel"_L1; - case QOpcUa::NodeAttribute::MinimumSamplingInterval: - return "MinimumSamplingInterval"_L1; - case QOpcUa::NodeAttribute::Historizing: - return "Historizing"_L1; - case QOpcUa::NodeAttribute::Executable: - return "Executable"_L1; - case QOpcUa::NodeAttribute::UserExecutable: - return "UserExecutable"_L1; - default: - return "Unknown"_L1; - } -} diff --git a/src/BobinkNode.h b/src/BobinkNode.h deleted file mode 100644 index c37a883..0000000 --- a/src/BobinkNode.h +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file BobinkNode.h - * @brief QML component representing a single OPC UA node. - * - * Inherits QQuickItem so that monitoring is automatically tied to - * visibility: when the item (or any ancestor) becomes invisible - * (e.g. StackView navigates away, Loader unloads), the monitored - * item is removed from the subscription. When visible again, - * monitoring resumes. - */ -#ifndef BOBINKNODE_H -#define BOBINKNODE_H - -#include -#include -#include -#include - -class BobinkNode : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged) - Q_PROPERTY (QVariant value READ value WRITE setValue NOTIFY valueChanged) - Q_PROPERTY (NodeStatus status READ status NOTIFY statusChanged) - Q_PROPERTY (QDateTime sourceTimestamp READ sourceTimestamp NOTIFY - sourceTimestampChanged) - Q_PROPERTY (QDateTime serverTimestamp READ serverTimestamp NOTIFY - serverTimestampChanged) - -public: - explicit BobinkNode (QQuickItem *parent = nullptr); - ~BobinkNode () override; - - /// Simplified OPC UA status severity. - enum NodeStatus - { - Good, - Uncertain, - Bad - }; - Q_ENUM (NodeStatus) - - QString nodeId () const; - void setNodeId (const QString &id); - - QVariant value () const; - /** Setting value writes to the server; the property updates when - * the server confirms via the monitored data change. */ - void setValue (const QVariant &value); - - NodeStatus status () const; - QDateTime sourceTimestamp () const; - QDateTime serverTimestamp () const; - - /** Read an attribute on demand (DisplayName, Description, DataType, …). - * Result arrives asynchronously via attributeRead(). */ - Q_INVOKABLE void readAttribute (const QString &attributeName); - -signals: - void nodeIdChanged (); - void valueChanged (); - void statusChanged (); - void sourceTimestampChanged (); - void serverTimestampChanged (); - - /** Emitted when a readAttribute() call completes. */ - void attributeRead (const QString &attributeName, const QVariant &value); - - /** Emitted when a write to the server fails. */ - void writeError (const QString &message); - -protected: - void componentComplete () override; - void itemChange (ItemChange change, const ItemChangeData &data) override; - -private: - void startMonitoring (); - void stopMonitoring (); - - void handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val); - void handleAttributeUpdated (QOpcUa::NodeAttribute attr, - const QVariant &val); - void handleAttributeWritten (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode); - void handleClientConnectedChanged (); - void handleAttributeReadFinished (QOpcUa::NodeAttributes attrs); - void handleEnableMonitoringFinished (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode); - void handleDisableMonitoringFinished (QOpcUa::NodeAttribute attr, - QOpcUa::UaStatusCode statusCode); - - static NodeStatus statusFromCode (QOpcUa::UaStatusCode code); - static QOpcUa::NodeAttribute attributeFromName (const QString &name); - static QLatin1StringView nameFromAttribute (QOpcUa::NodeAttribute attr); - - QString m_nodeId; - QVariant m_value; - NodeStatus m_status = Bad; - QDateTime m_sourceTimestamp; - QDateTime m_serverTimestamp; - - QOpcUaNode *m_opcuaNode = nullptr; - bool m_componentComplete = false; - - /** Tracks in-flight readAttribute() requests (enum → original name). */ - QHash m_pendingReads; -}; - -#endif // BOBINKNODE_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1292194..b0781cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,12 +7,12 @@ qt_add_qml_module( VERSION 1.0 SOURCES - BobinkAuth.h - BobinkAuth.cpp - BobinkClient.h - BobinkClient.cpp - BobinkNode.h - BobinkNode.cpp + OpcUaAuth.h + OpcUaAuth.cpp + OpcUaClient.h + OpcUaClient.cpp + OpcUaMonitoredNode.h + OpcUaMonitoredNode.cpp OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml/Bobink") diff --git a/src/OpcUaAuth.cpp b/src/OpcUaAuth.cpp new file mode 100644 index 0000000..258c9c7 --- /dev/null +++ b/src/OpcUaAuth.cpp @@ -0,0 +1,101 @@ +/** + * @file OpcUaAuth.cpp + * @brief OpcUaAuth implementation. + */ +#include "OpcUaAuth.h" + +OpcUaAuth::OpcUaAuth (QObject *parent) : QObject (parent) {} + +OpcUaAuth::AuthMode +OpcUaAuth::mode () const +{ + return m_mode; +} + +void +OpcUaAuth::setMode (AuthMode mode) +{ + if (m_mode == mode) + return; + m_mode = mode; + emit modeChanged (); +} + +QString +OpcUaAuth::username () const +{ + return m_username; +} + +void +OpcUaAuth::setUsername (const QString &username) +{ + if (m_username == username) + return; + m_username = username; + emit usernameChanged (); +} + +QString +OpcUaAuth::password () const +{ + return m_password; +} + +void +OpcUaAuth::setPassword (const QString &password) +{ + if (m_password == password) + return; + m_password = password; + emit passwordChanged (); +} + +QString +OpcUaAuth::certPath () const +{ + return m_certPath; +} + +void +OpcUaAuth::setCertPath (const QString &path) +{ + if (m_certPath == path) + return; + m_certPath = path; + emit certPathChanged (); +} + +QString +OpcUaAuth::keyPath () const +{ + return m_keyPath; +} + +void +OpcUaAuth::setKeyPath (const QString &path) +{ + if (m_keyPath == path) + return; + m_keyPath = path; + emit keyPathChanged (); +} + +QOpcUaAuthenticationInformation +OpcUaAuth::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/OpcUaAuth.h b/src/OpcUaAuth.h new file mode 100644 index 0000000..8d53c1d --- /dev/null +++ b/src/OpcUaAuth.h @@ -0,0 +1,75 @@ +/** + * @file OpcUaAuth.h + * @brief QML component for OPC UA authentication configuration. + */ +#ifndef OPCUAAUTH_H +#define OPCUAAUTH_H + +#include +#include +#include + +class OpcUaAuth : 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: + /// Authentication modes supported by OPC UA. + enum AuthMode + { + Anonymous, + UserPass, + Certificate + }; + Q_ENUM (AuthMode) + + explicit OpcUaAuth (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); + + /** + * @brief Build a QOpcUaAuthenticationInformation from the + * current mode and credentials. + */ + 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 // OPCUAAUTH_H diff --git a/src/OpcUaClient.cpp b/src/OpcUaClient.cpp new file mode 100644 index 0000000..91ce47b --- /dev/null +++ b/src/OpcUaClient.cpp @@ -0,0 +1,569 @@ +/** + * @file OpcUaClient.cpp + * @brief OpcUaClient implementation. + */ +#include "OpcUaClient.h" +#include "OpcUaAuth.h" + +#include +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +static constexpr qint32 DISCOVERY_INTERVAL = 30000; // ms + +static QString +defaultPkiDir () +{ + return QStandardPaths::writableLocation (QStandardPaths::AppDataLocation) + + QStringLiteral ("/pki"); +} + +/** @brief Create the standard OPC UA PKI directory tree. */ +static void +ensurePkiDirs (const QString &base) +{ + for (const auto sub : + { u"own/certs"_s, u"own/private"_s, u"trusted/certs"_s, + u"trusted/crl"_s, u"issuers/certs"_s, u"issuers/crl"_s }) + { + QDir ().mkpath (base + QLatin1Char ('/') + sub); + } +} + +static QString +securityPolicyUri (OpcUaClient::SecurityPolicy policy) +{ + switch (policy) + { + case OpcUaClient::Basic256Sha256: + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"); + case OpcUaClient::Aes128_Sha256_RsaOaep: + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"); + case OpcUaClient::Aes256_Sha256_RsaPss: + return QStringLiteral ( + "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss"); + } + return {}; +} + +/* ====================================== + * Construction + * ====================================== */ + +OpcUaClient::OpcUaClient (QObject *parent) + : QObject (parent), m_provider (new QOpcUaProvider (this)), + m_pkiDir (defaultPkiDir ()) +{ + Q_ASSERT (!s_instance); + ensurePkiDirs (m_pkiDir); + setupClient (); + autoDetectPki (); + applyPki (); + connect (&m_discoveryTimer, &QTimer::timeout, this, + &OpcUaClient::doDiscovery); + s_instance = this; +} + +void +OpcUaClient::setupClient () +{ + m_client = m_provider->createClient (QStringLiteral ("open62541")); + if (!m_client) + { + qWarning () << "OpcUaClient: failed to create open62541 backend"; + return; + } + + connect (m_client, &QOpcUaClient::stateChanged, this, + &OpcUaClient::handleStateChanged); + connect (m_client, &QOpcUaClient::endpointsRequestFinished, this, + &OpcUaClient::handleEndpointsReceived); + connect (m_client, &QOpcUaClient::connectError, this, + &OpcUaClient::handleConnectError); + connect (m_client, &QOpcUaClient::findServersFinished, this, + &OpcUaClient::handleFindServersFinished); + connect (m_client, &QOpcUaClient::errorChanged, this, + &OpcUaClient::handleClientError); +} + +/* ====================================== + * Connection + * ====================================== */ + +OpcUaClient * +OpcUaClient::instance () +{ + return s_instance; +} + +bool +OpcUaClient::connected () const +{ + return m_connected; +} + +QString +OpcUaClient::serverUrl () const +{ + return m_serverUrl; +} + +void +OpcUaClient::setServerUrl (const QString &url) +{ + if (m_serverUrl == url) + return; + m_serverUrl = url; + emit serverUrlChanged (); +} + +OpcUaAuth * +OpcUaClient::auth () const +{ + return m_auth; +} + +void +OpcUaClient::setAuth (OpcUaAuth *auth) +{ + if (m_auth == auth) + return; + m_auth = auth; + emit authChanged (); +} + +QOpcUaClient * +OpcUaClient::opcuaClient () const +{ + return m_client; +} + +void +OpcUaClient::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; + } + + QUrl url (m_serverUrl); + if (!url.isValid ()) + { + emit connectionError ( + QStringLiteral ("Invalid server URL: %1").arg (m_serverUrl)); + return; + } + m_client->requestEndpoints (url); +} + +void +OpcUaClient::connectDirect (SecurityPolicy policy, SecurityMode mode) +{ + 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; + } + + QOpcUaEndpointDescription endpoint; + endpoint.setEndpointUrl (m_serverUrl); + endpoint.setSecurityPolicy (securityPolicyUri (policy)); + endpoint.setSecurityMode ( + static_cast (mode)); + + QOpcUaUserTokenPolicy tokenPolicy; + if (m_auth) + { + switch (m_auth->mode ()) + { + case OpcUaAuth::Anonymous: + tokenPolicy.setTokenType ( + QOpcUaUserTokenPolicy::TokenType::Anonymous); + break; + case OpcUaAuth::UserPass: + tokenPolicy.setTokenType ( + QOpcUaUserTokenPolicy::TokenType::Username); + break; + case OpcUaAuth::Certificate: + tokenPolicy.setTokenType ( + QOpcUaUserTokenPolicy::TokenType::Certificate); + break; + } + m_client->setAuthenticationInformation ( + m_auth->toAuthenticationInformation ()); + } + else + { + tokenPolicy.setTokenType (QOpcUaUserTokenPolicy::TokenType::Anonymous); + } + endpoint.setUserIdentityTokens ({ tokenPolicy }); + + m_client->connectToEndpoint (endpoint); +} + +void +OpcUaClient::disconnectFromServer () +{ + if (m_client) + m_client->disconnectFromEndpoint (); +} + +void +OpcUaClient::acceptCertificate () +{ + m_certAccepted = true; + if (m_certLoop) + m_certLoop->quit (); +} + +void +OpcUaClient::rejectCertificate () +{ + m_certAccepted = false; + if (m_certLoop) + m_certLoop->quit (); +} + +void +OpcUaClient::handleStateChanged (QOpcUaClient::ClientState state) +{ + bool nowConnected = (state == QOpcUaClient::Connected); + if (m_connected != nowConnected) + { + m_connected = nowConnected; + emit connectedChanged (); + } +} + +void +OpcUaClient::handleEndpointsReceived ( + const QList &endpoints, + QOpcUa::UaStatusCode statusCode, const QUrl &) +{ + if (statusCode != QOpcUa::Good || endpoints.isEmpty ()) + { + emit connectionError (QStringLiteral ("Failed to retrieve endpoints")); + return; + } + + // Pick the endpoint with the highest securityLevel (server-assigned score). + 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 +OpcUaClient::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 +OpcUaClient::handleClientError (QOpcUaClient::ClientError error) +{ + if (error == QOpcUaClient::NoError) + return; + auto me = QMetaEnum::fromType (); + const char *key = me.valueToKey (static_cast (error)); + emit connectionError ( + QStringLiteral ("Client error: %1") + .arg (key ? QLatin1StringView (key) : "Unknown"_L1)); +} + +/* ====================================== + * Discovery + * ====================================== */ + +QString +OpcUaClient::discoveryUrl () const +{ + return m_discoveryUrl; +} + +void +OpcUaClient::setDiscoveryUrl (const QString &url) +{ + if (m_discoveryUrl == url) + return; + m_discoveryUrl = url; + emit discoveryUrlChanged (); +} + +bool +OpcUaClient::discovering () const +{ + return m_discovering; +} + +const QList & +OpcUaClient::discoveredServers () const +{ + return m_discoveredServers; +} + +QVariantList +OpcUaClient::servers () const +{ + return m_serversCache; +} + +void +OpcUaClient::startDiscovery () +{ + if (m_discoveryUrl.isEmpty () || !m_client) + return; + + doDiscovery (); + m_discoveryTimer.start (DISCOVERY_INTERVAL); + + if (!m_discovering) + { + m_discovering = true; + emit discoveringChanged (); + } +} + +void +OpcUaClient::stopDiscovery () +{ + m_discoveryTimer.stop (); + + if (m_discovering) + { + m_discovering = false; + emit discoveringChanged (); + } +} + +void +OpcUaClient::doDiscovery () +{ + if (!m_client || m_discoveryUrl.isEmpty ()) + return; + QUrl url (m_discoveryUrl); + if (!url.isValid ()) + return; + m_client->findServers (url); +} + +void +OpcUaClient::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 + * ====================================== */ + +QString +OpcUaClient::pkiDir () const +{ + return m_pkiDir; +} + +void +OpcUaClient::setPkiDir (const QString &path) +{ + if (m_pkiDir == path) + return; + m_pkiDir = path; + ensurePkiDirs (m_pkiDir); + emit pkiDirChanged (); +} + +QString +OpcUaClient::certFile () const +{ + return m_certFile; +} + +void +OpcUaClient::setCertFile (const QString &path) +{ + if (m_certFile == path) + return; + m_certFile = path; + emit certFileChanged (); +} + +QString +OpcUaClient::keyFile () const +{ + return m_keyFile; +} + +void +OpcUaClient::setKeyFile (const QString &path) +{ + if (m_keyFile == path) + return; + m_keyFile = path; + emit keyFileChanged (); +} + +void +OpcUaClient::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 ())); +} + +void +OpcUaClient::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; + } + + 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)")); + } +} diff --git a/src/OpcUaClient.h b/src/OpcUaClient.h new file mode 100644 index 0000000..1476911 --- /dev/null +++ b/src/OpcUaClient.h @@ -0,0 +1,193 @@ +/** + * @file OpcUaClient.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 OPCUACLIENT_H +#define OPCUACLIENT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class OpcUaAuth; + +class OpcUaClient : public QObject +{ + Q_OBJECT + 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 (OpcUaAuth *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) + Q_PROPERTY ( + QString keyFile READ keyFile WRITE setKeyFile NOTIFY keyFileChanged) + +public: + /// OPC UA message security mode (values match MessageSecurityMode). + enum SecurityMode + { + SignAndEncrypt = 3, + Sign = 2, + None = 1, + }; + Q_ENUM (SecurityMode) + + /// OPC UA security policy for encryption algorithms. + enum SecurityPolicy + { + Basic256Sha256, + Aes128_Sha256_RsaOaep, + Aes256_Sha256_RsaPss, + }; + Q_ENUM (SecurityPolicy) + + OpcUaClient (QObject *parent = nullptr); + + /* -- Connection -- */ + + bool connected () const; + + QString serverUrl () const; + void setServerUrl (const QString &url); + + OpcUaAuth *auth () const; + void setAuth (OpcUaAuth *auth); + + /** @brief Discover endpoints, pick the most secure, connect. */ + Q_INVOKABLE void connectToServer (); + /** @brief Connect directly without endpoint discovery. */ + Q_INVOKABLE void connectDirect (SecurityPolicy policy, SecurityMode mode); + Q_INVOKABLE void disconnectFromServer (); + + /** @brief Accept the pending server certificate. */ + Q_INVOKABLE void acceptCertificate (); + /** @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 OpcUaClient *instance (); + QOpcUaClient *opcuaClient () const; + +signals: + /* -- Connection -- */ + 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); + 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); + void handleClientError (QOpcUaClient::ClientError error); + + /* -- Discovery -- */ + void handleFindServersFinished ( + const QList &servers, + QOpcUa::UaStatusCode statusCode, const QUrl &requestUrl); + void doDiscovery (); + +private: + void setupClient (); + + /* -- Connection -- */ + QOpcUaProvider *m_provider = nullptr; + QOpcUaClient *m_client = nullptr; + OpcUaAuth *m_auth = nullptr; + QString m_serverUrl; + bool m_connected = false; + QEventLoop *m_certLoop = nullptr; + bool m_certAccepted = false; + + /* -- Discovery -- */ + QString m_discoveryUrl; + QTimer m_discoveryTimer; + bool m_discovering = false; + QList m_discoveredServers; + QVariantList m_serversCache; + + /* -- PKI -- */ + QString m_pkiDir; + QString m_certFile; + QString m_keyFile; + + static inline OpcUaClient *s_instance = nullptr; +}; + +#endif // OPCUACLIENT_H diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp new file mode 100644 index 0000000..da66e89 --- /dev/null +++ b/src/OpcUaMonitoredNode.cpp @@ -0,0 +1,18 @@ +/** + * @file OpcUaMonitoredNode.cpp + * @brief OpcUaMonitoredNode implementation. + */ +#include "OpcUaMonitoredNode.h" + +OpcUaMonitoredNode::OpcUaMonitoredNode (QObject *parent) : QObject (parent) {} + +void +OpcUaMonitoredNode::classBegin () +{ +} + +void +OpcUaMonitoredNode::componentComplete () +{ + m_componentComplete = true; +} diff --git a/src/OpcUaMonitoredNode.h b/src/OpcUaMonitoredNode.h new file mode 100644 index 0000000..ccf3444 --- /dev/null +++ b/src/OpcUaMonitoredNode.h @@ -0,0 +1,31 @@ +/** + * @file OpcUaMonitoredNode.h + * @brief QML component for monitoring a single OPC UA node. + * + * Inherits QObject + QQmlParserStatus so that initialisation is + * deferred until all QML bindings are applied (componentComplete). + */ +#ifndef OPCUAMONITOREDNODE_H +#define OPCUAMONITOREDNODE_H + +#include +#include +#include + +class OpcUaMonitoredNode : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES (QQmlParserStatus) + QML_ELEMENT + +public: + explicit OpcUaMonitoredNode (QObject *parent = nullptr); + + void classBegin () override; + void componentComplete () override; + +private: + bool m_componentComplete = false; +}; + +#endif // OPCUAMONITOREDNODE_H -- cgit v1.2.3