演示效果1. UI 界面构建与初始化功能描述创建窗口布局包含服务器地址/用户名/密码输入框、连接/断开/读取/写入/订阅控制按钮、状态标签和日志输出框并初始化定时器及信号槽连接。Widget::Widget(QWidget *parent) : QWidget(parent) , m_connected(false) { setWindowTitle(OPC UA Qt Demo); resize(700, 550); // 中央组件 // auto *central new QWidget(this); // setCentralWidget(central); auto *mainLayout new QVBoxLayout(this); // 连接配置区域 auto *configGroup new QWidget(this); auto *configLayout new QFormLayout(configGroup); m_serverUrlEdit new QLineEdit(opc.tcp://127.0.0.1:49320, this); m_usernameEdit new QLineEdit(zlg, this); m_passwordEdit new QLineEdit(123456, this); m_passwordEdit-setEchoMode(QLineEdit::Password); configLayout-addRow(服务器地址:, m_serverUrlEdit); configLayout-addRow(用户名:, m_usernameEdit); configLayout-addRow(密码:, m_passwordEdit); mainLayout-addWidget(configGroup); // 按钮区域 auto *btnLayout new QHBoxLayout(); m_connectBtn new QPushButton(连接, this); m_disconnectBtn new QPushButton(断开, this); m_disconnectBtn-setEnabled(false); m_readBtn new QPushButton(读取节点, this); m_readBtn-setEnabled(false); m_addNodeBtn new QPushButton(写入节点, this); m_addNodeBtn-setEnabled(false); m_OpenSubscription new QPushButton(开启订阅,this); m_OpenSubscription-setEnabled(true); m_CloseSubscription new QPushButton(关闭订阅,this); m_CloseSubscription-setEnabled(false); btnLayout-addWidget(m_connectBtn); btnLayout-addWidget(m_disconnectBtn); btnLayout-addStretch(); btnLayout-addWidget(m_readBtn); btnLayout-addWidget(m_addNodeBtn); btnLayout-addSpacing(20); btnLayout-addWidget(m_OpenSubscription); btnLayout-addWidget(m_CloseSubscription); mainLayout-addLayout(btnLayout); // // 节点 ID 输入 // auto *nodeLayout new QHBoxLayout(); // nodeLayout-addWidget(new QLabel(节点 ID:, this)); // m_nodeIdEdit new QLineEdit(ns0;i2258, this); // nodeLayout-addWidget(m_nodeIdEdit); // mainLayout-addLayout(nodeLayout); // 状态栏 m_statusLabel new QLabel(未连接, this); mainLayout-addWidget(m_statusLabel); // 日志输出 m_logEdit new QTextEdit(this); m_logEdit-setReadOnly(true); mainLayout-addWidget(m_logEdit); // 定时器用于周期读取 m_readTimer new QTimer(this); m_readTimer-setInterval(1000); // 信号槽连接 connect(m_connectBtn, QPushButton::clicked, this, Widget::onConnectClicked); connect(m_disconnectBtn, QPushButton::clicked, this, Widget::onDisconnectClicked); connect(m_readBtn, QPushButton::clicked, this, Widget::onReadClicked); connect(m_addNodeBtn, QPushButton::clicked, this, Widget::onAddNodeClicked); connect(m_readTimer, QTimer::timeout, this, Widget::onTimerTick); connect(m_OpenSubscription,QPushButton::clicked,this,Widget::onOpenSubscriptionClicked); connect(m_CloseSubscription,QPushButton::clicked,this,Widget::onCloseSubscriptionClicked); m_opcTimer new QTimer(this); m_opcTimer-start(50); // 50ms 调用一次保证回调及时触发 connect(m_opcTimer, QTimer::timeout, this, [this]() { if (m_client m_client-isConnected()) { m_client-runIterate(50); } }); log(OPC UA Qt Demo 已启动); log(默认用户名: zlg / 密码: 123456); }2. 日志输出功能void Widget::log(const QString msg) { m_logEdit-append(QString([%1] %2) .arg(QDateTime::currentDateTime().toString(hh:mm:ss)) .arg(msg)); }3. UI 状态更新按钮启用/禁用void Widget::updateUiState() { m_connectBtn-setEnabled(!m_connected); m_disconnectBtn-setEnabled(m_connected); m_readBtn-setEnabled(m_connected); m_addNodeBtn-setEnabled(m_connected); m_serverUrlEdit-setEnabled(!m_connected); m_usernameEdit-setEnabled(!m_connected); m_passwordEdit-setEnabled(!m_connected); m_statusLabel-setText(m_connected ? 已连接 : 未连接); }4. 连接 OPC UA 服务器void Widget::onConnectClicked() { try { // 创建客户端配置 opcua::ClientConfig config; // 设置用户名/密码认证 opcua::UserNameIdentityToken token( opcua::String(m_usernameEdit-text().toStdString()), opcua::String(m_passwordEdit-text().toStdString()) ); config.setUserIdentityToken(token); // 创建客户端 // m_client std::make_uniqueopcua::Client(std::move(config)); m_client std::make_uniqueopcua::Client(); //m_client-config().setUserIdentityToken(token); // 设置状态回调 m_client-onConnected([this]() { QMetaObject::invokeMethod(this, [this]() { log(已连接到服务器); m_connected true; updateUiState(); }); }); m_client-onDisconnected([this]() { QMetaObject::invokeMethod(this, [this]() { log(已断开连接); m_connected false; updateUiState(); }); }); // 连接服务器 std::string url m_serverUrlEdit-text().toStdString(); log(QString(正在连接 %1 ...).arg(m_serverUrlEdit-text())); m_client-connect(url); // 启动周期读取 m_readTimer-start(); } catch (const opcua::BadStatus e) { log(QString(连接失败: %1).arg(e.what())); QMessageBox::critical(this, 错误, QString(连接失败:\n%1).arg(e.what())); } catch (const std::exception e) { log(QString(连接失败: %1).arg(e.what())); QMessageBox::critical(this, 错误, QString(连接失败:\n%1).arg(e.what())); } }5. 断开连接void Widget::onDisconnectClicked() { try { m_readTimer-stop(); if (m_client m_client-isConnected()) { m_client-disconnect(); log(正在断开连接...); } m_client.reset(); m_connected false; updateUiState(); } catch (const std::exception e) { log(QString(断开连接异常: %1).arg(e.what())); } }6. 读取节点单个、批量同步、批量异步功能描述单个读取Modbus.Test.temp2和temp3节点的值批量同步读取temp至temp5五个节点批量异步读取相同节点并在回调中通过invokeMethod输出结果。void Widget::onReadClicked() { if (!m_client || !m_connected) { log(未连接到服务器); return; } try { // 解析节点 ID // opcua::NodeId nodeId opcua::NodeId::parse( // m_nodeIdEdit-text().toStdString() // ); // 单个读取 opcua::NodeId nodeId_2(2, Modbus.Test.temp2); opcua::Node node_2(*m_client,nodeId_2); opcua::Variant value_2 node_2.readValue(); QString result; if(value_2.isScalar()){ if(value_2.isType(opcua::getDataTypeshort())){ result QString::number(value_2.toshort()); } else { result (未知类型); } } else { result QString([数组 %1 项]).arg(value_2.arrayLength()); } opcua::NodeId nodeId_3(2,Modbus.Test.temp3); opcua::Node node_3(*m_client,nodeId_3); opcua::Variant value_3 node_3.readValue(); QString result_3; if(value_3.isScalar()){ if(value_3.isType(opcua::getDataTypeshort())){ result_3 QString::number(value_3.toshort()); } else { result_3 (未知类型); } } else { result_3 QString([数组 %1 项]).arg(value_3.arrayLength()); } log(QString(Modbus.Test.temp2 %1).arg(result)); log(QString(Modbus.Test.temp3 %1).arg(result_3)); /* opcua::ReadRequest 是什么OPC UA Read 服务的完整请求对象封装了批量读取的所有参数。 包含什么 opcua::ReadRequest request; 内部包含 - nodesToRead: 要读取的节点列表NodeId AttributeId - timestampsToReturn: 时间戳策略 - maxAge: 数据最大允许年龄缓存控制 - requestHeader: 请求头超时、认证等*/ /* * TimestampsToReturn 是什么控制服务器返回数据时是否附带时间戳。 值 含义 使用场景 Neither 不返回时间戳 最常用只需数据值 Source 只返回源时间戳 需要知道数据在设备端的产生时间 Server 只返回服务器时间戳 需要知道服务器接收数据的时间 Both 返回两个时间戳 审计、数据溯源、时序分析 时间戳区别 传感器采集 → [Source Timestamp] → OPC Server → [Server Timestamp] → 客户端 (设备产生时间) (服务器接收时间) * */ // 批量读取 std::vectoropcua::ReadValueId nodesToRead { {opcua::NodeId(2,Modbus.Test.temp),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp2),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp3),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp4),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp5),opcua::AttributeId::Value}, }; // 方法一直接调用批量读取服务 ReadResponse 读回复 opcua::ReadResponse response opcua::services::read(*m_client,nodesToRead,opcua::TimestampsToReturn::Neither); QString resultStr; for(const auto result : response.results()) { opcua::Variant value result.value(); if(value.isScalar()){ if(value.isType(opcua::getDataTypeshort())){ resultStr QString::number(value.toshort()); } else { resultStr (未知类型); } } else { resultStr QString([数组 %1 项]).arg(value.arrayLength()); } resultStr ; } log(QString(方法一 pcua::services::read 批量读取数据结果 %1).arg(resultStr)); // 方法二 异步读取 opcua::services::readAsync( *m_client, nodesToRead, opcua::TimestampsToReturn::Neither, [this](opcua::ReadResponse response) { // 回调函数 for (size_t i 0; i response.results().size(); i) { auto result response.results()[i]; if (result.status().isGood()) { auto value result.value(); QMetaObject::invokeMethod(this, [this, i, value]() { log(QString(异步读取节点 %1 读取完成 ,value %2).arg(i).arg(value.toshort())); }); } } } ); /* * * QMetaObject::invokeMethod( // 它是 Qt 的通用方法调用机制底层就是信号槽的实现原理。最常用场景是跨线程调用 UI。 QObject *context, // 目标对象决定在哪个线程执行 Functor functor, // 要执行的函数/lambda Qt::ConnectionType type Qt::AutoConnection // 连接类型可选 ); * / /* * * opcua::ReadValueId 它是 OPC UA Read 服务中的读取描述符用来告诉服务器“我要读哪个节点的哪个属性”。 构造函数 ReadValueId( NodeId nodeId, // [必填] 节点 ID AttributeId attributeId, // [必填] 要读的属性 std::string_view indexRange {}, // [选填] 数组索引范围 QualifiedName dataEncoding {} // [选填] 数据编码方式 ) 参数详解 参数 含义 示例 nodeId 目标节点 opcua::NodeId(2, Modbus.Test.temp) attributeId 要读的属性 AttributeId::Value值、AttributeId::DisplayName显示名 indexRange 数组切片范围 读全部、0:2读前 3 个元素、5读第 6 个 dataEncoding 编码格式 通常留空 {}仅用于复杂结构体 * */ } catch (const opcua::BadStatus e) { log(QString(读取失败: %1).arg(e.what())); } catch (const std::exception e) { log(QString(读取异常: %1).arg(e.what())); } }7. 写入节点批量写入功能描述向Modbus.Test.temp~temp5五个节点批量写入short类型数值111,122,133,144,155。void Widget::onAddNodeClicked() { if (!m_client || !m_connected) { log(未连接到服务器); return; } try { //批量写入 用 opcua::WriteRequest 方法 std::vectoropcua::WriteValue nodesToWrite; nodesToWrite.reserve(5); auto makeWriteItem [](const opcua::NodeId id, short value) { return opcua::WriteValue( id, opcua::AttributeId::Value, , // indexRange空字符串表示整个值 opcua::DataValue{opcua::Variant{value}} // 注意需要包装成 DataValue ); }; nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp), 111)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp2), 122)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp3), 133)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp4), 144)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp5), 155)); opcua::WriteResponse responseToWrite opcua::services::write(*m_client, nodesToWrite); log(QString(批量写入值成功)); } catch (const opcua::BadStatus e) { log(QString(写入失败: %1).arg(e.what())); } catch (const std::exception e) { log(QString(写入异常: %1).arg(e.what())); } }8. 开启订阅数据变化监控功能描述创建订阅设置发布间隔 1000ms为temp~temp5五个节点分别添加数据变化监控项回调中将变化值通过invokeMethod输出到日志。void Widget::onOpenSubscriptionClicked() { //创建订阅 if(!m_client || m_client-isConnected()false){ log(未连接到服务器无法创建订阅); return; } try { opcua::SubscriptionParameters params; params.publishingInterval 1000; // 1秒发布一次 //发布间隔 m_subscription std::make_uniqueopcua::Subscriptionopcua::Client(*m_client,params); // 2. 订阅多个节点的数据变化 std::vectorstd::pairopcua::NodeId, QString nodes { {opcua::NodeId(2, Modbus.Test.temp), temp}, {opcua::NodeId(2, Modbus.Test.temp2), temp2}, {opcua::NodeId(2, Modbus.Test.temp3), temp3}, {opcua::NodeId(2, Modbus.Test.temp4), temp4}, {opcua::NodeId(2, Modbus.Test.temp5), temp5}, }; for(const auto [nodeId, name] : nodes) { auto item m_subscription-subscribeDataChange( nodeId, opcua::AttributeId::Value, [this, name](opcua::IntegerId subId, opcua::IntegerId monId, const opcua::DataValue dv) { // 注意回调有 3 个参数 subId, monId, dv QMetaObject::invokeMethod(this, [this, name, dv]() { if (dv.hasValue()) { auto variant dv.value(); if (variant.isTypefloat()) { float val variant.tofloat(); log(QString( [%1] %2).arg(name).arg(val)); } else if (variant.isTypeshort()) { short val variant.toshort(); log(QString( [%1] %2).arg(name).arg(val)); } else { log(QString( [%1] (未知类型)).arg(name)); } } }); } ); /* 1. subId 和 monId 的作用 这两个 ID 是服务器返回的唯一标识符用于区分不同的订阅和监控项 参数 全称 含义 类比 subId Subscription ID 订阅 ID区分不同的订阅通道 类似微信群的 ID monId Monitored Item ID 监控项 ID区分群里的不同节点 类似群里某个成员的 ID 为什么需要它们 - 一个客户端可以创建多个订阅不同 subId - 一个订阅可以监控多个节点不同 monId - 回调触发时通过这两个 ID 就知道是哪个订阅的哪个节点数据变了 */ m_monitoredItems.push_back(std::move(item)); } log(QString(已订阅 %1 个节点).arg(m_monitoredItems.size())); m_OpenSubscription-setEnabled(false); m_CloseSubscription-setEnabled(true); } catch (const std::exception e) { log(QString(订阅失败: %1).arg(e.what())); } }9. 关闭订阅功能描述删除当前订阅清空监控项列表并更新按钮状态。void Widget::onCloseSubscriptionClicked() { if (m_subscription) { try { m_subscription-deleteSubscription(); log(订阅已删除); } catch (...) {} m_subscription.reset(); m_monitoredItems.clear(); m_OpenSubscription-setEnabled(true); m_CloseSubscription-setEnabled(false); } }10. 定时器驱动OPC UA 异步事件处理功能描述通过m_opcTimer每 50ms 调用m_client-runIterate(50)确保异步回调如订阅、异步读取得到及时处理。m_opcTimer new QTimer(this); m_opcTimer-start(50); // 50ms 调用一次保证回调及时触发 connect(m_opcTimer, QTimer::timeout, this, [this]() { if (m_client m_client-isConnected()) { m_client-runIterate(50); } });11. 周期读取定时器槽函数空实现功能描述m_readTimer超时时调用但当前为空函数预留用于周期读取扩展。void Widget::onTimerTick() { }完整代码#ifndef WIDGET_H #define WIDGET_H #include QLabel #include QLineEdit #include QPushButton #include QTextEdit #include QWidget #include open62541pp/open62541pp.hpp class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent nullptr); ~Widget(); private slots: void onConnectClicked(); void onDisconnectClicked(); void onReadClicked(); void onAddNodeClicked(); void onTimerTick(); void onOpenSubscriptionClicked(); void onCloseSubscriptionClicked(); private: void log(const QString msg); void updateUiState(); // UI 组件 QTextEdit *m_logEdit; QLineEdit *m_serverUrlEdit; QLineEdit *m_usernameEdit; QLineEdit *m_passwordEdit; QLineEdit *m_nodeIdEdit; QPushButton *m_connectBtn; QPushButton *m_disconnectBtn; QPushButton *m_readBtn; QPushButton *m_addNodeBtn; QLabel *m_statusLabel; QPushButton *m_OpenSubscription; //开启订阅 QPushButton *m_CloseSubscription; //关闭订阅 // OPC UA 客户端 std::unique_ptropcua::Client m_client; QTimer *m_readTimer; bool m_connected; QTimer *m_opcTimer; std::unique_ptropcua::Subscriptionopcua::Client m_subscription; std::vectoropcua::MonitoredItemopcua::Client m_monitoredItems; }; #endif // WIDGET_H #include widget.h #include open62541pp/open62541pp.hpp #include QFormLayout #include QHBoxLayout #include QMessageBox #include QTimer #include QDateTime Widget::Widget(QWidget *parent) : QWidget(parent) , m_connected(false) { setWindowTitle(OPC UA Qt Demo); resize(700, 550); // 中央组件 // auto *central new QWidget(this); // setCentralWidget(central); auto *mainLayout new QVBoxLayout(this); // 连接配置区域 auto *configGroup new QWidget(this); auto *configLayout new QFormLayout(configGroup); m_serverUrlEdit new QLineEdit(opc.tcp://127.0.0.1:49320, this); m_usernameEdit new QLineEdit(zlg, this); m_passwordEdit new QLineEdit(123456, this); m_passwordEdit-setEchoMode(QLineEdit::Password); configLayout-addRow(服务器地址:, m_serverUrlEdit); configLayout-addRow(用户名:, m_usernameEdit); configLayout-addRow(密码:, m_passwordEdit); mainLayout-addWidget(configGroup); // 按钮区域 auto *btnLayout new QHBoxLayout(); m_connectBtn new QPushButton(连接, this); m_disconnectBtn new QPushButton(断开, this); m_disconnectBtn-setEnabled(false); m_readBtn new QPushButton(读取节点, this); m_readBtn-setEnabled(false); m_addNodeBtn new QPushButton(写入节点, this); m_addNodeBtn-setEnabled(false); m_OpenSubscription new QPushButton(开启订阅,this); m_OpenSubscription-setEnabled(true); m_CloseSubscription new QPushButton(关闭订阅,this); m_CloseSubscription-setEnabled(false); btnLayout-addWidget(m_connectBtn); btnLayout-addWidget(m_disconnectBtn); btnLayout-addStretch(); btnLayout-addWidget(m_readBtn); btnLayout-addWidget(m_addNodeBtn); btnLayout-addSpacing(20); btnLayout-addWidget(m_OpenSubscription); btnLayout-addWidget(m_CloseSubscription); mainLayout-addLayout(btnLayout); // // 节点 ID 输入 // auto *nodeLayout new QHBoxLayout(); // nodeLayout-addWidget(new QLabel(节点 ID:, this)); // m_nodeIdEdit new QLineEdit(ns0;i2258, this); // nodeLayout-addWidget(m_nodeIdEdit); // mainLayout-addLayout(nodeLayout); // 状态栏 m_statusLabel new QLabel(未连接, this); mainLayout-addWidget(m_statusLabel); // 日志输出 m_logEdit new QTextEdit(this); m_logEdit-setReadOnly(true); mainLayout-addWidget(m_logEdit); // 定时器用于周期读取 m_readTimer new QTimer(this); m_readTimer-setInterval(1000); // 信号槽连接 connect(m_connectBtn, QPushButton::clicked, this, Widget::onConnectClicked); connect(m_disconnectBtn, QPushButton::clicked, this, Widget::onDisconnectClicked); connect(m_readBtn, QPushButton::clicked, this, Widget::onReadClicked); connect(m_addNodeBtn, QPushButton::clicked, this, Widget::onAddNodeClicked); connect(m_readTimer, QTimer::timeout, this, Widget::onTimerTick); connect(m_OpenSubscription,QPushButton::clicked,this,Widget::onOpenSubscriptionClicked); connect(m_CloseSubscription,QPushButton::clicked,this,Widget::onCloseSubscriptionClicked); m_opcTimer new QTimer(this); m_opcTimer-start(50); // 50ms 调用一次保证回调及时触发 connect(m_opcTimer, QTimer::timeout, this, [this]() { if (m_client m_client-isConnected()) { m_client-runIterate(50); } }); log(OPC UA Qt Demo 已启动); log(默认用户名: zlg / 密码: 123456); } Widget::~Widget() default; void Widget::log(const QString msg) { m_logEdit-append(QString([%1] %2) .arg(QDateTime::currentDateTime().toString(hh:mm:ss)) .arg(msg)); } void Widget::updateUiState() { m_connectBtn-setEnabled(!m_connected); m_disconnectBtn-setEnabled(m_connected); m_readBtn-setEnabled(m_connected); m_addNodeBtn-setEnabled(m_connected); m_serverUrlEdit-setEnabled(!m_connected); m_usernameEdit-setEnabled(!m_connected); m_passwordEdit-setEnabled(!m_connected); m_statusLabel-setText(m_connected ? 已连接 : 未连接); } void Widget::onConnectClicked() { try { // 创建客户端配置 opcua::ClientConfig config; // 设置用户名/密码认证 opcua::UserNameIdentityToken token( opcua::String(m_usernameEdit-text().toStdString()), opcua::String(m_passwordEdit-text().toStdString()) ); config.setUserIdentityToken(token); // 创建客户端 // m_client std::make_uniqueopcua::Client(std::move(config)); m_client std::make_uniqueopcua::Client(); //m_client-config().setUserIdentityToken(token); // 设置状态回调 m_client-onConnected([this]() { QMetaObject::invokeMethod(this, [this]() { log(已连接到服务器); m_connected true; updateUiState(); }); }); m_client-onDisconnected([this]() { QMetaObject::invokeMethod(this, [this]() { log(已断开连接); m_connected false; updateUiState(); }); }); // 连接服务器 std::string url m_serverUrlEdit-text().toStdString(); log(QString(正在连接 %1 ...).arg(m_serverUrlEdit-text())); m_client-connect(url); // 启动周期读取 m_readTimer-start(); } catch (const opcua::BadStatus e) { log(QString(连接失败: %1).arg(e.what())); QMessageBox::critical(this, 错误, QString(连接失败:\n%1).arg(e.what())); } catch (const std::exception e) { log(QString(连接失败: %1).arg(e.what())); QMessageBox::critical(this, 错误, QString(连接失败:\n%1).arg(e.what())); } } void Widget::onDisconnectClicked() { try { m_readTimer-stop(); if (m_client m_client-isConnected()) { m_client-disconnect(); log(正在断开连接...); } m_client.reset(); m_connected false; updateUiState(); } catch (const std::exception e) { log(QString(断开连接异常: %1).arg(e.what())); } } void Widget::onReadClicked() { if (!m_client || !m_connected) { log(未连接到服务器); return; } try { // 解析节点 ID // opcua::NodeId nodeId opcua::NodeId::parse( // m_nodeIdEdit-text().toStdString() // ); // 单个读取 opcua::NodeId nodeId_2(2, Modbus.Test.temp2); opcua::Node node_2(*m_client,nodeId_2); opcua::Variant value_2 node_2.readValue(); QString result; if(value_2.isScalar()){ if(value_2.isType(opcua::getDataTypeshort())){ result QString::number(value_2.toshort()); } else { result (未知类型); } } else { result QString([数组 %1 项]).arg(value_2.arrayLength()); } opcua::NodeId nodeId_3(2,Modbus.Test.temp3); opcua::Node node_3(*m_client,nodeId_3); opcua::Variant value_3 node_3.readValue(); QString result_3; if(value_3.isScalar()){ if(value_3.isType(opcua::getDataTypeshort())){ result_3 QString::number(value_3.toshort()); } else { result_3 (未知类型); } } else { result_3 QString([数组 %1 项]).arg(value_3.arrayLength()); } log(QString(Modbus.Test.temp2 %1).arg(result)); log(QString(Modbus.Test.temp3 %1).arg(result_3)); /* opcua::ReadRequest 是什么OPC UA Read 服务的完整请求对象封装了批量读取的所有参数。 包含什么 opcua::ReadRequest request; 内部包含 - nodesToRead: 要读取的节点列表NodeId AttributeId - timestampsToReturn: 时间戳策略 - maxAge: 数据最大允许年龄缓存控制 - requestHeader: 请求头超时、认证等*/ /* * TimestampsToReturn 是什么控制服务器返回数据时是否附带时间戳。 值 含义 使用场景 Neither 不返回时间戳 最常用只需数据值 Source 只返回源时间戳 需要知道数据在设备端的产生时间 Server 只返回服务器时间戳 需要知道服务器接收数据的时间 Both 返回两个时间戳 审计、数据溯源、时序分析 时间戳区别 传感器采集 → [Source Timestamp] → OPC Server → [Server Timestamp] → 客户端 (设备产生时间) (服务器接收时间) * */ // 批量读取 std::vectoropcua::ReadValueId nodesToRead { {opcua::NodeId(2,Modbus.Test.temp),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp2),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp3),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp4),opcua::AttributeId::Value}, {opcua::NodeId(2,Modbus.Test.temp5),opcua::AttributeId::Value}, }; // 方法一直接调用批量读取服务 ReadResponse 读回复 opcua::ReadResponse response opcua::services::read(*m_client,nodesToRead,opcua::TimestampsToReturn::Neither); QString resultStr; for(const auto result : response.results()) { opcua::Variant value result.value(); if(value.isScalar()){ if(value.isType(opcua::getDataTypeshort())){ resultStr QString::number(value.toshort()); } else { resultStr (未知类型); } } else { resultStr QString([数组 %1 项]).arg(value.arrayLength()); } resultStr ; } log(QString(方法一 pcua::services::read 批量读取数据结果 %1).arg(resultStr)); // 方法二 异步读取 opcua::services::readAsync( *m_client, nodesToRead, opcua::TimestampsToReturn::Neither, [this](opcua::ReadResponse response) { // 回调函数 for (size_t i 0; i response.results().size(); i) { auto result response.results()[i]; if (result.status().isGood()) { auto value result.value(); QMetaObject::invokeMethod(this, [this, i, value]() { log(QString(异步读取节点 %1 读取完成 ,value %2).arg(i).arg(value.toshort())); }); } } } ); /* * * QMetaObject::invokeMethod( // 它是 Qt 的通用方法调用机制底层就是信号槽的实现原理。最常用场景是跨线程调用 UI。 QObject *context, // 目标对象决定在哪个线程执行 Functor functor, // 要执行的函数/lambda Qt::ConnectionType type Qt::AutoConnection // 连接类型可选 ); * / /* * * opcua::ReadValueId 它是 OPC UA Read 服务中的读取描述符用来告诉服务器“我要读哪个节点的哪个属性”。 构造函数 ReadValueId( NodeId nodeId, // [必填] 节点 ID AttributeId attributeId, // [必填] 要读的属性 std::string_view indexRange {}, // [选填] 数组索引范围 QualifiedName dataEncoding {} // [选填] 数据编码方式 ) 参数详解 参数 含义 示例 nodeId 目标节点 opcua::NodeId(2, Modbus.Test.temp) attributeId 要读的属性 AttributeId::Value值、AttributeId::DisplayName显示名 indexRange 数组切片范围 读全部、0:2读前 3 个元素、5读第 6 个 dataEncoding 编码格式 通常留空 {}仅用于复杂结构体 * */ } catch (const opcua::BadStatus e) { log(QString(读取失败: %1).arg(e.what())); } catch (const std::exception e) { log(QString(读取异常: %1).arg(e.what())); } } void Widget::onAddNodeClicked() { if (!m_client || !m_connected) { log(未连接到服务器); return; } try { //批量写入 用 opcua::WriteRequest 方法 std::vectoropcua::WriteValue nodesToWrite; nodesToWrite.reserve(5); auto makeWriteItem [](const opcua::NodeId id, short value) { return opcua::WriteValue( id, opcua::AttributeId::Value, , // indexRange空字符串表示整个值 opcua::DataValue{opcua::Variant{value}} // 注意需要包装成 DataValue ); }; nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp), 111)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp2), 122)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp3), 133)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp4), 144)); nodesToWrite.push_back(makeWriteItem(opcua::NodeId(2, Modbus.Test.temp5), 155)); opcua::WriteResponse responseToWrite opcua::services::write(*m_client, nodesToWrite); log(QString(批量写入值成功)); } catch (const opcua::BadStatus e) { log(QString(写入失败: %1).arg(e.what())); } catch (const std::exception e) { log(QString(写入异常: %1).arg(e.what())); } } void Widget::onTimerTick() { } void Widget::onOpenSubscriptionClicked() { //创建订阅 if(!m_client || m_client-isConnected()false){ log(未连接到服务器无法创建订阅); return; } try { opcua::SubscriptionParameters params; params.publishingInterval 1000; // 1秒发布一次 //发布间隔 m_subscription std::make_uniqueopcua::Subscriptionopcua::Client(*m_client,params); // 2. 订阅多个节点的数据变化 std::vectorstd::pairopcua::NodeId, QString nodes { {opcua::NodeId(2, Modbus.Test.temp), temp}, {opcua::NodeId(2, Modbus.Test.temp2), temp2}, {opcua::NodeId(2, Modbus.Test.temp3), temp3}, {opcua::NodeId(2, Modbus.Test.temp4), temp4}, {opcua::NodeId(2, Modbus.Test.temp5), temp5}, }; for(const auto [nodeId, name] : nodes) { auto item m_subscription-subscribeDataChange( nodeId, opcua::AttributeId::Value, [this, name](opcua::IntegerId subId, opcua::IntegerId monId, const opcua::DataValue dv) { // 注意回调有 3 个参数 subId, monId, dv QMetaObject::invokeMethod(this, [this, name, dv]() { if (dv.hasValue()) { auto variant dv.value(); if (variant.isTypefloat()) { float val variant.tofloat(); log(QString( [%1] %2).arg(name).arg(val)); } else if (variant.isTypeshort()) { short val variant.toshort(); log(QString( [%1] %2).arg(name).arg(val)); } else { log(QString( [%1] (未知类型)).arg(name)); } } }); } ); /* 1. subId 和 monId 的作用 这两个 ID 是服务器返回的唯一标识符用于区分不同的订阅和监控项 参数 全称 含义 类比 subId Subscription ID 订阅 ID区分不同的订阅通道 类似微信群的 ID monId Monitored Item ID 监控项 ID区分群里的不同节点 类似群里某个成员的 ID 为什么需要它们 - 一个客户端可以创建多个订阅不同 subId - 一个订阅可以监控多个节点不同 monId - 回调触发时通过这两个 ID 就知道是哪个订阅的哪个节点数据变了 */ m_monitoredItems.push_back(std::move(item)); } log(QString(已订阅 %1 个节点).arg(m_monitoredItems.size())); m_OpenSubscription-setEnabled(false); m_CloseSubscription-setEnabled(true); } catch (const std::exception e) { log(QString(订阅失败: %1).arg(e.what())); } } void Widget::onCloseSubscriptionClicked() { if (m_subscription) { try { m_subscription-deleteSubscription(); log(订阅已删除); } catch (...) {} m_subscription.reset(); m_monitoredItems.clear(); m_OpenSubscription-setEnabled(true); m_CloseSubscription-setEnabled(false); } }