/** * @file OpcUaMonitoredNode.cpp * @brief OpcUaMonitoredNode implementation. */ #include "OpcUaMonitoredNode.h" #include "OpcUaClient.h" #include #include #include #include #include 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 ().text (); break; case QOpcUa::NodeAttribute::Description: m_info.description = value.value ().text (); break; case QOpcUa::NodeAttribute::NodeClass: { auto me = QMetaEnum::fromType (); auto nc = static_cast (value.toInt ()); const char *key = me.valueToKey (static_cast (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 (v)); } case QOpcUa::Byte: { bool ok = false; quint32 v = input.toUInt (&ok); if (!ok || v > 255) return {}; return QVariant::fromValue (static_cast (v)); } case QOpcUa::Int16: { bool ok = false; qint32 v = input.toInt (&ok); if (!ok || v < -32768 || v > 32767) return {}; return QVariant::fromValue (static_cast (v)); } case QOpcUa::UInt16: { bool ok = false; quint32 v = input.toUInt (&ok); if (!ok || v > 65535) return {}; return QVariant::fromValue (static_cast (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")); }