diff options
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | demo/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | demo/Main.qml | 191 | ||||
| -rw-r--r-- | demo/NodePage.qml | 2 | ||||
| -rw-r--r-- | demo/main.cpp | 12 | ||||
| -rw-r--r-- | src/BobinkClient.cpp | 28 | ||||
| -rw-r--r-- | src/BobinkClient.h | 13 | ||||
| -rw-r--r-- | src/BobinkNode.cpp | 30 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 3 |
9 files changed, 168 insertions, 121 deletions
@@ -1,3 +1,9 @@ -CLAUDE.md +# IDE / editor +.qtcreator .cache + +# Build output build + +# AI assistant +CLAUDE.md diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index 255a9ee..efdfc72 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -14,7 +14,7 @@ qt_add_qml_module( set_target_properties(BobinkDemo PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") -target_link_libraries(BobinkDemo PRIVATE Qt6::Quick BobinkQtOpcUa) +target_link_libraries(BobinkDemo PRIVATE Qt6::Quick BobinkQtOpcUaplugin) # Tell the demo where to find the locally-built OpcUa plugin at runtime target_compile_definitions( diff --git a/demo/Main.qml b/demo/Main.qml index 7b76fee..ef09b7d 100644 --- a/demo/Main.qml +++ b/demo/Main.qml @@ -18,15 +18,15 @@ ApplicationWindow { property bool showPkiSettings: false Connections { - target: BobinkClient + target: Bobink function onServersChanged() { debugConsole.appendLog("Discovered server list updated"); } function onConnectedChanged() { - debugConsole.appendLog("Connected: " + BobinkClient.connected); - if (BobinkClient.connected) { + debugConsole.appendLog("Connected: " + Bobink.connected); + if (Bobink.connected) { root.autoConnectFailed = false; - BobinkClient.stopDiscovery(); + Bobink.stopDiscovery(); stack.push("NodePage.qml", { stackRef: stack, pageNumber: 1, @@ -44,7 +44,7 @@ ApplicationWindow { debugConsole.appendLog(message); } function onDiscoveringChanged() { - debugConsole.appendLog("Discovering: " + BobinkClient.discovering); + debugConsole.appendLog("Discovering: " + Bobink.discovering); } function onCertificateTrustRequested(certInfo) { certTrustDialog.certInfo = certInfo; @@ -59,9 +59,11 @@ ApplicationWindow { title: "Certificate Trust" modal: true standardButtons: Dialog.Yes | Dialog.No - Label { text: certTrustDialog.certInfo } - onAccepted: BobinkClient.acceptCertificate() - onRejected: BobinkClient.rejectCertificate() + Label { + text: certTrustDialog.certInfo + } + onAccepted: Bobink.acceptCertificate() + onRejected: Bobink.rejectCertificate() } ColumnLayout { @@ -75,8 +77,8 @@ ApplicationWindow { initialItem: Page { Component.onCompleted: { - BobinkClient.discoveryUrl = discoveryUrlField.text; - BobinkClient.startDiscovery(); + Bobink.discoveryUrl = discoveryUrlField.text; + Bobink.startDiscovery(); } BobinkAuth { @@ -84,8 +86,8 @@ ApplicationWindow { mode: authModeCombo.currentValue username: usernameField.text password: passwordField.text - certPath: BobinkClient.certFile - keyPath: BobinkClient.keyFile + certPath: Bobink.certFile + keyPath: Bobink.keyFile } FileDialog { @@ -126,22 +128,20 @@ ApplicationWindow { text: "opc.tcp://localhost:4840" } Button { - text: BobinkClient.discovering ? "Stop" : "Discover" + text: Bobink.discovering ? "Stop" : "Discover" onClicked: { - if (BobinkClient.discovering) { - BobinkClient.stopDiscovery(); + if (Bobink.discovering) { + Bobink.stopDiscovery(); } else { - BobinkClient.discoveryUrl = discoveryUrlField.text; - BobinkClient.startDiscovery(); + Bobink.discoveryUrl = discoveryUrlField.text; + Bobink.startDiscovery(); } } } } Label { - text: BobinkClient.discovering - ? "Discovering... (" + BobinkClient.servers.length + " found)" - : BobinkClient.servers.length + " server(s)" + text: Bobink.discovering ? "Discovering... (" + Bobink.servers.length + " found)" : Bobink.servers.length + " server(s)" font.italic: true } @@ -149,7 +149,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.preferredHeight: 100 clip: true - model: BobinkClient.servers + model: Bobink.servers delegate: ItemDelegate { required property var modelData width: ListView.view.width @@ -163,15 +163,18 @@ ApplicationWindow { } RowLayout { - Label { text: "PKI"; font.bold: true } Label { - text: BobinkClient.certFile - ? " (" + BobinkClient.certFile.split("/").pop() + ")" - : " (no certificate found)" + text: "PKI" + font.bold: true + } + Label { + text: Bobink.certFile ? " (" + Bobink.certFile.split("/").pop() + ")" : " (no certificate found)" font.italic: true - color: BobinkClient.certFile ? "green" : "gray" + color: Bobink.certFile ? "green" : "gray" + } + Item { + Layout.fillWidth: true } - Item { Layout.fillWidth: true } Button { text: root.showPkiSettings ? "Hide" : "Configure..." flat: true @@ -184,31 +187,46 @@ ApplicationWindow { Layout.fillWidth: true visible: root.showPkiSettings - Label { text: "Certificate:" } + Label { + text: "Certificate:" + } TextField { id: certFileField Layout.fillWidth: true - text: BobinkClient.certFile + text: Bobink.certFile placeholderText: "Client certificate (.der)" } - Button { text: "Browse..."; onClicked: certFileDialog.open() } + Button { + text: "Browse..." + onClicked: certFileDialog.open() + } - Label { text: "Private key:" } + Label { + text: "Private key:" + } TextField { id: keyFileField Layout.fillWidth: true - text: BobinkClient.keyFile + text: Bobink.keyFile placeholderText: "Private key (.pem, .crt)" } - Button { text: "Browse..."; onClicked: keyFileDialog.open() } + Button { + text: "Browse..." + onClicked: keyFileDialog.open() + } - Label { text: "Trust folder:" } + Label { + text: "Trust folder:" + } TextField { id: trustFolderField Layout.fillWidth: true - text: BobinkClient.pkiDir + text: Bobink.pkiDir + } + Button { + text: "Browse..." + onClicked: trustFolderDialog.open() } - Button { text: "Browse..."; onClicked: trustFolderDialog.open() } } Button { @@ -216,14 +234,17 @@ ApplicationWindow { Layout.fillWidth: true visible: root.showPkiSettings onClicked: { - BobinkClient.pkiDir = trustFolderField.text; - BobinkClient.certFile = certFileField.text; - BobinkClient.keyFile = keyFileField.text; - BobinkClient.applyPki(); + Bobink.pkiDir = trustFolderField.text; + Bobink.certFile = certFileField.text; + Bobink.keyFile = keyFileField.text; + Bobink.applyPki(); } } - Label { text: "Server URL"; font.bold: true } + Label { + text: "Server URL" + font.bold: true + } TextField { id: serverUrlField @@ -231,7 +252,10 @@ ApplicationWindow { placeholderText: "opc.tcp://..." } - Label { text: "Authentication"; font.bold: true } + Label { + text: "Authentication" + font.bold: true + } ComboBox { id: authModeCombo @@ -239,9 +263,18 @@ ApplicationWindow { textRole: "text" valueRole: "mode" model: [ - { text: "Anonymous", mode: BobinkAuth.Anonymous }, - { text: "Username / Password", mode: BobinkAuth.UserPass }, - { text: "Certificate", mode: BobinkAuth.Certificate } + { + text: "Anonymous", + mode: BobinkAuth.Anonymous + }, + { + text: "Username / Password", + mode: BobinkAuth.UserPass + }, + { + text: "Certificate", + mode: BobinkAuth.Certificate + } ] } @@ -250,9 +283,16 @@ ApplicationWindow { visible: authModeCombo.currentValue === BobinkAuth.UserPass Layout.fillWidth: true - Label { text: "Username:" } - TextField { id: usernameField; Layout.fillWidth: true } - Label { text: "Password:" } + Label { + text: "Username:" + } + TextField { + id: usernameField + Layout.fillWidth: true + } + Label { + text: "Password:" + } TextField { id: passwordField Layout.fillWidth: true @@ -265,9 +305,9 @@ ApplicationWindow { Layout.fillWidth: true onClicked: { root.autoConnectFailed = false; - BobinkClient.auth = auth; - BobinkClient.serverUrl = serverUrlField.text; - BobinkClient.connectToServer(); + Bobink.auth = auth; + Bobink.serverUrl = serverUrlField.text; + Bobink.connectToServer(); } } @@ -282,29 +322,51 @@ ApplicationWindow { Layout.fillWidth: true visible: root.autoConnectFailed - Label { text: "Security policy:" } + Label { + text: "Security policy:" + } ComboBox { id: securityPolicyCombo Layout.fillWidth: true textRole: "text" valueRole: "policy" model: [ - { text: "Basic256Sha256", policy: BobinkClient.Basic256Sha256 }, - { text: "Aes128-Sha256-RsaOaep", policy: BobinkClient.Aes128_Sha256_RsaOaep }, - { text: "Aes256-Sha256-RsaPss", policy: BobinkClient.Aes256_Sha256_RsaPss } + { + text: "Basic256Sha256", + policy: Bobink.Basic256Sha256 + }, + { + text: "Aes128-Sha256-RsaOaep", + policy: Bobink.Aes128_Sha256_RsaOaep + }, + { + text: "Aes256-Sha256-RsaPss", + policy: Bobink.Aes256_Sha256_RsaPss + } ] } - Label { text: "Security mode:" } + Label { + text: "Security mode:" + } ComboBox { id: securityModeCombo Layout.fillWidth: true textRole: "text" valueRole: "mode" model: [ - { text: "Sign & Encrypt", mode: BobinkClient.SignAndEncrypt }, - { text: "Sign", mode: BobinkClient.Sign }, - { text: "None", mode: BobinkClient.None } + { + text: "Sign & Encrypt", + mode: Bobink.SignAndEncrypt + }, + { + text: "Sign", + mode: Bobink.Sign + }, + { + text: "None", + mode: Bobink.None + } ] } } @@ -314,14 +376,15 @@ ApplicationWindow { Layout.fillWidth: true visible: root.autoConnectFailed onClicked: { - BobinkClient.auth = auth; - BobinkClient.serverUrl = serverUrlField.text; - BobinkClient.connectDirect(securityPolicyCombo.currentValue, - securityModeCombo.currentValue); + Bobink.auth = auth; + Bobink.serverUrl = serverUrlField.text; + Bobink.connectDirect(securityPolicyCombo.currentValue, securityModeCombo.currentValue); } } - Item { Layout.fillHeight: true } + Item { + Layout.fillHeight: true + } } } } diff --git a/demo/NodePage.qml b/demo/NodePage.qml index e4862d8..fd81db5 100644 --- a/demo/NodePage.qml +++ b/demo/NodePage.qml @@ -46,7 +46,7 @@ Page { Item { Layout.fillWidth: true } Button { text: "Disconnect" - onClicked: BobinkClient.disconnectFromServer() + onClicked: Bobink.disconnectFromServer() } } diff --git a/demo/main.cpp b/demo/main.cpp index b52df9e..6d0b9be 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -5,18 +5,20 @@ #include <QGuiApplication> #include <QQmlApplicationEngine> +#include <QtQml/QQmlExtensionPlugin> +Q_IMPORT_QML_PLUGIN(BobinkPlugin) + int main(int argc, char *argv[]) { // Load the locally-built OpcUa backend plugin (open62541). QCoreApplication::addLibraryPath(QStringLiteral(QTOPCUA_PLUGIN_PATH)); QGuiApplication app(argc, argv); - QQmlApplicationEngine engine; - QObject::connect( - &engine, &QQmlApplicationEngine::objectCreationFailed, - &app, []() { QCoreApplication::exit(1); }, - Qt::QueuedConnection); + QQmlApplicationEngine engine; + QObject::connect ( + &engine, &QQmlApplicationEngine::objectCreationFailed, &app, + [] () { QCoreApplication::exit (1); }, Qt::QueuedConnection); engine.loadFromModule("BobinkDemo", "Main"); return app.exec(); diff --git a/src/BobinkClient.cpp b/src/BobinkClient.cpp index f8303a3..01f9912 100644 --- a/src/BobinkClient.cpp +++ b/src/BobinkClient.cpp @@ -11,8 +11,6 @@ using namespace Qt::Literals::StringLiterals; -BobinkClient *BobinkClient::s_instance = nullptr; - static QString defaultPkiDir () { @@ -36,35 +34,15 @@ BobinkClient::BobinkClient (QObject *parent) : QObject (parent), m_provider (new QOpcUaProvider (this)), m_pkiDir (defaultPkiDir ()) { + // Singleton pattern: construct only once + Q_ASSERT(!s_instance); ensurePkiDirs (m_pkiDir); setupClient (); autoDetectPki (); applyPki (); 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; + s_instance = this; } void diff --git a/src/BobinkClient.h b/src/BobinkClient.h index 5b7b284..3f36e75 100644 --- a/src/BobinkClient.h +++ b/src/BobinkClient.h @@ -25,8 +25,8 @@ class BobinkAuth; class BobinkClient : public QObject { Q_OBJECT - QML_ELEMENT QML_SINGLETON + QML_NAMED_ELEMENT (Bobink) Q_PROPERTY (bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY (QString serverUrl READ serverUrl WRITE setServerUrl NOTIFY @@ -45,16 +45,18 @@ class BobinkClient : public QObject QString keyFile READ keyFile WRITE setKeyFile NOTIFY keyFileChanged) public: - explicit BobinkClient (QObject *parent = nullptr); - ~BobinkClient () override; + BobinkClient (QObject *parent = nullptr); + // ~BobinkClient () override; - static BobinkClient *instance (); + static inline BobinkClient *s_instance = nullptr; + + // static BobinkClient *instance (); /** * @brief QML singleton factory. * @note CppOwnership — lives for the process lifetime. */ - static BobinkClient *create (QQmlEngine *qmlEngine, QJSEngine *jsEngine); + // static BobinkClient *create (QQmlEngine *qmlEngine, QJSEngine *jsEngine); bool connected () const; @@ -155,7 +157,6 @@ private slots: private: void setupClient (); - static BobinkClient *s_instance; QOpcUaProvider *m_provider = nullptr; QOpcUaClient *m_client = nullptr; BobinkAuth *m_auth = nullptr; diff --git a/src/BobinkNode.cpp b/src/BobinkNode.cpp index a39c195..fed87bc 100644 --- a/src/BobinkNode.cpp +++ b/src/BobinkNode.cpp @@ -104,7 +104,7 @@ BobinkNode::componentComplete () QQuickItem::componentComplete (); m_componentComplete = true; - auto *client = BobinkClient::instance (); + auto *client = BobinkClient::s_instance; if (client) connect (client, &BobinkClient::connectedChanged, this, &BobinkNode::handleClientConnectedChanged); @@ -137,7 +137,7 @@ BobinkNode::startMonitoring () if (m_opcuaNode || m_nodeId.isEmpty ()) return; - auto *client = BobinkClient::instance (); + auto *client = BobinkClient::s_instance; if (!client || !client->connected ()) return; @@ -245,11 +245,10 @@ BobinkNode::handleAttributeWritten (QOpcUa::NodeAttribute attr, void BobinkNode::handleClientConnectedChanged () { - auto *client = BobinkClient::instance (); - if (!client) + if (!BobinkClient::s_instance) return; - if (client->connected ()) + if (BobinkClient::s_instance->connected ()) { if (m_componentComplete && isVisible ()) startMonitoring (); @@ -263,8 +262,7 @@ BobinkNode::handleClientConnectedChanged () void BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs) { - auto *client = BobinkClient::instance (); - if (!client || !m_opcuaNode) + if (!BobinkClient::s_instance || !m_opcuaNode) return; for (int bit = 0; bit < 27; ++bit) @@ -276,12 +274,12 @@ BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs) auto sc = m_opcuaNode->attributeError (attr); QLatin1StringView name = nameFromAttribute (attr); if (sc == QOpcUa::UaStatusCode::Good) - emit client->statusMessage ( + emit BobinkClient::s_instance->statusMessage ( QStringLiteral ("Read %1.%2 = %3") .arg (m_nodeId, name, m_opcuaNode->attribute (attr).toString ())); else - emit client->statusMessage ( + emit BobinkClient::s_instance->statusMessage ( QStringLiteral ("Read %1.%2 failed: 0x%3") .arg (m_nodeId, name) .arg (static_cast<quint32> (sc), 8, 16, QLatin1Char ('0'))); @@ -292,15 +290,14 @@ void BobinkNode::handleEnableMonitoringFinished (QOpcUa::NodeAttribute, QOpcUa::UaStatusCode statusCode) { - auto *client = BobinkClient::instance (); - if (!client) + if (!BobinkClient::s_instance) return; if (statusCode == QOpcUa::Good) - emit client->statusMessage ( + emit BobinkClient::s_instance->statusMessage ( QStringLiteral ("Monitoring started: %1").arg (m_nodeId)); else - emit client->statusMessage ( + emit BobinkClient::s_instance->statusMessage ( QStringLiteral ("Monitoring failed for %1: 0x%2") .arg (m_nodeId) .arg (static_cast<quint32> (statusCode), 8, 16, @@ -311,15 +308,14 @@ void BobinkNode::handleDisableMonitoringFinished (QOpcUa::NodeAttribute, QOpcUa::UaStatusCode statusCode) { - auto *client = BobinkClient::instance (); - if (!client) + if (!BobinkClient::s_instance) return; if (statusCode == QOpcUa::Good) - emit client->statusMessage ( + emit BobinkClient::s_instance->statusMessage ( QStringLiteral ("Monitoring stopped: %1").arg (m_nodeId)); else - emit client->statusMessage ( + emit BobinkClient::s_instance->statusMessage ( QStringLiteral ("Stop monitoring failed for %1: 0x%2") .arg (m_nodeId) .arg (static_cast<quint32> (statusCode), 8, 16, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5898132..1292194 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,4 @@ +qt_add_library(BobinkQtOpcUa STATIC) # BobinkQtOpcUa — QML module wrapping QtOpcUa for declarative use. qt_add_qml_module( BobinkQtOpcUa @@ -15,4 +16,4 @@ qt_add_qml_module( OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml/Bobink") -target_link_libraries(BobinkQtOpcUa PRIVATE Qt6::Core Qt6::Quick Qt6::OpcUa) +target_link_libraries(BobinkQtOpcUa PUBLIC Qt6::Core Qt6::Quick Qt6::OpcUa) |
