diff options
Diffstat (limited to 'src/BobinkClient.cpp')
| -rw-r--r-- | src/BobinkClient.cpp | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp new file mode 100644 index 0000000..ea61583 --- /dev/null +++ b/src/BobinkClient.cpp @@ -0,0 +1,315 @@ +#include "BobinkClient.h" +#include "BobinkAuth.h" + +#include <QDir> +#include <QStandardPaths> + +BobinkClient *BobinkClient::s_instance = nullptr; + +static QString defaultPkiDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + QStringLiteral("/pki"); +} + +static void ensurePkiDirs(const QString &base) +{ + for (const auto *sub : {"own/certs", "own/private", + "trusted/certs", "trusted/crl", + "issuers/certs", "issuers/crl"}) { + QDir().mkpath(base + QLatin1Char('/') + QLatin1String(sub)); + } +} + +BobinkClient::BobinkClient(QObject *parent) + : QObject(parent) + , m_provider(new QOpcUaProvider(this)) + , m_pkiDir(defaultPkiDir()) +{ + ensurePkiDirs(m_pkiDir); + setupClient(); + connect(&m_discoveryTimer, &QTimer::timeout, this, &BobinkClient::doDiscovery); +} + +BobinkClient::~BobinkClient() +{ + if (s_instance == this) + s_instance = nullptr; +} + +BobinkClient *BobinkClient::instance() +{ + return s_instance; +} + +BobinkClient *BobinkClient::create(QQmlEngine *, QJSEngine *) +{ + if (!s_instance) { + s_instance = new BobinkClient; + QJSEngine::setObjectOwnership(s_instance, QJSEngine::CppOwnership); + } + return s_instance; +} + +void BobinkClient::setupClient() +{ + m_client = m_provider->createClient(QStringLiteral("open62541")); + if (!m_client) { + qWarning() << "BobinkClient: failed to create open62541 backend"; + return; + } + + connect(m_client, &QOpcUaClient::stateChanged, + this, &BobinkClient::handleStateChanged); + connect(m_client, &QOpcUaClient::endpointsRequestFinished, + this, &BobinkClient::handleEndpointsReceived); + connect(m_client, &QOpcUaClient::connectError, + this, &BobinkClient::handleConnectError); + connect(m_client, &QOpcUaClient::findServersFinished, + this, &BobinkClient::handleFindServersFinished); +} + +// --- Connection properties --- + +bool BobinkClient::connected() const { return m_connected; } + +QString BobinkClient::serverUrl() const { return m_serverUrl; } + +void BobinkClient::setServerUrl(const QString &url) +{ + if (m_serverUrl == url) + return; + m_serverUrl = url; + emit serverUrlChanged(); +} + +BobinkAuth *BobinkClient::auth() const { return m_auth; } + +void BobinkClient::setAuth(BobinkAuth *auth) +{ + if (m_auth == auth) + return; + m_auth = auth; + emit authChanged(); +} + +QOpcUaClient *BobinkClient::opcuaClient() const { return m_client; } + +// --- Connection methods --- + +void BobinkClient::connectToServer() +{ + if (!m_client) { + emit connectionError(QStringLiteral("OPC UA backend not available")); + return; + } + if (m_serverUrl.isEmpty()) { + emit connectionError(QStringLiteral("No server URL set")); + return; + } + if (m_client->state() != QOpcUaClient::Disconnected) { + emit connectionError(QStringLiteral("Already connected or connecting")); + return; + } + + m_client->requestEndpoints(QUrl(m_serverUrl)); +} + +void BobinkClient::disconnectFromServer() +{ + if (m_client) + m_client->disconnectFromEndpoint(); +} + +void BobinkClient::acceptCertificate() +{ + m_certAccepted = true; + if (m_certLoop) + m_certLoop->quit(); +} + +void BobinkClient::rejectCertificate() +{ + m_certAccepted = false; + if (m_certLoop) + m_certLoop->quit(); +} + +// --- Discovery properties --- + +QString BobinkClient::discoveryUrl() const { return m_discoveryUrl; } + +void BobinkClient::setDiscoveryUrl(const QString &url) +{ + if (m_discoveryUrl == url) + return; + m_discoveryUrl = url; + emit discoveryUrlChanged(); +} + +int BobinkClient::discoveryInterval() const { return m_discoveryInterval; } + +void BobinkClient::setDiscoveryInterval(int ms) +{ + if (m_discoveryInterval == ms) + return; + m_discoveryInterval = ms; + emit discoveryIntervalChanged(); + + if (m_discoveryTimer.isActive()) + m_discoveryTimer.setInterval(ms); +} + +bool BobinkClient::discovering() const { return m_discovering; } + +const QList<QOpcUaApplicationDescription> &BobinkClient::discoveredServers() const +{ + return m_discoveredServers; +} + +QVariantList BobinkClient::servers() const +{ + QVariantList list; + for (const auto &s : m_discoveredServers) { + QVariantMap entry; + entry[QStringLiteral("serverName")] = s.applicationName().text(); + entry[QStringLiteral("applicationUri")] = s.applicationUri(); + entry[QStringLiteral("discoveryUrls")] = QVariant::fromValue(s.discoveryUrls()); + list.append(entry); + } + return list; +} + +// --- PKI --- + +QString BobinkClient::pkiDir() const { return m_pkiDir; } + +void BobinkClient::setPkiDir(const QString &path) +{ + if (m_pkiDir == path) + return; + m_pkiDir = path; + ensurePkiDirs(m_pkiDir); + emit pkiDirChanged(); +} + +void BobinkClient::applyPki() +{ + if (!m_client || m_pkiDir.isEmpty()) + return; + + QOpcUaPkiConfiguration pki; + pki.setClientCertificateFile(m_pkiDir + QStringLiteral("/own/certs/BobinkDemo_cert.der")); + pki.setPrivateKeyFile(m_pkiDir + QStringLiteral("/own/private/BobinkDemo_key.pem")); + pki.setTrustListDirectory(m_pkiDir + QStringLiteral("/trusted/certs")); + pki.setRevocationListDirectory(m_pkiDir + QStringLiteral("/trusted/crl")); + pki.setIssuerListDirectory(m_pkiDir + QStringLiteral("/issuers/certs")); + pki.setIssuerRevocationListDirectory(m_pkiDir + QStringLiteral("/issuers/crl")); + + m_client->setPkiConfiguration(pki); + + if (pki.isKeyAndCertificateFileSet()) + m_client->setApplicationIdentity(pki.applicationIdentity()); +} + +// --- Discovery methods --- + +void BobinkClient::startDiscovery() +{ + if (m_discoveryUrl.isEmpty() || !m_client) + return; + + doDiscovery(); + m_discoveryTimer.start(m_discoveryInterval); + + if (!m_discovering) { + m_discovering = true; + emit discoveringChanged(); + } +} + +void BobinkClient::stopDiscovery() +{ + m_discoveryTimer.stop(); + + if (m_discovering) { + m_discovering = false; + emit discoveringChanged(); + } +} + +void BobinkClient::doDiscovery() +{ + if (m_client && !m_discoveryUrl.isEmpty()) + m_client->findServers(QUrl(m_discoveryUrl)); +} + +// --- Private slots --- + +void BobinkClient::handleStateChanged(QOpcUaClient::ClientState state) +{ + bool nowConnected = (state == QOpcUaClient::Connected); + if (m_connected != nowConnected) { + m_connected = nowConnected; + emit connectedChanged(); + } +} + +void BobinkClient::handleEndpointsReceived( + const QList<QOpcUaEndpointDescription> &endpoints, + QOpcUa::UaStatusCode statusCode, const QUrl &) +{ + if (statusCode != QOpcUa::Good || endpoints.isEmpty()) { + emit connectionError(QStringLiteral("Failed to retrieve endpoints")); + return; + } + + QOpcUaEndpointDescription best = endpoints.first(); + for (const auto &ep : endpoints) { + if (ep.securityLevel() > best.securityLevel()) + best = ep; + } + + if (m_auth) + m_client->setAuthenticationInformation(m_auth->toAuthenticationInformation()); + + m_client->connectToEndpoint(best); +} + +void BobinkClient::handleConnectError(QOpcUaErrorState *errorState) +{ + if (errorState->connectionStep() == + QOpcUaErrorState::ConnectionStep::CertificateValidation) { + // connectError uses BlockingQueuedConnection — the backend thread is + // blocked waiting for us to return. The errorState pointer is stack- + // allocated in the backend, so it is only valid during this call. + // Spin a local event loop so QML can show a dialog and call + // acceptCertificate() / rejectCertificate() while we stay in scope. + m_certAccepted = false; + emit certificateTrustRequested( + QStringLiteral("The server certificate is not trusted. Accept?")); + + QEventLoop loop; + m_certLoop = &loop; + loop.exec(); + m_certLoop = nullptr; + + errorState->setIgnoreError(m_certAccepted); + } else { + emit connectionError( + QStringLiteral("Connection error at step %1, code 0x%2") + .arg(static_cast<int>(errorState->connectionStep())) + .arg(static_cast<uint>(errorState->errorCode()), 8, 16, QLatin1Char('0'))); + } +} + +void BobinkClient::handleFindServersFinished( + const QList<QOpcUaApplicationDescription> &servers, + QOpcUa::UaStatusCode statusCode, const QUrl &) +{ + if (statusCode != QOpcUa::Good) + return; + + m_discoveredServers = servers; + emit serversChanged(); +} |
