summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-19 18:00:52 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-19 18:00:52 +0100
commite73fe498e86dbb20d74f8d6ca13b541642676b82 (patch)
tree572593fd880d10ae23544986116847271ee49f99
parent50c62c35463b62a3a7acebf9ebe22d44f1c6dca2 (diff)
downloadBobinkQtOpcUa-e73fe498e86dbb20d74f8d6ca13b541642676b82.tar.gz
BobinkQtOpcUa-e73fe498e86dbb20d74f8d6ca13b541642676b82.zip
Rename QML singleton to Bobink and simplify singleton lifecycle
Replace QML_ELEMENT with QML_NAMED_ELEMENT(Bobink) so QML references use `Bobink` instead of `BobinkClient`. Remove instance()/create() factory in favor of inline s_instance set in the constructor. Import BobinkPlugin statically in demo, link demo to BobinkQtOpcUaplugin, and make library link dependencies PUBLIC. Add .qtcreator to gitignore.
-rw-r--r--.gitignore8
-rw-r--r--demo/CMakeLists.txt2
-rw-r--r--demo/Main.qml191
-rw-r--r--demo/NodePage.qml2
-rw-r--r--demo/main.cpp12
-rw-r--r--src/BobinkClient.cpp28
-rw-r--r--src/BobinkClient.h13
-rw-r--r--src/BobinkNode.cpp30
-rw-r--r--src/CMakeLists.txt3
9 files changed, 168 insertions, 121 deletions
diff --git a/.gitignore b/.gitignore
index ca7b255..625ae33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)