summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/BobinkAuth.cpp73
-rw-r--r--src/BobinkAuth.h60
-rw-r--r--src/BobinkClient.cpp315
-rw-r--r--src/BobinkClient.h123
-rw-r--r--src/CMakeLists.txt12
5 files changed, 583 insertions, 0 deletions
diff --git a/src/BobinkAuth.cpp b/src/BobinkAuth.cpp
new file mode 100644
index 0000000..fed1da2
--- /dev/null
+++ b/src/BobinkAuth.cpp
@@ -0,0 +1,73 @@
+#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
new file mode 100644
index 0000000..2e3ea6a
--- /dev/null
+++ b/src/BobinkAuth.h
@@ -0,0 +1,60 @@
+#ifndef BOBINKAUTH_H
+#define BOBINKAUTH_H
+
+#include <QObject>
+#include <QOpcUaAuthenticationInformation>
+#include <QQmlEngine>
+
+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:
+ 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);
+
+ 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
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();
+}
diff --git a/src/BobinkClient.h b/src/BobinkClient.h
new file mode 100644
index 0000000..b8e5624
--- /dev/null
+++ b/src/BobinkClient.h
@@ -0,0 +1,123 @@
+#ifndef BOBINKCLIENT_H
+#define BOBINKCLIENT_H
+
+#include <QEventLoop>
+#include <QObject>
+#include <QOpcUaApplicationDescription>
+#include <QOpcUaClient>
+#include <QOpcUaEndpointDescription>
+#include <QOpcUaErrorState>
+#include <QOpcUaPkiConfiguration>
+#include <QOpcUaProvider>
+#include <QQmlEngine>
+#include <QTimer>
+
+class BobinkAuth;
+
+class BobinkClient : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_SINGLETON
+
+ 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)
+
+ Q_PROPERTY (QString discoveryUrl READ discoveryUrl WRITE setDiscoveryUrl
+ NOTIFY discoveryUrlChanged)
+ Q_PROPERTY (int discoveryInterval READ discoveryInterval WRITE
+ setDiscoveryInterval NOTIFY discoveryIntervalChanged)
+ Q_PROPERTY (bool discovering READ discovering NOTIFY discoveringChanged)
+ Q_PROPERTY (QVariantList servers READ servers NOTIFY serversChanged)
+
+ Q_PROPERTY (QString pkiDir READ pkiDir WRITE setPkiDir NOTIFY pkiDirChanged)
+
+public:
+ explicit BobinkClient (QObject *parent = nullptr);
+ ~BobinkClient () override;
+
+ static BobinkClient *instance ();
+ 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);
+
+ int discoveryInterval () const;
+ void setDiscoveryInterval (int ms);
+
+ bool discovering () const;
+
+ const QList<QOpcUaApplicationDescription> &discoveredServers () const;
+ QVariantList servers () const;
+
+ QString pkiDir () const;
+ void setPkiDir (const QString &path);
+
+ Q_INVOKABLE void connectToServer ();
+ Q_INVOKABLE void disconnectFromServer ();
+ Q_INVOKABLE void acceptCertificate ();
+ Q_INVOKABLE void rejectCertificate ();
+
+ Q_INVOKABLE void startDiscovery ();
+ Q_INVOKABLE void stopDiscovery ();
+ Q_INVOKABLE void applyPki ();
+
+signals:
+ void connectedChanged ();
+ void serverUrlChanged ();
+ void authChanged ();
+ void certificateTrustRequested (const QString &certInfo);
+ void connectionError (const QString &message);
+
+ void discoveryUrlChanged ();
+ void discoveryIntervalChanged ();
+ void discoveringChanged ();
+ void serversChanged ();
+ void pkiDirChanged ();
+
+private slots:
+ void handleStateChanged (QOpcUaClient::ClientState state);
+ void
+ handleEndpointsReceived (const QList<QOpcUaEndpointDescription> &endpoints,
+ QOpcUa::UaStatusCode statusCode,
+ const QUrl &requestUrl);
+ void handleConnectError (QOpcUaErrorState *errorState);
+ void handleFindServersFinished (
+ const QList<QOpcUaApplicationDescription> &servers,
+ QOpcUa::UaStatusCode statusCode, const QUrl &requestUrl);
+ void doDiscovery ();
+
+private:
+ void setupClient ();
+
+ static BobinkClient *s_instance;
+ 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;
+
+ QString m_discoveryUrl;
+ int m_discoveryInterval = 10000;
+ QTimer m_discoveryTimer;
+ bool m_discovering = false;
+ QList<QOpcUaApplicationDescription> m_discoveredServers;
+
+ QString m_pkiDir;
+};
+
+#endif // BOBINKCLIENT_H
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..9b7cac2
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,12 @@
+qt_add_qml_module(bobink
+ URI Bobink
+ VERSION 1.0
+ SOURCES
+ BobinkAuth.h BobinkAuth.cpp
+ BobinkClient.h BobinkClient.cpp
+ # BobinkServerDiscovery.h BobinkServerDiscovery.cpp
+ # BobinkNode.h BobinkNode.cpp
+ OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml/Bobink"
+)
+
+target_link_libraries(bobink PRIVATE Qt6::Core Qt6::Quick Qt6::OpcUa)