summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--demo/Main.qml29
-rw-r--r--demo/NodePage.qml135
-rw-r--r--demo/main.cpp81
-rw-r--r--src/OpcUaMonitoredNode.cpp302
-rw-r--r--src/OpcUaMonitoredNode.h32
5 files changed, 543 insertions, 36 deletions
diff --git a/demo/Main.qml b/demo/Main.qml
index ca0a419..361e3bd 100644
--- a/demo/Main.qml
+++ b/demo/Main.qml
@@ -153,15 +153,31 @@ ApplicationWindow {
clip: true
model: Bobink.servers
delegate: ItemDelegate {
+ id: serverDelegate
required property var modelData
width: ListView.view.width
- text: modelData.serverName + " — " + modelData.applicationUri
+ contentItem: ColumnLayout {
+ spacing: 2
+ Label {
+ text: serverDelegate.modelData.serverName
+ }
+ Label {
+ text: serverDelegate.modelData.applicationUri
+ color: "gray"
+ font.italic: true
+ font.pointSize: 8
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ }
onClicked: {
if (modelData.discoveryUrls.length > 0)
serverUrlField.text = modelData.discoveryUrls[0];
}
}
- ScrollBar.vertical: ScrollBar {}
+ ScrollBar.vertical: ScrollBar {
+ policy: ScrollBar.AsNeeded
+ }
}
RowLayout {
@@ -178,8 +194,7 @@ ApplicationWindow {
Layout.fillWidth: true
}
Button {
- text: root.showPkiSettings ? "Hide" : "Configure..."
- flat: true
+ text: root.showPkiSettings ? "Hide" : "Configure"
onClicked: root.showPkiSettings = !root.showPkiSettings
}
}
@@ -199,7 +214,7 @@ ApplicationWindow {
placeholderText: "Client certificate (.der)"
}
Button {
- text: "Browse..."
+ text: "Browse"
onClicked: certFileDialog.open()
}
@@ -213,7 +228,7 @@ ApplicationWindow {
placeholderText: "Private key (.pem, .crt)"
}
Button {
- text: "Browse..."
+ text: "Browse"
onClicked: keyFileDialog.open()
}
@@ -226,7 +241,7 @@ ApplicationWindow {
text: Bobink.pkiDir
}
Button {
- text: "Browse..."
+ text: "Browse"
onClicked: trustFolderDialog.open()
}
}
diff --git a/demo/NodePage.qml b/demo/NodePage.qml
index 1468817..e677232 100644
--- a/demo/NodePage.qml
+++ b/demo/NodePage.qml
@@ -14,7 +14,20 @@ Page {
readonly property var pages: [
{
+ title: "Server Info",
+ description: "Standard OPC UA server nodes (namespace 0)."
+ + " CurrentTime updates live via monitoring.",
+ nodes: [
+ "ns=0;i=2258", // CurrentTime
+ "ns=0;i=2257", // StartTime
+ "ns=0;i=2259", // State
+ "ns=0;i=2261", // ProductName
+ "ns=0;i=2264" // SoftwareVersion
+ ]
+ },
+ {
title: "Read-Write Scalars",
+ description: "Single-value nodes with read and write access.",
nodes: [
"ns=1;s=bool_rw_scalar",
"ns=1;s=int16_rw_scalar",
@@ -25,11 +38,17 @@ Page {
"ns=1;s=uint64_rw_scalar",
"ns=1;s=float_rw_scalar",
"ns=1;s=double_rw_scalar",
- "ns=1;s=string_rw_scalar"
+ "ns=1;s=string_rw_scalar",
+ "ns=1;s=sbyte_rw_scalar",
+ "ns=1;s=byte_rw_scalar",
+ "ns=1;s=datetime_rw_scalar",
+ "ns=1;s=guid_rw_scalar",
+ "ns=1;s=bytestring_rw_scalar"
]
},
{
title: "Read-Only Scalars",
+ description: "Single-value nodes with read-only access.",
nodes: [
"ns=1;s=bool_ro_scalar",
"ns=1;s=int16_ro_scalar",
@@ -40,11 +59,19 @@ Page {
"ns=1;s=uint64_ro_scalar",
"ns=1;s=float_ro_scalar",
"ns=1;s=double_ro_scalar",
- "ns=1;s=string_ro_scalar"
+ "ns=1;s=string_ro_scalar",
+ "ns=1;s=sbyte_ro_scalar",
+ "ns=1;s=byte_ro_scalar",
+ "ns=1;s=datetime_ro_scalar",
+ "ns=1;s=guid_ro_scalar",
+ "ns=1;s=bytestring_ro_scalar"
]
},
{
title: "Read-Write Arrays",
+ description: "Array nodes. Values are displayed comma-separated."
+ + " To write, enter comma-separated values (e.g. \"1, 2, 3\")."
+ + " Commas cannot appear inside individual values.",
nodes: [
"ns=1;s=bool_rw_array",
"ns=1;s=int16_rw_array",
@@ -55,13 +82,50 @@ Page {
"ns=1;s=uint64_rw_array",
"ns=1;s=float_rw_array",
"ns=1;s=double_rw_array",
- "ns=1;s=string_rw_array"
+ "ns=1;s=string_rw_array",
+ "ns=1;s=sbyte_rw_array",
+ "ns=1;s=byte_rw_array",
+ "ns=1;s=datetime_rw_array",
+ "ns=1;s=guid_rw_array",
+ "ns=1;s=bytestring_rw_array"
+ ]
+ },
+ {
+ title: "Non-Existent Nodes",
+ description: "These node IDs do not exist on the server."
+ + " The row should show no value and no metadata in the tooltip.",
+ nodes: [
+ "ns=1;s=does_not_exist",
+ "ns=99;i=12345",
+ "ns=1;s=also_missing"
]
+ },
+ {
+ title: "Empty (Monitoring Test)",
+ description: "No nodes on this page. All previous pages are inactive,"
+ + " so the server log should show zero active monitored items.",
+ nodes: []
}
]
readonly property var currentPage: pages[pageNumber - 1]
+ // OPC UA ServerState enum (Part 4, Table 120).
+ readonly property var serverStates: [
+ "Running", "Failed", "NoConfiguration", "Suspended",
+ "Shutdown", "Test", "CommunicationFault", "Unknown"
+ ]
+
+ function formatValue(node) {
+ var v = node.value;
+ if (v === undefined || String(v) === "")
+ return "—";
+ // Map ServerState enum to human-readable string.
+ if (node.nodeId === "ns=0;i=2259" && !isNaN(v))
+ return serverStates[v] || String(v);
+ return String(v);
+ }
+
Component.onCompleted: nodePage.logFunction(
currentPage.title + " page loaded (" + currentPage.nodes.length + " nodes)")
@@ -78,7 +142,7 @@ Page {
font.pointSize: 14
}
Label {
- text: "(" + nodePage.pageNumber + "/3)"
+ text: "(" + nodePage.pageNumber + "/" + nodePage.pages.length + ")"
color: "gray"
}
Item { Layout.fillWidth: true }
@@ -88,24 +152,41 @@ Page {
}
}
+ Label {
+ id: pageDescription
+ visible: currentPage.description !== undefined
+ text: currentPage.description || ""
+ wrapMode: Text.WordWrap
+ color: "gray"
+ font.italic: true
+ Layout.fillWidth: true
+ }
+
// Column headers
RowLayout {
Layout.fillWidth: true
- spacing: 0
+ Layout.leftMargin: 12
+ Layout.rightMargin: 12
+ spacing: 12
Label {
text: "Identifier"
font.bold: true
- Layout.preferredWidth: 200
- leftPadding: 12
+ Layout.preferredWidth: 160
}
Label {
text: "Value"
font.bold: true
+ Layout.preferredWidth: 300
+ }
+ Label {
+ text: "Write"
+ font.bold: true
Layout.fillWidth: true
}
}
Rectangle {
+ id: separator
Layout.fillWidth: true
height: 1
color: "#ccc"
@@ -128,6 +209,10 @@ Page {
id: node
nodeId: delegate.modelData
monitored: nodePage.StackView.status === StackView.Active
+ onWriteCompleted: (success, message) => {
+ var short_id = node.nodeId.substring(node.nodeId.indexOf(";s=") + 3);
+ nodePage.logFunction(short_id + ": " + message);
+ }
}
background: Rectangle {
@@ -137,18 +222,46 @@ Page {
contentItem: RowLayout {
spacing: 12
+
+ // Column 1: Display name if available, otherwise short ID.
Label {
text: {
+ if (node.info.displayName)
+ return node.info.displayName;
var idx = node.nodeId.indexOf(";s=");
return idx >= 0 ? node.nodeId.substring(idx + 3) : node.nodeId;
}
- Layout.preferredWidth: 188
+ Layout.preferredWidth: 160
+ elide: Text.ElideRight
+ }
+
+ // Column 2: Live value (always visible)
+ Label {
+ text: nodePage.formatValue(node)
+ Layout.preferredWidth: 300
elide: Text.ElideRight
}
+
+ // Column 3: Edit area (writable) or READ-ONLY label
+ TextField {
+ id: editField
+ visible: node.writable
+ Layout.fillWidth: true
+ placeholderText: "Enter value..."
+ onAccepted: node.writeValue(text)
+ }
+ Button {
+ id: writeButton
+ visible: node.writable
+ text: "Write"
+ onClicked: node.writeValue(editField.text)
+ }
Label {
- text: node.value !== undefined && String(node.value) !== "" ? String(node.value) : "—"
+ visible: !node.writable
+ text: "(READ-ONLY)"
+ color: "gray"
+ font.italic: true
Layout.fillWidth: true
- elide: Text.ElideRight
}
}
@@ -179,7 +292,7 @@ Page {
Item { Layout.fillWidth: true }
Button {
text: "Next →"
- visible: nodePage.pageNumber < 3
+ visible: nodePage.pageNumber < nodePage.pages.length
onClicked: nodePage.stackRef.push("NodePage.qml", {
stackRef: nodePage.stackRef,
pageNumber: nodePage.pageNumber + 1,
diff --git a/demo/main.cpp b/demo/main.cpp
index 6d0b9be..689b998 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -2,24 +2,83 @@
* @file main.cpp
* @brief Entry point for the Bobink demo application.
*/
+#include <QDateTime>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQml/QQmlExtensionPlugin>
-Q_IMPORT_QML_PLUGIN(BobinkPlugin)
+Q_IMPORT_QML_PLUGIN (BobinkPlugin)
-int main(int argc, char *argv[])
+/** @brief Custom log handler matching open62541 server log format. */
+static void
+logHandler (QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
{
- // Load the locally-built OpcUa backend plugin (open62541).
- QCoreApplication::addLibraryPath(QStringLiteral(QTOPCUA_PLUGIN_PATH));
+ // Color only the type/category tag.
+ const char *color = "";
+ const char *label = "debug";
+ switch (type)
+ {
+ case QtDebugMsg:
+ label = "debug";
+ break;
+ case QtInfoMsg:
+ color = "\x1b[32m";
+ label = "info";
+ break;
+ case QtWarningMsg:
+ color = "\x1b[33m";
+ label = "warning";
+ break;
+ case QtCriticalMsg:
+ color = "\x1b[31m";
+ label = "critical";
+ break;
+ case QtFatalMsg:
+ color = "\x1b[1;31m";
+ label = "fatal";
+ break;
+ }
- QGuiApplication app(argc, argv);
+ // Shorten "qt.opcua.plugins.open62541.sdk.client" → "client".
+ QLatin1StringView cat (ctx.category ? ctx.category : "default");
+ qsizetype dot = cat.lastIndexOf (QLatin1Char ('.'));
+ if (dot >= 0)
+ cat = cat.sliced (dot + 1);
- QQmlApplicationEngine engine;
- QObject::connect (
- &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
- [] () { QCoreApplication::exit (1); }, Qt::QueuedConnection);
+ // "debug/client", "warning/network", etc. — padded to 20 chars.
+ QByteArray tag
+ = QByteArray (label) + '/' + QByteArray (cat.data (), cat.size ());
- engine.loadFromModule("BobinkDemo", "Main");
- return app.exec();
+ // Format UTC offset as "(UTC+0100)" to match open62541 server logs.
+ QDateTime now = QDateTime::currentDateTime ();
+ qint32 offset = now.offsetFromUtc ();
+ QChar sign = offset >= 0 ? u'+' : u'-';
+ offset = qAbs (offset);
+ QString ts = now.toString (u"yyyy-MM-dd HH:mm:ss.zzz")
+ + QStringLiteral (" (UTC%1%2%3)")
+ .arg (sign)
+ .arg (offset / 3600, 2, 10, QLatin1Char ('0'))
+ .arg ((offset % 3600) / 60, 2, 10, QLatin1Char ('0'));
+
+ fprintf (stderr, "[%s] %s%-20.*s\x1b[0m %s\n", qPrintable (ts), color,
+ static_cast<int> (tag.size ()), tag.data (), qPrintable (msg));
+}
+
+int
+main (int argc, char *argv[])
+{
+ // Load the locally-built OpcUa backend plugin (open62541).
+ QCoreApplication::addLibraryPath (QStringLiteral (QTOPCUA_PLUGIN_PATH));
+
+ qInstallMessageHandler (logHandler);
+
+ QGuiApplication app (argc, argv);
+
+ QQmlApplicationEngine engine;
+ QObject::connect (
+ &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
+ [] () { QCoreApplication::exit (1); }, Qt::QueuedConnection);
+
+ engine.loadFromModule ("BobinkDemo", "Main");
+ return app.exec ();
}
diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp
index b158881..cd93730 100644
--- a/src/OpcUaMonitoredNode.cpp
+++ b/src/OpcUaMonitoredNode.cpp
@@ -7,7 +7,9 @@
#include <QMetaEnum>
#include <QOpcUaLocalizedText>
+#include <QOpcUaMonitoringParameters>
#include <QStringList>
+#include <QUuid>
OpcUaMonitoredNode::OpcUaMonitoredNode (QObject *parent) : QObject (parent) {}
@@ -33,7 +35,8 @@ OpcUaMonitoredNode::setNodeId (const QString &id)
return;
teardownNode ();
- if (OpcUaClient::instance () && OpcUaClient::instance ()->connected ())
+ auto *client = OpcUaClient::instance ();
+ if (client && client->connected ())
setupNode ();
}
@@ -50,6 +53,29 @@ OpcUaMonitoredNode::setMonitored (bool 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
@@ -58,6 +84,12 @@ OpcUaMonitoredNode::value () const
return m_value;
}
+bool
+OpcUaMonitoredNode::writable () const
+{
+ return m_writable;
+}
+
OpcUaNodeInfo
OpcUaMonitoredNode::info () const
{
@@ -113,11 +145,20 @@ OpcUaMonitoredNode::setupNode ()
&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::Value | QOpcUa::NodeAttribute::DisplayName
- | QOpcUa::NodeAttribute::Description | QOpcUa::NodeAttribute::NodeClass
- | QOpcUa::NodeAttribute::DataType | QOpcUa::NodeAttribute::AccessLevel);
+ QOpcUa::NodeAttribute::DisplayName | QOpcUa::NodeAttribute::Description
+ | QOpcUa::NodeAttribute::NodeClass | QOpcUa::NodeAttribute::DataType
+ | QOpcUa::NodeAttribute::AccessLevel);
+
+ if (m_monitored)
+ startMonitoring ();
}
void
@@ -134,6 +175,31 @@ OpcUaMonitoredNode::teardownNode ()
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);
}
/* ======================================
@@ -163,15 +229,24 @@ OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
break;
case QOpcUa::NodeAttribute::DataType:
{
- auto ns0 = QOpcUa::namespace0IdFromNodeId (value.toString ());
+ 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 = value.toString ();
+ 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");
@@ -187,7 +262,7 @@ OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
}
break;
default:
- return;
+ return; // Skip infoChanged for attributes we don't track.
}
emit infoChanged ();
}
@@ -223,3 +298,216 @@ OpcUaMonitoredNode::handleClientConnectedChanged ()
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
index ac5983a..2e86b2c 100644
--- a/src/OpcUaMonitoredNode.h
+++ b/src/OpcUaMonitoredNode.h
@@ -56,7 +56,10 @@ class OpcUaMonitoredNode : public QObject, public QQmlParserStatus
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:
@@ -68,33 +71,62 @@ public:
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;
};