diff options
| -rw-r--r-- | demo/Main.qml | 29 | ||||
| -rw-r--r-- | demo/NodePage.qml | 135 | ||||
| -rw-r--r-- | demo/main.cpp | 81 | ||||
| -rw-r--r-- | src/OpcUaMonitoredNode.cpp | 302 | ||||
| -rw-r--r-- | src/OpcUaMonitoredNode.h | 32 |
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; }; |
