summaryrefslogtreecommitdiffstats
path: root/src/BobinkClient.cpp
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-17 23:58:08 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-17 23:58:08 +0100
commit343169dff6b062074fd3c4a5e240b449ffc4a449 (patch)
treea2ef1edc8dd1b1c1dbac757192fa681d8ec76717 /src/BobinkClient.cpp
downloadBobinkQtOpcUa-343169dff6b062074fd3c4a5e240b449ffc4a449.tar.gz
BobinkQtOpcUa-343169dff6b062074fd3c4a5e240b449ffc4a449.zip
Initial Bobink library: BobinkAuth, BobinkClient, and demo app
Implements the core OPC UA wrapper library with: - Build system with automatic dep building (open62541, QtOpcUa) - BobinkAuth: QML auth component (anonymous/userpass/certificate) - BobinkClient: QML singleton managing connection, LDS discovery, PKI configuration, endpoint selection, and certificate trust flow - Demo app for manual testing of the full connection flow
Diffstat (limited to 'src/BobinkClient.cpp')
-rw-r--r--src/BobinkClient.cpp315
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();
+}