summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-20 12:59:07 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-20 13:06:24 +0100
commit47227acd25c59a1d4b2961c0e1b1eb879e68adec (patch)
treeb814869a40def023ebb5d0f35931f848c384f122
parent263a055f9b9460e4da747c7e56372e963f72fe68 (diff)
downloadBobinkQtOpcUa-47227acd25c59a1d4b2961c0e1b1eb879e68adec.tar.gz
BobinkQtOpcUa-47227acd25c59a1d4b2961c0e1b1eb879e68adec.zip
Add write support with automatic type coercion to OpcUaMonitoredNode
writeValue() Q_INVOKABLE coerces QML JS types to the exact C++ type expected by the OPC UA node (auto-detected from DataType attribute via opcUaDataTypeToQOpcUaType). Handles all scalar types, booleans, and comma-separated array input. Adds writable property derived from AccessLevel bits. Demo shows inline TextField + Write button for writable nodes, "(READ-ONLY)" for others.
-rw-r--r--demo/NodePage.qml47
-rw-r--r--src/OpcUaMonitoredNode.cpp220
-rw-r--r--src/OpcUaMonitoredNode.h19
3 files changed, 277 insertions, 9 deletions
diff --git a/demo/NodePage.qml b/demo/NodePage.qml
index 1468817..442a6fd 100644
--- a/demo/NodePage.qml
+++ b/demo/NodePage.qml
@@ -91,21 +91,28 @@ Page {
// 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: 160
+ }
+ Label {
+ text: "Write"
+ font.bold: true
Layout.fillWidth: true
}
}
Rectangle {
+ id: separator
Layout.fillWidth: true
height: 1
color: "#ccc"
@@ -128,6 +135,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,19 +148,45 @@ Page {
contentItem: RowLayout {
spacing: 12
+
+ // Column 1: Identifier
Label {
text: {
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: node.value !== undefined && String(node.value) !== "" ? String(node.value) : "—"
- Layout.fillWidth: true
+ Layout.preferredWidth: 160
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 {
+ visible: !node.writable
+ text: "(READ-ONLY)"
+ color: "gray"
+ font.italic: true
+ Layout.fillWidth: true
+ }
}
ToolTip.visible: hovered
diff --git a/src/OpcUaMonitoredNode.cpp b/src/OpcUaMonitoredNode.cpp
index b158881..518ddd5 100644
--- a/src/OpcUaMonitoredNode.cpp
+++ b/src/OpcUaMonitoredNode.cpp
@@ -33,7 +33,8 @@ OpcUaMonitoredNode::setNodeId (const QString &id)
return;
teardownNode ();
- if (OpcUaClient::instance () && OpcUaClient::instance ()->connected ())
+ auto *client = OpcUaClient::instance ();
+ if (client && client->connected ())
setupNode ();
}
@@ -58,6 +59,12 @@ OpcUaMonitoredNode::value () const
return m_value;
}
+bool
+OpcUaMonitoredNode::writable () const
+{
+ return m_writable;
+}
+
OpcUaNodeInfo
OpcUaMonitoredNode::info () const
{
@@ -113,6 +120,8 @@ OpcUaMonitoredNode::setupNode ()
&OpcUaMonitoredNode::handleAttributeUpdated);
connect (m_node, &QOpcUaNode::valueAttributeUpdated, this,
&OpcUaMonitoredNode::handleValueUpdated);
+ connect (m_node, &QOpcUaNode::attributeWritten, this,
+ &OpcUaMonitoredNode::handleAttributeWritten);
m_node->readAttributes (
QOpcUa::NodeAttribute::Value | QOpcUa::NodeAttribute::DisplayName
@@ -134,6 +143,14 @@ OpcUaMonitoredNode::teardownNode ()
m_info = OpcUaNodeInfo{};
emit infoChanged ();
+
+ m_valueType = QOpcUa::Types::Undefined;
+
+ if (m_writable)
+ {
+ m_writable = false;
+ emit writableChanged ();
+ }
}
/* ======================================
@@ -163,15 +180,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 +213,7 @@ OpcUaMonitoredNode::handleAttributeUpdated (QOpcUa::NodeAttribute attr,
}
break;
default:
- return;
+ return; // Skip infoChanged for attributes we don't track.
}
emit infoChanged ();
}
@@ -223,3 +249,189 @@ 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));
+}
+
+/* ======================================
+ * 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 ());
+ 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..5895bea 100644
--- a/src/OpcUaMonitoredNode.h
+++ b/src/OpcUaMonitoredNode.h
@@ -57,6 +57,7 @@ class OpcUaMonitoredNode : public QObject, public QQmlParserStatus
Q_PROPERTY (
bool monitored READ monitored WRITE setMonitored NOTIFY monitoredChanged)
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:
@@ -69,8 +70,19 @@ public:
void setMonitored (bool monitored);
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;
@@ -78,23 +90,30 @@ signals:
void nodeIdChanged ();
void monitoredChanged ();
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);
private:
void setupNode ();
void teardownNode ();
+ QVariant coerceValue (const QVariant &input) const;
QString m_nodeId;
bool m_monitored = true;
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;
};