summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/BobinkNode.cpp410
-rw-r--r--src/BobinkNode.h111
-rw-r--r--src/CMakeLists.txt12
-rw-r--r--src/OpcUaAuth.cpp (renamed from src/BobinkAuth.cpp)32
-rw-r--r--src/OpcUaAuth.h (renamed from src/BobinkAuth.h)12
-rw-r--r--src/OpcUaClient.cpp (renamed from src/BobinkClient.cpp)110
-rw-r--r--src/OpcUaClient.h (renamed from src/BobinkClient.h)26
-rw-r--r--src/OpcUaMonitoredNode.cpp513
-rw-r--r--src/OpcUaMonitoredNode.h134
9 files changed, 743 insertions, 617 deletions
diff --git a/src/BobinkNode.cpp b/src/BobinkNode.cpp
deleted file mode 100644
index 55e3b75..0000000
--- a/src/BobinkNode.cpp
+++ /dev/null
@@ -1,410 +0,0 @@
-/**
- * @file BobinkNode.cpp
- * @brief QML component representing a single OPC UA node.
- */
-#include "BobinkNode.h"
-#include "BobinkClient.h"
-
-#include <QOpcUaClient>
-#include <QOpcUaMonitoringParameters>
-
-using namespace Qt::Literals::StringLiterals;
-
-static constexpr double DEFAULT_PUBLISHING_INTERVAL = 250.0; // ms
-
-/* ------------------------------------------------------------------ */
-/* Construction / destruction */
-/* ------------------------------------------------------------------ */
-
-BobinkNode::BobinkNode (QQuickItem *parent) : QQuickItem (parent) {}
-
-BobinkNode::~BobinkNode () { stopMonitoring (); }
-
-/* ------------------------------------------------------------------ */
-/* Properties */
-/* ------------------------------------------------------------------ */
-
-QString
-BobinkNode::nodeId () const
-{
- return m_nodeId;
-}
-
-void
-BobinkNode::setNodeId (const QString &id)
-{
- if (m_nodeId == id)
- return;
- m_nodeId = id;
- emit nodeIdChanged ();
-
- if (m_componentComplete && isVisible ())
- {
- stopMonitoring ();
- startMonitoring ();
- }
-}
-
-QVariant
-BobinkNode::value () const
-{
- return m_value;
-}
-
-void
-BobinkNode::setValue (const QVariant &value)
-{
- if (m_opcuaNode)
- m_opcuaNode->writeValueAttribute (value);
-}
-
-BobinkNode::NodeStatus
-BobinkNode::status () const
-{
- return m_status;
-}
-
-QDateTime
-BobinkNode::sourceTimestamp () const
-{
- return m_sourceTimestamp;
-}
-
-QDateTime
-BobinkNode::serverTimestamp () const
-{
- return m_serverTimestamp;
-}
-
-/* ------------------------------------------------------------------ */
-/* readAttribute */
-/* ------------------------------------------------------------------ */
-
-void
-BobinkNode::readAttribute (const QString &attributeName)
-{
- if (!m_opcuaNode)
- return;
-
- QOpcUa::NodeAttribute attr = attributeFromName (attributeName);
- if (attr == QOpcUa::NodeAttribute::None)
- return;
-
- m_pendingReads.insert (attr, attributeName);
- m_opcuaNode->readAttributes (attr);
-}
-
-/* ------------------------------------------------------------------ */
-/* QQuickItem lifecycle */
-/* ------------------------------------------------------------------ */
-
-void
-BobinkNode::componentComplete ()
-{
- QQuickItem::componentComplete ();
- m_componentComplete = true;
-
- auto *client = BobinkClient::instance ();
- if (client)
- connect (client, &BobinkClient::connectedChanged, this,
- &BobinkNode::handleClientConnectedChanged);
-
- if (isVisible ())
- startMonitoring ();
-}
-
-void
-BobinkNode::itemChange (ItemChange change, const ItemChangeData &data)
-{
- QQuickItem::itemChange (change, data);
-
- if (change == ItemVisibleHasChanged && m_componentComplete)
- {
- if (data.boolValue)
- startMonitoring ();
- else
- stopMonitoring ();
- }
-}
-
-/* ------------------------------------------------------------------ */
-/* Monitoring lifecycle */
-/* ------------------------------------------------------------------ */
-
-void
-BobinkNode::startMonitoring ()
-{
- if (m_opcuaNode || m_nodeId.isEmpty ())
- return;
-
- auto *client = BobinkClient::instance ();
- if (!client || !client->connected ())
- return;
-
- QOpcUaClient *opcua = client->opcuaClient ();
- if (!opcua)
- return;
-
- m_opcuaNode = opcua->node (m_nodeId);
- if (!m_opcuaNode)
- return;
-
- connect (m_opcuaNode, &QOpcUaNode::dataChangeOccurred, this,
- &BobinkNode::handleDataChange);
- connect (m_opcuaNode, &QOpcUaNode::attributeUpdated, this,
- &BobinkNode::handleAttributeUpdated);
- connect (m_opcuaNode, &QOpcUaNode::attributeWritten, this,
- &BobinkNode::handleAttributeWritten);
- connect (m_opcuaNode, &QOpcUaNode::attributeRead, this,
- &BobinkNode::handleAttributeReadFinished);
- connect (m_opcuaNode, &QOpcUaNode::enableMonitoringFinished, this,
- &BobinkNode::handleEnableMonitoringFinished);
- connect (m_opcuaNode, &QOpcUaNode::disableMonitoringFinished, this,
- &BobinkNode::handleDisableMonitoringFinished);
-
- QOpcUaMonitoringParameters params (DEFAULT_PUBLISHING_INTERVAL);
- m_opcuaNode->enableMonitoring (QOpcUa::NodeAttribute::Value, params);
-}
-
-void
-BobinkNode::stopMonitoring ()
-{
- if (!m_opcuaNode)
- return;
-
- m_pendingReads.clear ();
- delete m_opcuaNode;
- m_opcuaNode = nullptr;
-}
-
-/* ------------------------------------------------------------------ */
-/* Signal handlers */
-/* ------------------------------------------------------------------ */
-
-void
-BobinkNode::handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val)
-{
- if (attr != QOpcUa::NodeAttribute::Value)
- return;
-
- if (m_value != val)
- {
- m_value = val;
- emit valueChanged ();
- }
-
- NodeStatus newStatus = statusFromCode (m_opcuaNode->valueAttributeError ());
- if (m_status != newStatus)
- {
- m_status = newStatus;
- emit statusChanged ();
- }
-
- QDateTime srcTs
- = m_opcuaNode->sourceTimestamp (QOpcUa::NodeAttribute::Value);
- if (m_sourceTimestamp != srcTs)
- {
- m_sourceTimestamp = srcTs;
- emit sourceTimestampChanged ();
- }
-
- QDateTime srvTs
- = m_opcuaNode->serverTimestamp (QOpcUa::NodeAttribute::Value);
- if (m_serverTimestamp != srvTs)
- {
- m_serverTimestamp = srvTs;
- emit serverTimestampChanged ();
- }
-}
-
-void
-BobinkNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
- const QVariant &val)
-{
- auto it = m_pendingReads.find (attr);
- if (it != m_pendingReads.end ())
- {
- emit attributeRead (it.value (), val);
- m_pendingReads.erase (it);
- }
-}
-
-void
-BobinkNode::handleAttributeWritten (QOpcUa::NodeAttribute attr,
- QOpcUa::UaStatusCode statusCode)
-{
- if (attr != QOpcUa::NodeAttribute::Value)
- return;
-
- if (statusCode != QOpcUa::UaStatusCode::Good)
- emit writeError (QStringLiteral ("Write failed: 0x%1")
- .arg (static_cast<quint32> (statusCode), 8, 16,
- QLatin1Char ('0')));
-}
-
-void
-BobinkNode::handleClientConnectedChanged ()
-{
- if (!BobinkClient::instance ())
- return;
-
- if (BobinkClient::instance ()->connected ())
- {
- if (m_componentComplete && isVisible ())
- startMonitoring ();
- }
- else
- {
- stopMonitoring ();
- }
-}
-
-void
-BobinkNode::handleAttributeReadFinished (QOpcUa::NodeAttributes attrs)
-{
- if (!BobinkClient::instance () || !m_opcuaNode)
- return;
-
- for (int bit = 0; bit < 27; ++bit)
- {
- auto attr = static_cast<QOpcUa::NodeAttribute> (1 << bit);
- if (!attrs.testFlag (attr))
- continue;
-
- auto sc = m_opcuaNode->attributeError (attr);
- QLatin1StringView name = nameFromAttribute (attr);
- if (sc == QOpcUa::UaStatusCode::Good)
- emit BobinkClient::instance () -> statusMessage (
- QStringLiteral ("Read %1.%2 = %3")
- .arg (m_nodeId, name,
- m_opcuaNode->attribute (attr).toString ()));
- else
- emit BobinkClient::instance () -> statusMessage (
- QStringLiteral ("Read %1.%2 failed: 0x%3")
- .arg (m_nodeId, name)
- .arg (static_cast<quint32> (sc), 8, 16, QLatin1Char ('0')));
- }
-}
-
-void
-BobinkNode::handleEnableMonitoringFinished (QOpcUa::NodeAttribute,
- QOpcUa::UaStatusCode statusCode)
-{
- if (!BobinkClient::instance ())
- return;
-
- if (statusCode == QOpcUa::Good)
- emit BobinkClient::instance () -> statusMessage (
- QStringLiteral ("Monitoring started: %1").arg (m_nodeId));
- else
- emit BobinkClient::instance ()
- -> statusMessage (QStringLiteral ("Monitoring failed for %1: 0x%2")
- .arg (m_nodeId)
- .arg (static_cast<quint32> (statusCode), 8, 16,
- QLatin1Char ('0')));
-}
-
-void
-BobinkNode::handleDisableMonitoringFinished (QOpcUa::NodeAttribute,
- QOpcUa::UaStatusCode statusCode)
-{
- if (!BobinkClient::instance ())
- return;
-
- if (statusCode == QOpcUa::Good)
- emit BobinkClient::instance () -> statusMessage (
- QStringLiteral ("Monitoring stopped: %1").arg (m_nodeId));
- else
- emit BobinkClient::instance () -> statusMessage (
- QStringLiteral ("Stop monitoring failed for %1: 0x%2")
- .arg (m_nodeId)
- .arg (static_cast<quint32> (statusCode), 8, 16,
- QLatin1Char ('0')));
-}
-
-/* ------------------------------------------------------------------ */
-/* Helpers */
-/* ------------------------------------------------------------------ */
-
-BobinkNode::NodeStatus
-BobinkNode::statusFromCode (QOpcUa::UaStatusCode code)
-{
- quint32 severity = static_cast<quint32> (code) >> 30;
- switch (severity)
- {
- case 0:
- return Good;
- case 1:
- return Uncertain;
- default:
- return Bad;
- }
-}
-
-QOpcUa::NodeAttribute
-BobinkNode::attributeFromName (const QString &name)
-{
- static const QHash<QString, QOpcUa::NodeAttribute> map = {
- { QStringLiteral ("NodeId"), QOpcUa::NodeAttribute::NodeId },
- { QStringLiteral ("NodeClass"), QOpcUa::NodeAttribute::NodeClass },
- { QStringLiteral ("BrowseName"), QOpcUa::NodeAttribute::BrowseName },
- { QStringLiteral ("DisplayName"), QOpcUa::NodeAttribute::DisplayName },
- { QStringLiteral ("Description"), QOpcUa::NodeAttribute::Description },
- { QStringLiteral ("Value"), QOpcUa::NodeAttribute::Value },
- { QStringLiteral ("DataType"), QOpcUa::NodeAttribute::DataType },
- { QStringLiteral ("ValueRank"), QOpcUa::NodeAttribute::ValueRank },
- { QStringLiteral ("ArrayDimensions"),
- QOpcUa::NodeAttribute::ArrayDimensions },
- { QStringLiteral ("AccessLevel"), QOpcUa::NodeAttribute::AccessLevel },
- { QStringLiteral ("UserAccessLevel"),
- QOpcUa::NodeAttribute::UserAccessLevel },
- { QStringLiteral ("MinimumSamplingInterval"),
- QOpcUa::NodeAttribute::MinimumSamplingInterval },
- { QStringLiteral ("Historizing"), QOpcUa::NodeAttribute::Historizing },
- { QStringLiteral ("Executable"), QOpcUa::NodeAttribute::Executable },
- { QStringLiteral ("UserExecutable"),
- QOpcUa::NodeAttribute::UserExecutable },
- };
-
- return map.value (name, QOpcUa::NodeAttribute::None);
-}
-
-QLatin1StringView
-BobinkNode::nameFromAttribute (QOpcUa::NodeAttribute attr)
-{
- switch (attr)
- {
- case QOpcUa::NodeAttribute::NodeId:
- return "NodeId"_L1;
- case QOpcUa::NodeAttribute::NodeClass:
- return "NodeClass"_L1;
- case QOpcUa::NodeAttribute::BrowseName:
- return "BrowseName"_L1;
- case QOpcUa::NodeAttribute::DisplayName:
- return "DisplayName"_L1;
- case QOpcUa::NodeAttribute::Description:
- return "Description"_L1;
- case QOpcUa::NodeAttribute::Value:
- return "Value"_L1;
- case QOpcUa::NodeAttribute::DataType:
- return "DataType"_L1;
- case QOpcUa::NodeAttribute::ValueRank:
- return "ValueRank"_L1;
- case QOpcUa::NodeAttribute::ArrayDimensions:
- return "ArrayDimensions"_L1;
- case QOpcUa::NodeAttribute::AccessLevel:
- return "AccessLevel"_L1;
- case QOpcUa::NodeAttribute::UserAccessLevel:
- return "UserAccessLevel"_L1;
- case QOpcUa::NodeAttribute::MinimumSamplingInterval:
- return "MinimumSamplingInterval"_L1;
- case QOpcUa::NodeAttribute::Historizing:
- return "Historizing"_L1;
- case QOpcUa::NodeAttribute::Executable:
- return "Executable"_L1;
- case QOpcUa::NodeAttribute::UserExecutable:
- return "UserExecutable"_L1;
- default:
- return "Unknown"_L1;
- }
-}
diff --git a/src/BobinkNode.h b/src/BobinkNode.h
deleted file mode 100644
index c37a883..0000000
--- a/src/BobinkNode.h
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * @file BobinkNode.h
- * @brief QML component representing a single OPC UA node.
- *
- * Inherits QQuickItem so that monitoring is automatically tied to
- * visibility: when the item (or any ancestor) becomes invisible
- * (e.g. StackView navigates away, Loader unloads), the monitored
- * item is removed from the subscription. When visible again,
- * monitoring resumes.
- */
-#ifndef BOBINKNODE_H
-#define BOBINKNODE_H
-
-#include <QDateTime>
-#include <QHash>
-#include <QOpcUaNode>
-#include <QQuickItem>
-
-class BobinkNode : public QQuickItem
-{
- Q_OBJECT
- QML_ELEMENT
-
- Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged)
- Q_PROPERTY (QVariant value READ value WRITE setValue NOTIFY valueChanged)
- Q_PROPERTY (NodeStatus status READ status NOTIFY statusChanged)
- Q_PROPERTY (QDateTime sourceTimestamp READ sourceTimestamp NOTIFY
- sourceTimestampChanged)
- Q_PROPERTY (QDateTime serverTimestamp READ serverTimestamp NOTIFY
- serverTimestampChanged)
-
-public:
- explicit BobinkNode (QQuickItem *parent = nullptr);
- ~BobinkNode () override;
-
- /// Simplified OPC UA status severity.
- enum NodeStatus
- {
- Good,
- Uncertain,
- Bad
- };
- Q_ENUM (NodeStatus)
-
- QString nodeId () const;
- void setNodeId (const QString &id);
-
- QVariant value () const;
- /** Setting value writes to the server; the property updates when
- * the server confirms via the monitored data change. */
- void setValue (const QVariant &value);
-
- NodeStatus status () const;
- QDateTime sourceTimestamp () const;
- QDateTime serverTimestamp () const;
-
- /** Read an attribute on demand (DisplayName, Description, DataType, …).
- * Result arrives asynchronously via attributeRead(). */
- Q_INVOKABLE void readAttribute (const QString &attributeName);
-
-signals:
- void nodeIdChanged ();
- void valueChanged ();
- void statusChanged ();
- void sourceTimestampChanged ();
- void serverTimestampChanged ();
-
- /** Emitted when a readAttribute() call completes. */
- void attributeRead (const QString &attributeName, const QVariant &value);
-
- /** Emitted when a write to the server fails. */
- void writeError (const QString &message);
-
-protected:
- void componentComplete () override;
- void itemChange (ItemChange change, const ItemChangeData &data) override;
-
-private:
- void startMonitoring ();
- void stopMonitoring ();
-
- void handleDataChange (QOpcUa::NodeAttribute attr, const QVariant &val);
- void handleAttributeUpdated (QOpcUa::NodeAttribute attr,
- const QVariant &val);
- void handleAttributeWritten (QOpcUa::NodeAttribute attr,
- QOpcUa::UaStatusCode statusCode);
- void handleClientConnectedChanged ();
- void handleAttributeReadFinished (QOpcUa::NodeAttributes attrs);
- void handleEnableMonitoringFinished (QOpcUa::NodeAttribute attr,
- QOpcUa::UaStatusCode statusCode);
- void handleDisableMonitoringFinished (QOpcUa::NodeAttribute attr,
- QOpcUa::UaStatusCode statusCode);
-
- static NodeStatus statusFromCode (QOpcUa::UaStatusCode code);
- static QOpcUa::NodeAttribute attributeFromName (const QString &name);
- static QLatin1StringView nameFromAttribute (QOpcUa::NodeAttribute attr);
-
- QString m_nodeId;
- QVariant m_value;
- NodeStatus m_status = Bad;
- QDateTime m_sourceTimestamp;
- QDateTime m_serverTimestamp;
-
- QOpcUaNode *m_opcuaNode = nullptr;
- bool m_componentComplete = false;
-
- /** Tracks in-flight readAttribute() requests (enum → original name). */
- QHash<QOpcUa::NodeAttribute, QString> m_pendingReads;
-};
-
-#endif // BOBINKNODE_H
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1292194..b0781cd 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -7,12 +7,12 @@ qt_add_qml_module(
VERSION
1.0
SOURCES
- BobinkAuth.h
- BobinkAuth.cpp
- BobinkClient.h
- BobinkClient.cpp
- BobinkNode.h
- BobinkNode.cpp
+ OpcUaAuth.h
+ OpcUaAuth.cpp
+ OpcUaClient.h
+ OpcUaClient.cpp
+ OpcUaMonitoredNode.h
+ OpcUaMonitoredNode.cpp
OUTPUT_DIRECTORY
"${CMAKE_BINARY_DIR}/qml/Bobink")
diff --git a/src/BobinkAuth.cpp b/src/OpcUaAuth.cpp
index 132e4be..258c9c7 100644
--- a/src/BobinkAuth.cpp
+++ b/src/OpcUaAuth.cpp
@@ -1,19 +1,19 @@
/**
- * @file BobinkAuth.cpp
- * @brief BobinkAuth implementation.
+ * @file OpcUaAuth.cpp
+ * @brief OpcUaAuth implementation.
*/
-#include "BobinkAuth.h"
+#include "OpcUaAuth.h"
-BobinkAuth::BobinkAuth (QObject *parent) : QObject (parent) {}
+OpcUaAuth::OpcUaAuth (QObject *parent) : QObject (parent) {}
-BobinkAuth::AuthMode
-BobinkAuth::mode () const
+OpcUaAuth::AuthMode
+OpcUaAuth::mode () const
{
return m_mode;
}
void
-BobinkAuth::setMode (AuthMode mode)
+OpcUaAuth::setMode (AuthMode mode)
{
if (m_mode == mode)
return;
@@ -22,13 +22,13 @@ BobinkAuth::setMode (AuthMode mode)
}
QString
-BobinkAuth::username () const
+OpcUaAuth::username () const
{
return m_username;
}
void
-BobinkAuth::setUsername (const QString &username)
+OpcUaAuth::setUsername (const QString &username)
{
if (m_username == username)
return;
@@ -37,13 +37,13 @@ BobinkAuth::setUsername (const QString &username)
}
QString
-BobinkAuth::password () const
+OpcUaAuth::password () const
{
return m_password;
}
void
-BobinkAuth::setPassword (const QString &password)
+OpcUaAuth::setPassword (const QString &password)
{
if (m_password == password)
return;
@@ -52,13 +52,13 @@ BobinkAuth::setPassword (const QString &password)
}
QString
-BobinkAuth::certPath () const
+OpcUaAuth::certPath () const
{
return m_certPath;
}
void
-BobinkAuth::setCertPath (const QString &path)
+OpcUaAuth::setCertPath (const QString &path)
{
if (m_certPath == path)
return;
@@ -67,13 +67,13 @@ BobinkAuth::setCertPath (const QString &path)
}
QString
-BobinkAuth::keyPath () const
+OpcUaAuth::keyPath () const
{
return m_keyPath;
}
void
-BobinkAuth::setKeyPath (const QString &path)
+OpcUaAuth::setKeyPath (const QString &path)
{
if (m_keyPath == path)
return;
@@ -82,7 +82,7 @@ BobinkAuth::setKeyPath (const QString &path)
}
QOpcUaAuthenticationInformation
-BobinkAuth::toAuthenticationInformation () const
+OpcUaAuth::toAuthenticationInformation () const
{
QOpcUaAuthenticationInformation info;
switch (m_mode)
diff --git a/src/BobinkAuth.h b/src/OpcUaAuth.h
index 2bd3c05..8d53c1d 100644
--- a/src/BobinkAuth.h
+++ b/src/OpcUaAuth.h
@@ -1,15 +1,15 @@
/**
- * @file BobinkAuth.h
+ * @file OpcUaAuth.h
* @brief QML component for OPC UA authentication configuration.
*/
-#ifndef BOBINKAUTH_H
-#define BOBINKAUTH_H
+#ifndef OPCUAAUTH_H
+#define OPCUAAUTH_H
#include <QObject>
#include <QOpcUaAuthenticationInformation>
#include <QQmlEngine>
-class BobinkAuth : public QObject
+class OpcUaAuth : public QObject
{
Q_OBJECT
QML_ELEMENT
@@ -34,7 +34,7 @@ public:
};
Q_ENUM (AuthMode)
- explicit BobinkAuth (QObject *parent = nullptr);
+ explicit OpcUaAuth (QObject *parent = nullptr);
AuthMode mode () const;
void setMode (AuthMode mode);
@@ -72,4 +72,4 @@ private:
QString m_keyPath;
};
-#endif // BOBINKAUTH_H
+#endif // OPCUAAUTH_H
diff --git a/src/BobinkClient.cpp b/src/OpcUaClient.cpp
index 41b7dbf..91ce47b 100644
--- a/src/BobinkClient.cpp
+++ b/src/OpcUaClient.cpp
@@ -1,9 +1,9 @@
/**
- * @file BobinkClient.cpp
- * @brief BobinkClient implementation.
+ * @file OpcUaClient.cpp
+ * @brief OpcUaClient implementation.
*/
-#include "BobinkClient.h"
-#include "BobinkAuth.h"
+#include "OpcUaClient.h"
+#include "OpcUaAuth.h"
#include <QDir>
#include <QMetaEnum>
@@ -35,17 +35,17 @@ ensurePkiDirs (const QString &base)
}
static QString
-securityPolicyUri (BobinkClient::SecurityPolicy policy)
+securityPolicyUri (OpcUaClient::SecurityPolicy policy)
{
switch (policy)
{
- case BobinkClient::Basic256Sha256:
+ case OpcUaClient::Basic256Sha256:
return QStringLiteral (
"http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
- case BobinkClient::Aes128_Sha256_RsaOaep:
+ case OpcUaClient::Aes128_Sha256_RsaOaep:
return QStringLiteral (
"http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep");
- case BobinkClient::Aes256_Sha256_RsaPss:
+ case OpcUaClient::Aes256_Sha256_RsaPss:
return QStringLiteral (
"http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss");
}
@@ -56,7 +56,7 @@ securityPolicyUri (BobinkClient::SecurityPolicy policy)
* Construction
* ====================================== */
-BobinkClient::BobinkClient (QObject *parent)
+OpcUaClient::OpcUaClient (QObject *parent)
: QObject (parent), m_provider (new QOpcUaProvider (this)),
m_pkiDir (defaultPkiDir ())
{
@@ -66,56 +66,56 @@ BobinkClient::BobinkClient (QObject *parent)
autoDetectPki ();
applyPki ();
connect (&m_discoveryTimer, &QTimer::timeout, this,
- &BobinkClient::doDiscovery);
+ &OpcUaClient::doDiscovery);
s_instance = this;
}
void
-BobinkClient::setupClient ()
+OpcUaClient::setupClient ()
{
m_client = m_provider->createClient (QStringLiteral ("open62541"));
if (!m_client)
{
- qWarning () << "BobinkClient: failed to create open62541 backend";
+ qWarning () << "OpcUaClient: failed to create open62541 backend";
return;
}
connect (m_client, &QOpcUaClient::stateChanged, this,
- &BobinkClient::handleStateChanged);
+ &OpcUaClient::handleStateChanged);
connect (m_client, &QOpcUaClient::endpointsRequestFinished, this,
- &BobinkClient::handleEndpointsReceived);
+ &OpcUaClient::handleEndpointsReceived);
connect (m_client, &QOpcUaClient::connectError, this,
- &BobinkClient::handleConnectError);
+ &OpcUaClient::handleConnectError);
connect (m_client, &QOpcUaClient::findServersFinished, this,
- &BobinkClient::handleFindServersFinished);
+ &OpcUaClient::handleFindServersFinished);
connect (m_client, &QOpcUaClient::errorChanged, this,
- &BobinkClient::handleClientError);
+ &OpcUaClient::handleClientError);
}
/* ======================================
* Connection
* ====================================== */
-BobinkClient *
-BobinkClient::instance ()
+OpcUaClient *
+OpcUaClient::instance ()
{
return s_instance;
}
bool
-BobinkClient::connected () const
+OpcUaClient::connected () const
{
return m_connected;
}
QString
-BobinkClient::serverUrl () const
+OpcUaClient::serverUrl () const
{
return m_serverUrl;
}
void
-BobinkClient::setServerUrl (const QString &url)
+OpcUaClient::setServerUrl (const QString &url)
{
if (m_serverUrl == url)
return;
@@ -123,14 +123,14 @@ BobinkClient::setServerUrl (const QString &url)
emit serverUrlChanged ();
}
-BobinkAuth *
-BobinkClient::auth () const
+OpcUaAuth *
+OpcUaClient::auth () const
{
return m_auth;
}
void
-BobinkClient::setAuth (BobinkAuth *auth)
+OpcUaClient::setAuth (OpcUaAuth *auth)
{
if (m_auth == auth)
return;
@@ -139,13 +139,13 @@ BobinkClient::setAuth (BobinkAuth *auth)
}
QOpcUaClient *
-BobinkClient::opcuaClient () const
+OpcUaClient::opcuaClient () const
{
return m_client;
}
void
-BobinkClient::connectToServer ()
+OpcUaClient::connectToServer ()
{
if (!m_client)
{
@@ -175,7 +175,7 @@ BobinkClient::connectToServer ()
}
void
-BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode)
+OpcUaClient::connectDirect (SecurityPolicy policy, SecurityMode mode)
{
if (!m_client)
{
@@ -205,15 +205,15 @@ BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode)
{
switch (m_auth->mode ())
{
- case BobinkAuth::Anonymous:
+ case OpcUaAuth::Anonymous:
tokenPolicy.setTokenType (
QOpcUaUserTokenPolicy::TokenType::Anonymous);
break;
- case BobinkAuth::UserPass:
+ case OpcUaAuth::UserPass:
tokenPolicy.setTokenType (
QOpcUaUserTokenPolicy::TokenType::Username);
break;
- case BobinkAuth::Certificate:
+ case OpcUaAuth::Certificate:
tokenPolicy.setTokenType (
QOpcUaUserTokenPolicy::TokenType::Certificate);
break;
@@ -231,14 +231,14 @@ BobinkClient::connectDirect (SecurityPolicy policy, SecurityMode mode)
}
void
-BobinkClient::disconnectFromServer ()
+OpcUaClient::disconnectFromServer ()
{
if (m_client)
m_client->disconnectFromEndpoint ();
}
void
-BobinkClient::acceptCertificate ()
+OpcUaClient::acceptCertificate ()
{
m_certAccepted = true;
if (m_certLoop)
@@ -246,7 +246,7 @@ BobinkClient::acceptCertificate ()
}
void
-BobinkClient::rejectCertificate ()
+OpcUaClient::rejectCertificate ()
{
m_certAccepted = false;
if (m_certLoop)
@@ -254,7 +254,7 @@ BobinkClient::rejectCertificate ()
}
void
-BobinkClient::handleStateChanged (QOpcUaClient::ClientState state)
+OpcUaClient::handleStateChanged (QOpcUaClient::ClientState state)
{
bool nowConnected = (state == QOpcUaClient::Connected);
if (m_connected != nowConnected)
@@ -265,7 +265,7 @@ BobinkClient::handleStateChanged (QOpcUaClient::ClientState state)
}
void
-BobinkClient::handleEndpointsReceived (
+OpcUaClient::handleEndpointsReceived (
const QList<QOpcUaEndpointDescription> &endpoints,
QOpcUa::UaStatusCode statusCode, const QUrl &)
{
@@ -291,7 +291,7 @@ BobinkClient::handleEndpointsReceived (
}
void
-BobinkClient::handleConnectError (QOpcUaErrorState *errorState)
+OpcUaClient::handleConnectError (QOpcUaErrorState *errorState)
{
if (errorState->connectionStep ()
== QOpcUaErrorState::ConnectionStep::CertificateValidation)
@@ -324,7 +324,7 @@ BobinkClient::handleConnectError (QOpcUaErrorState *errorState)
}
void
-BobinkClient::handleClientError (QOpcUaClient::ClientError error)
+OpcUaClient::handleClientError (QOpcUaClient::ClientError error)
{
if (error == QOpcUaClient::NoError)
return;
@@ -340,13 +340,13 @@ BobinkClient::handleClientError (QOpcUaClient::ClientError error)
* ====================================== */
QString
-BobinkClient::discoveryUrl () const
+OpcUaClient::discoveryUrl () const
{
return m_discoveryUrl;
}
void
-BobinkClient::setDiscoveryUrl (const QString &url)
+OpcUaClient::setDiscoveryUrl (const QString &url)
{
if (m_discoveryUrl == url)
return;
@@ -355,25 +355,25 @@ BobinkClient::setDiscoveryUrl (const QString &url)
}
bool
-BobinkClient::discovering () const
+OpcUaClient::discovering () const
{
return m_discovering;
}
const QList<QOpcUaApplicationDescription> &
-BobinkClient::discoveredServers () const
+OpcUaClient::discoveredServers () const
{
return m_discoveredServers;
}
QVariantList
-BobinkClient::servers () const
+OpcUaClient::servers () const
{
return m_serversCache;
}
void
-BobinkClient::startDiscovery ()
+OpcUaClient::startDiscovery ()
{
if (m_discoveryUrl.isEmpty () || !m_client)
return;
@@ -389,7 +389,7 @@ BobinkClient::startDiscovery ()
}
void
-BobinkClient::stopDiscovery ()
+OpcUaClient::stopDiscovery ()
{
m_discoveryTimer.stop ();
@@ -401,7 +401,7 @@ BobinkClient::stopDiscovery ()
}
void
-BobinkClient::doDiscovery ()
+OpcUaClient::doDiscovery ()
{
if (!m_client || m_discoveryUrl.isEmpty ())
return;
@@ -412,7 +412,7 @@ BobinkClient::doDiscovery ()
}
void
-BobinkClient::handleFindServersFinished (
+OpcUaClient::handleFindServersFinished (
const QList<QOpcUaApplicationDescription> &servers,
QOpcUa::UaStatusCode statusCode, const QUrl &)
{
@@ -438,13 +438,13 @@ BobinkClient::handleFindServersFinished (
* ====================================== */
QString
-BobinkClient::pkiDir () const
+OpcUaClient::pkiDir () const
{
return m_pkiDir;
}
void
-BobinkClient::setPkiDir (const QString &path)
+OpcUaClient::setPkiDir (const QString &path)
{
if (m_pkiDir == path)
return;
@@ -454,13 +454,13 @@ BobinkClient::setPkiDir (const QString &path)
}
QString
-BobinkClient::certFile () const
+OpcUaClient::certFile () const
{
return m_certFile;
}
void
-BobinkClient::setCertFile (const QString &path)
+OpcUaClient::setCertFile (const QString &path)
{
if (m_certFile == path)
return;
@@ -469,13 +469,13 @@ BobinkClient::setCertFile (const QString &path)
}
QString
-BobinkClient::keyFile () const
+OpcUaClient::keyFile () const
{
return m_keyFile;
}
void
-BobinkClient::setKeyFile (const QString &path)
+OpcUaClient::setKeyFile (const QString &path)
{
if (m_keyFile == path)
return;
@@ -484,7 +484,7 @@ BobinkClient::setKeyFile (const QString &path)
}
void
-BobinkClient::autoDetectPki ()
+OpcUaClient::autoDetectPki ()
{
if (m_pkiDir.isEmpty ())
return;
@@ -503,7 +503,7 @@ BobinkClient::autoDetectPki ()
}
void
-BobinkClient::applyPki ()
+OpcUaClient::applyPki ()
{
if (!m_client || m_pkiDir.isEmpty ())
return;
diff --git a/src/BobinkClient.h b/src/OpcUaClient.h
index 7eb6c3c..1476911 100644
--- a/src/BobinkClient.h
+++ b/src/OpcUaClient.h
@@ -1,13 +1,13 @@
/**
- * @file BobinkClient.h
+ * @file OpcUaClient.h
* @brief QML singleton managing the OPC UA connection lifecycle.
*
* Wraps QOpcUaClient into a declarative interface: LDS discovery,
* endpoint selection, PKI, and certificate trust flow.
* Single connection at a time (app-wide singleton).
*/
-#ifndef BOBINKCLIENT_H
-#define BOBINKCLIENT_H
+#ifndef OPCUACLIENT_H
+#define OPCUACLIENT_H
#include <QEventLoop>
#include <QObject>
@@ -19,9 +19,9 @@
#include <QQmlEngine>
#include <QTimer>
-class BobinkAuth;
+class OpcUaAuth;
-class BobinkClient : public QObject
+class OpcUaClient : public QObject
{
Q_OBJECT
QML_SINGLETON
@@ -31,7 +31,7 @@ class BobinkClient : public QObject
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 (OpcUaAuth *auth READ auth WRITE setAuth NOTIFY authChanged)
/* -- Discovery -- */
Q_PROPERTY (QString discoveryUrl READ discoveryUrl WRITE setDiscoveryUrl
@@ -65,7 +65,7 @@ public:
};
Q_ENUM (SecurityPolicy)
- BobinkClient (QObject *parent = nullptr);
+ OpcUaClient (QObject *parent = nullptr);
/* -- Connection -- */
@@ -74,8 +74,8 @@ public:
QString serverUrl () const;
void setServerUrl (const QString &url);
- BobinkAuth *auth () const;
- void setAuth (BobinkAuth *auth);
+ OpcUaAuth *auth () const;
+ void setAuth (OpcUaAuth *auth);
/** @brief Discover endpoints, pick the most secure, connect. */
Q_INVOKABLE void connectToServer ();
@@ -119,7 +119,7 @@ public:
/* -- C++ only -- */
- static BobinkClient *instance ();
+ static OpcUaClient *instance ();
QOpcUaClient *opcuaClient () const;
signals:
@@ -169,7 +169,7 @@ private:
/* -- Connection -- */
QOpcUaProvider *m_provider = nullptr;
QOpcUaClient *m_client = nullptr;
- BobinkAuth *m_auth = nullptr;
+ OpcUaAuth *m_auth = nullptr;
QString m_serverUrl;
bool m_connected = false;
QEventLoop *m_certLoop = nullptr;
@@ -187,7 +187,7 @@ private:
QString m_certFile;
QString m_keyFile;
- static inline BobinkClient *s_instance = nullptr;
+ static inline OpcUaClient *s_instance = nullptr;
};
-#endif // BOBINKCLIENT_H
+#endif // OPCUACLIENT_H
diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp
new file mode 100644
index 0000000..cd93730
--- /dev/null
+++ b/src/OpcUaMonitoredNode.cpp
@@ -0,0 +1,513 @@
+/**
+ * @file OpcUaMonitoredNode.cpp
+ * @brief OpcUaMonitoredNode implementation.
+ */
+#include "OpcUaMonitoredNode.h"
+#include "OpcUaClient.h"
+
+#include <QMetaEnum>
+#include <QOpcUaLocalizedText>
+#include <QOpcUaMonitoringParameters>
+#include <QStringList>
+#include <QUuid>
+
+OpcUaMonitoredNode::OpcUaMonitoredNode (QObject *parent) : QObject (parent) {}
+
+/* ======================================
+ * Properties
+ * ====================================== */
+
+QString
+OpcUaMonitoredNode::nodeId () const
+{
+ return m_nodeId;
+}
+
+void
+OpcUaMonitoredNode::setNodeId (const QString &id)
+{
+ if (m_nodeId == id)
+ return;
+ m_nodeId = id;
+ emit nodeIdChanged ();
+
+ if (!m_componentComplete)
+ return;
+
+ teardownNode ();
+ auto *client = OpcUaClient::instance ();
+ if (client && client->connected ())
+ setupNode ();
+}
+
+bool
+OpcUaMonitoredNode::monitored () const
+{
+ return m_monitored;
+}
+
+void
+OpcUaMonitoredNode::setMonitored (bool monitored)
+{
+ if (m_monitored == monitored)
+ return;
+ m_monitored = monitored;
+ emit monitoredChanged ();
+
+ if (!m_componentComplete || !m_node)
+ return;
+
+ if (m_monitored)
+ startMonitoring ();
+ else
+ stopMonitoring ();
+}
+
+double
+OpcUaMonitoredNode::publishingInterval () const
+{
+ return m_publishingInterval;
+}
+
+void
+OpcUaMonitoredNode::setPublishingInterval (double interval)
+{
+ if (qFuzzyCompare (m_publishingInterval, interval))
+ return;
+ m_publishingInterval = interval;
+ emit publishingIntervalChanged ();
+}
+
+QVariant
+OpcUaMonitoredNode::value () const
+{
+ return m_value;
+}
+
+bool
+OpcUaMonitoredNode::writable () const
+{
+ return m_writable;
+}
+
+OpcUaNodeInfo
+OpcUaMonitoredNode::info () const
+{
+ return m_info;
+}
+
+/* ======================================
+ * QQmlParserStatus
+ * ====================================== */
+
+void
+OpcUaMonitoredNode::classBegin ()
+{
+}
+
+void
+OpcUaMonitoredNode::componentComplete ()
+{
+ m_componentComplete = true;
+
+ auto *client = OpcUaClient::instance ();
+ if (!client)
+ return;
+
+ connect (client, &OpcUaClient::connectedChanged, this,
+ &OpcUaMonitoredNode::handleClientConnectedChanged);
+
+ if (client->connected () && !m_nodeId.isEmpty ())
+ setupNode ();
+}
+
+/* ======================================
+ * Node lifecycle
+ * ====================================== */
+
+void
+OpcUaMonitoredNode::setupNode ()
+{
+ if (m_node || m_nodeId.isEmpty ())
+ return;
+
+ auto *client = OpcUaClient::instance ();
+ if (!client || !client->connected ())
+ return;
+
+ m_node = client->opcuaClient ()->node (m_nodeId);
+ if (!m_node)
+ return;
+
+ m_node->setParent (this);
+
+ connect (m_node, &QOpcUaNode::attributeUpdated, this,
+ &OpcUaMonitoredNode::handleAttributeUpdated);
+ connect (m_node, &QOpcUaNode::valueAttributeUpdated, this,
+ &OpcUaMonitoredNode::handleValueUpdated);
+ connect (m_node, &QOpcUaNode::attributeWritten, this,
+ &OpcUaMonitoredNode::handleAttributeWritten);
+ connect (m_node, &QOpcUaNode::enableMonitoringFinished, this,
+ &OpcUaMonitoredNode::handleEnableMonitoringFinished);
+ connect (m_node, &QOpcUaNode::disableMonitoringFinished, this,
+ &OpcUaMonitoredNode::handleDisableMonitoringFinished);
+
+ m_node->readAttributes (
+ QOpcUa::NodeAttribute::DisplayName | QOpcUa::NodeAttribute::Description
+ | QOpcUa::NodeAttribute::NodeClass | QOpcUa::NodeAttribute::DataType
+ | QOpcUa::NodeAttribute::AccessLevel);
+
+ if (m_monitored)
+ startMonitoring ();
+}
+
+void
+OpcUaMonitoredNode::teardownNode ()
+{
+ if (!m_node)
+ return;
+
+ delete m_node;
+ m_node = nullptr;
+
+ m_value.clear ();
+ emit valueChanged ();
+
+ m_info = OpcUaNodeInfo{};
+ emit infoChanged ();
+
+ m_valueType = QOpcUa::Types::Undefined;
+
+ if (m_writable)
+ {
+ m_writable = false;
+ emit writableChanged ();
+ }
+}
+
+void
+OpcUaMonitoredNode::startMonitoring ()
+{
+ if (!m_node || !m_monitored)
+ return;
+ QOpcUaMonitoringParameters params (m_publishingInterval);
+ m_node->enableMonitoring (QOpcUa::NodeAttribute::Value, params);
+}
+
+void
+OpcUaMonitoredNode::stopMonitoring ()
+{
+ if (!m_node)
+ return;
+ m_node->disableMonitoring (QOpcUa::NodeAttribute::Value);
+}
+
+/* ======================================
+ * Signal handlers
+ * ====================================== */
+
+void
+OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
+ const QVariant &value)
+{
+ switch (attr)
+ {
+ case QOpcUa::NodeAttribute::DisplayName:
+ m_info.displayName = value.value<QOpcUaLocalizedText> ().text ();
+ break;
+ case QOpcUa::NodeAttribute::Description:
+ m_info.description = value.value<QOpcUaLocalizedText> ().text ();
+ break;
+ case QOpcUa::NodeAttribute::NodeClass:
+ {
+ auto me = QMetaEnum::fromType<QOpcUa::NodeClass> ();
+ auto nc = static_cast<QOpcUa::NodeClass> (value.toInt ());
+ const char *key = me.valueToKey (static_cast<int> (nc));
+ m_info.nodeClass
+ = key ? QLatin1StringView (key) : QStringLiteral ("Unknown");
+ }
+ break;
+ case QOpcUa::NodeAttribute::DataType:
+ {
+ const QString rawId = value.toString ();
+ m_valueType = QOpcUa::opcUaDataTypeToQOpcUaType (rawId);
+ auto ns0 = QOpcUa::namespace0IdFromNodeId (rawId);
+ m_info.dataType = QOpcUa::namespace0IdName (ns0);
+ if (m_info.dataType.isEmpty ())
+ m_info.dataType = rawId;
+ }
+ break;
+ case QOpcUa::NodeAttribute::AccessLevel:
+ {
+ quint32 bits = value.toUInt ();
+ bool newWritable
+ = (bits & quint32 (QOpcUa::AccessLevelBit::CurrentWrite)) != 0;
+ if (m_writable != newWritable)
+ {
+ m_writable = newWritable;
+ emit writableChanged ();
+ }
+ QStringList flags;
+ if (bits & quint32 (QOpcUa::AccessLevelBit::CurrentRead))
+ flags << QStringLiteral ("Read");
+ if (bits & quint32 (QOpcUa::AccessLevelBit::CurrentWrite))
+ flags << QStringLiteral ("Write");
+ if (bits & quint32 (QOpcUa::AccessLevelBit::HistoryRead))
+ flags << QStringLiteral ("History Read");
+ if (bits & quint32 (QOpcUa::AccessLevelBit::HistoryWrite))
+ flags << QStringLiteral ("History Write");
+ m_info.accessLevel = flags.isEmpty ()
+ ? QStringLiteral ("None")
+ : flags.join (QStringLiteral (", "));
+ }
+ break;
+ default:
+ return; // Skip infoChanged for attributes we don't track.
+ }
+ emit infoChanged ();
+}
+
+void
+OpcUaMonitoredNode::handleValueUpdated (const QVariant &value)
+{
+ m_value = value;
+ emit valueChanged ();
+
+ m_info.status = QOpcUa::statusToString (m_node->valueAttributeError ());
+ m_info.sourceTimestamp
+ = m_node->sourceTimestamp (QOpcUa::NodeAttribute::Value);
+ m_info.serverTimestamp
+ = m_node->serverTimestamp (QOpcUa::NodeAttribute::Value);
+ emit infoChanged ();
+}
+
+void
+OpcUaMonitoredNode::handleClientConnectedChanged ()
+{
+ auto *client = OpcUaClient::instance ();
+ if (!client)
+ return;
+
+ if (client->connected ())
+ {
+ if (!m_nodeId.isEmpty ())
+ setupNode ();
+ }
+ else
+ {
+ teardownNode ();
+ }
+}
+
+void
+OpcUaMonitoredNode::handleAttributeWritten (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+
+ bool ok = (statusCode == QOpcUa::Good);
+ emit writeCompleted (ok, ok ? QStringLiteral ("Write successful")
+ : QOpcUa::statusToString (statusCode));
+}
+
+void
+OpcUaMonitoredNode::handleEnableMonitoringFinished (
+ QOpcUa::NodeAttribute attr, QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+ if (statusCode != QOpcUa::Good)
+ qWarning () << "OpcUaMonitoredNode: enableMonitoring failed for"
+ << m_nodeId << ":" << QOpcUa::statusToString (statusCode);
+}
+
+void
+OpcUaMonitoredNode::handleDisableMonitoringFinished (
+ QOpcUa::NodeAttribute attr, QOpcUa::UaStatusCode statusCode)
+{
+ if (attr != QOpcUa::NodeAttribute::Value)
+ return;
+ if (statusCode != QOpcUa::Good)
+ qWarning () << "OpcUaMonitoredNode: disableMonitoring failed for"
+ << m_nodeId << ":" << QOpcUa::statusToString (statusCode);
+}
+
+/* ======================================
+ * Write support
+ * ====================================== */
+
+/**
+ * @brief Convert a QML value to the C++ type matching m_valueType.
+ *
+ * Returns an invalid QVariant if conversion fails (e.g. non-numeric
+ * string for an integer node, or out-of-range for narrow types).
+ * Arrays are handled by recursing over each element.
+ */
+QVariant
+OpcUaMonitoredNode::coerceValue (const QVariant &input) const
+{
+ if (input.metaType ().id () == QMetaType::QVariantList)
+ {
+ QVariantList list = input.toList ();
+ QVariantList result;
+ result.reserve (list.size ());
+ for (const QVariant &item : list)
+ {
+ QVariant coerced = coerceValue (item);
+ if (!coerced.isValid ())
+ return {};
+ result.append (coerced);
+ }
+ return QVariant::fromValue (result);
+ }
+
+ switch (m_valueType)
+ {
+ case QOpcUa::Boolean:
+ {
+ if (input.metaType ().id () == QMetaType::Bool)
+ return QVariant (input.toBool ());
+ QString s = input.toString ().trimmed ().toLower ();
+ if (s == QStringLiteral ("true") || s == QStringLiteral ("1"))
+ return QVariant (true);
+ if (s == QStringLiteral ("false") || s == QStringLiteral ("0"))
+ return QVariant (false);
+ return {};
+ }
+ case QOpcUa::SByte:
+ {
+ bool ok = false;
+ qint32 v = input.toInt (&ok);
+ if (!ok || v < -128 || v > 127)
+ return {};
+ return QVariant::fromValue (static_cast<qint8> (v));
+ }
+ case QOpcUa::Byte:
+ {
+ bool ok = false;
+ quint32 v = input.toUInt (&ok);
+ if (!ok || v > 255)
+ return {};
+ return QVariant::fromValue (static_cast<quint8> (v));
+ }
+ case QOpcUa::Int16:
+ {
+ bool ok = false;
+ qint32 v = input.toInt (&ok);
+ if (!ok || v < -32768 || v > 32767)
+ return {};
+ return QVariant::fromValue (static_cast<qint16> (v));
+ }
+ case QOpcUa::UInt16:
+ {
+ bool ok = false;
+ quint32 v = input.toUInt (&ok);
+ if (!ok || v > 65535)
+ return {};
+ return QVariant::fromValue (static_cast<quint16> (v));
+ }
+ case QOpcUa::Int32:
+ {
+ bool ok = false;
+ qint32 v = input.toInt (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::UInt32:
+ {
+ bool ok = false;
+ quint32 v = input.toUInt (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::Int64:
+ {
+ bool ok = false;
+ qint64 v = input.toLongLong (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::UInt64:
+ {
+ bool ok = false;
+ quint64 v = input.toULongLong (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::Float:
+ {
+ bool ok = false;
+ float v = input.toFloat (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::Double:
+ {
+ bool ok = false;
+ double v = input.toDouble (&ok);
+ return ok ? QVariant::fromValue (v) : QVariant{};
+ }
+ case QOpcUa::String:
+ return QVariant (input.toString ());
+ case QOpcUa::DateTime:
+ {
+ QDateTime dt = input.toDateTime ();
+ return dt.isValid () ? QVariant::fromValue (dt) : QVariant{};
+ }
+ case QOpcUa::ByteString:
+ return QVariant::fromValue (input.toByteArray ());
+ case QOpcUa::Guid:
+ {
+ QUuid uuid (input.toString ());
+ return !uuid.isNull () ? QVariant::fromValue (uuid) : QVariant{};
+ }
+ default:
+ return input;
+ }
+}
+
+void
+OpcUaMonitoredNode::writeValue (const QVariant &value)
+{
+ if (!m_node)
+ {
+ emit writeCompleted (false, QStringLiteral ("Node not connected"));
+ return;
+ }
+ if (!m_writable)
+ {
+ emit writeCompleted (false, QStringLiteral ("Node is read-only"));
+ return;
+ }
+ if (m_valueType == QOpcUa::Types::Undefined)
+ {
+ emit writeCompleted (false,
+ QStringLiteral ("Data type not yet resolved"));
+ return;
+ }
+
+ QVariant toWrite = value;
+
+ // QML text fields always send QString. For array nodes (current
+ // value is QVariantList) split "1, 2, 3" into a list first so
+ // coerceValue can handle each element individually.
+ if (m_value.metaType ().id () == QMetaType::QVariantList
+ && value.metaType ().id () == QMetaType::QString)
+ {
+ QStringList parts
+ = value.toString ().split (QLatin1Char (','), Qt::SkipEmptyParts);
+ QVariantList list;
+ list.reserve (parts.size ());
+ for (const QString &part : parts)
+ list.append (QVariant (part.trimmed ()));
+ toWrite = QVariant::fromValue (list);
+ }
+
+ QVariant coerced = coerceValue (toWrite);
+ if (!coerced.isValid ())
+ {
+ emit writeCompleted (
+ false, QStringLiteral ("Cannot convert value to node data type"));
+ return;
+ }
+
+ if (!m_node->writeValueAttribute (coerced, m_valueType))
+ emit writeCompleted (false,
+ QStringLiteral ("Write request failed to dispatch"));
+}
diff --git a/src/OpcUaMonitoredNode.h b/src/OpcUaMonitoredNode.h
new file mode 100644
index 0000000..2e86b2c
--- /dev/null
+++ b/src/OpcUaMonitoredNode.h
@@ -0,0 +1,134 @@
+/**
+ * @file OpcUaMonitoredNode.h
+ * @brief QML component for monitoring a single OPC UA node.
+ *
+ * Inherits QObject + QQmlParserStatus so that initialisation is
+ * deferred until all QML bindings are applied (componentComplete).
+ */
+#ifndef OPCUAMONITOREDNODE_H
+#define OPCUAMONITOREDNODE_H
+
+#include <QDateTime>
+#include <QObject>
+#include <QOpcUaNode>
+#include <QQmlEngine>
+#include <QQmlParserStatus>
+#include <QVariant>
+
+/**
+ * @brief Metadata bundle for a monitored OPC UA node.
+ *
+ * Exposed as a single Q_PROPERTY on OpcUaMonitoredNode so that
+ * the QML API stays simple (only `value` is top-level).
+ * Advanced users can access fields via dot notation:
+ * node.info.displayName, node.info.status, etc.
+ */
+struct OpcUaNodeInfo
+{
+ Q_GADGET
+ Q_PROPERTY (QString displayName MEMBER displayName)
+ Q_PROPERTY (QString description MEMBER description)
+ Q_PROPERTY (QString nodeClass MEMBER nodeClass)
+ Q_PROPERTY (QString dataType MEMBER dataType)
+ Q_PROPERTY (QString accessLevel MEMBER accessLevel)
+ Q_PROPERTY (QString status MEMBER status)
+ Q_PROPERTY (QDateTime sourceTimestamp MEMBER sourceTimestamp)
+ Q_PROPERTY (QDateTime serverTimestamp MEMBER serverTimestamp)
+
+public:
+ QString displayName;
+ QString description;
+ QString nodeClass;
+ QString dataType;
+ QString accessLevel;
+ QString status;
+ QDateTime sourceTimestamp;
+ QDateTime serverTimestamp;
+};
+Q_DECLARE_METATYPE (OpcUaNodeInfo)
+
+class OpcUaMonitoredNode : public QObject, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES (QQmlParserStatus)
+ QML_ELEMENT
+
+ Q_PROPERTY (QString nodeId READ nodeId WRITE setNodeId NOTIFY nodeIdChanged)
+ Q_PROPERTY (
+ bool monitored READ monitored WRITE setMonitored NOTIFY monitoredChanged)
+ Q_PROPERTY (double publishingInterval READ publishingInterval WRITE
+ setPublishingInterval NOTIFY publishingIntervalChanged)
+ Q_PROPERTY (QVariant value READ value NOTIFY valueChanged)
+ Q_PROPERTY (bool writable READ writable NOTIFY writableChanged)
+ Q_PROPERTY (OpcUaNodeInfo info READ info NOTIFY infoChanged)
+
+public:
+ explicit OpcUaMonitoredNode (QObject *parent = nullptr);
+
+ QString nodeId () const;
+ void setNodeId (const QString &id);
+
+ bool monitored () const;
+ void setMonitored (bool monitored);
+
+ double publishingInterval () const;
+ void setPublishingInterval (double interval);
+
+ QVariant value () const;
+ bool writable () const;
+ OpcUaNodeInfo info () const;
+
+ /**
+ * @brief Write a value to the OPC UA node.
+ *
+ * Coerces @a value to the node's data type (auto-detected from the
+ * server's DataType attribute). For array nodes, a comma-separated
+ * string is split and each element coerced individually.
+ * Result is reported asynchronously via writeCompleted().
+ */
+ Q_INVOKABLE void writeValue (const QVariant &value);
+
+ void classBegin () override;
+ void componentComplete () override;
+
+signals:
+ void nodeIdChanged ();
+ void monitoredChanged ();
+ void publishingIntervalChanged ();
+ void valueChanged ();
+ void writableChanged ();
+ void infoChanged ();
+ void writeCompleted (bool success, const QString &message);
+
+private slots:
+ void handleAttributeUpdated (QOpcUa::NodeAttribute attr,
+ const QVariant &value);
+ void handleValueUpdated (const QVariant &value);
+ void handleClientConnectedChanged ();
+ void handleAttributeWritten (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode);
+ void handleEnableMonitoringFinished (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode);
+ void handleDisableMonitoringFinished (QOpcUa::NodeAttribute attr,
+ QOpcUa::UaStatusCode statusCode);
+
+private:
+ void setupNode ();
+ void teardownNode ();
+ void startMonitoring ();
+ void stopMonitoring ();
+ QVariant coerceValue (const QVariant &input) const;
+
+ QString m_nodeId;
+ bool m_monitored = true;
+ double m_publishingInterval = 250.0;
+ bool m_componentComplete = false;
+
+ QOpcUaNode *m_node = nullptr;
+ QOpcUa::Types m_valueType = QOpcUa::Types::Undefined;
+ bool m_writable = false;
+ QVariant m_value;
+ OpcUaNodeInfo m_info;
+};
+
+#endif // OPCUAMONITOREDNODE_H