diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-20 12:59:07 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-20 13:06:24 +0100 |
| commit | 47227acd25c59a1d4b2961c0e1b1eb879e68adec (patch) | |
| tree | b814869a40def023ebb5d0f35931f848c384f122 /src | |
| parent | 263a055f9b9460e4da747c7e56372e963f72fe68 (diff) | |
| download | BobinkQtOpcUa-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.
Diffstat (limited to 'src')
| -rw-r--r-- | src/OpcUaMonitoredNode.cpp | 220 | ||||
| -rw-r--r-- | src/OpcUaMonitoredNode.h | 19 |
2 files changed, 235 insertions, 4 deletions
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; }; |
