/** * @file BobinkClient.cpp * @brief BobinkClient implementation. */ #include "BobinkClient.h" #include "BobinkAuth.h" #include #include BobinkClient *BobinkClient::s_instance = nullptr; 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 : {"own/certs", "own/private", "trusted/certs", "trusted/crl", "issuers/certs", "issuers/crl"}) { QDir().mkpath(base + QLatin1Char('/') + QLatin1String(sub)); } } BobinkClient::BobinkClient(QObject *parent) : QObject(parent) , m_provider(new QOpcUaProvider(this)) , m_pkiDir(defaultPkiDir()) { ensurePkiDirs(m_pkiDir); setupClient(); connect(&m_discoveryTimer, &QTimer::timeout, this, &BobinkClient::doDiscovery); } BobinkClient::~BobinkClient() { if (s_instance == this) s_instance = nullptr; } BobinkClient *BobinkClient::instance() { return s_instance; } BobinkClient *BobinkClient::create(QQmlEngine *, QJSEngine *) { if (!s_instance) { s_instance = new BobinkClient; QJSEngine::setObjectOwnership(s_instance, QJSEngine::CppOwnership); } return s_instance; } void BobinkClient::setupClient() { m_client = m_provider->createClient(QStringLiteral("open62541")); if (!m_client) { qWarning() << "BobinkClient: failed to create open62541 backend"; return; } connect(m_client, &QOpcUaClient::stateChanged, this, &BobinkClient::handleStateChanged); connect(m_client, &QOpcUaClient::endpointsRequestFinished, this, &BobinkClient::handleEndpointsReceived); connect(m_client, &QOpcUaClient::connectError, this, &BobinkClient::handleConnectError); connect(m_client, &QOpcUaClient::findServersFinished, this, &BobinkClient::handleFindServersFinished); } /* ====================================== * Connection properties * ====================================== */ bool BobinkClient::connected() const { return m_connected; } QString BobinkClient::serverUrl() const { return m_serverUrl; } void BobinkClient::setServerUrl(const QString &url) { if (m_serverUrl == url) return; m_serverUrl = url; emit serverUrlChanged(); } BobinkAuth *BobinkClient::auth() const { return m_auth; } void BobinkClient::setAuth(BobinkAuth *auth) { if (m_auth == auth) return; m_auth = auth; emit authChanged(); } QOpcUaClient *BobinkClient::opcuaClient() const { return m_client; } /* ====================================== * Connection methods * ====================================== */ void BobinkClient::connectToServer() { if (!m_client) { emit connectionError(QStringLiteral("OPC UA backend not available")); return; } if (m_serverUrl.isEmpty()) { emit connectionError(QStringLiteral("No server URL set")); return; } if (m_client->state() != QOpcUaClient::Disconnected) { emit connectionError(QStringLiteral("Already connected or connecting")); return; } 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() { if (m_client) m_client->disconnectFromEndpoint(); } void BobinkClient::acceptCertificate() { m_certAccepted = true; if (m_certLoop) m_certLoop->quit(); } void BobinkClient::rejectCertificate() { m_certAccepted = false; if (m_certLoop) m_certLoop->quit(); } /* ====================================== * Discovery properties * ====================================== */ QString BobinkClient::discoveryUrl() const { return m_discoveryUrl; } void BobinkClient::setDiscoveryUrl(const QString &url) { if (m_discoveryUrl == url) return; m_discoveryUrl = url; emit discoveryUrlChanged(); } int BobinkClient::discoveryInterval() const { return m_discoveryInterval; } void BobinkClient::setDiscoveryInterval(int ms) { if (m_discoveryInterval == ms) return; m_discoveryInterval = ms; emit discoveryIntervalChanged(); if (m_discoveryTimer.isActive()) m_discoveryTimer.setInterval(ms); } bool BobinkClient::discovering() const { return m_discovering; } const QList &BobinkClient::discoveredServers() const { return m_discoveredServers; } QVariantList BobinkClient::servers() const { return m_serversCache; } /* ====================================== * 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::applyPki() { if (!m_client || m_pkiDir.isEmpty()) 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()) m_client->setApplicationIdentity(pki.applicationIdentity()); } /* ====================================== * Discovery methods * ====================================== */ void BobinkClient::startDiscovery() { if (m_discoveryUrl.isEmpty() || !m_client) return; doDiscovery(); m_discoveryTimer.start(m_discoveryInterval); if (!m_discovering) { m_discovering = true; emit discoveringChanged(); } } void BobinkClient::stopDiscovery() { m_discoveryTimer.stop(); if (m_discovering) { m_discovering = false; emit discoveringChanged(); } } void BobinkClient::doDiscovery() { if (!m_client || m_discoveryUrl.isEmpty()) 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(); }