环境:Qt 6.6.3 (MinGW 11.2.0 64-bit)+QOPCUA 6.6.3
如果你不知道咋使用用这个库环境可以参考这个大佬的方法:网址
最近做了一个项目,想让一台鸿蒙的终端与PLC进行通讯,要实现不只是简单的信息交流还能够修改我PLC的OPCUA变量的节点值,自认为OPCUA和MQTT进行交互是比较简单的且好用的方法,所有就有了这玩意(虽然美化了一下ui但是还是好丑凑合看吧 ) ↓
这里是具体的ui名称对照
具体包含的变量和函数如下:
public:
Widget(QWidget *parent = nullptr);
~Widget();
//MQTT
QMqttClient *m_client = nullptr;
void updateLogStateChange();
void sendnodestateontime();
std::string recivemessage = "";
QString recivetopic = "";
//OPCUA
struct NodeStatus {
QString nodeid;
QString value;
};//存值结构体(没啥用)
void paintEvent(QPaintEvent *e);//重绘图函数
QStringList nodeIds;//存储需要更新到qcombobox的节点
QOpcUaClient *client = nullptr;//客户端的宏用于返回状态消息
QString n;
QOpcUaProvider provider;//后端的宏
QOpcUaNode *node = nullptr;//存储当前的节点对象
QString selectedText;//存储节点文本
int w_nodevalue=0;//存储写入节点的值
QTimer *timer = new QTimer(this);//qtimer对象
int nodeindex = 3;//父节点起始点
QSet<QString> uniqueValues;//使用Qset存储数据的唯一值用于处理实时节点更新
void addUniqueValue(const QString &nodeId, const QString &newValue);//实时更新状态到选择框和textedit
bool buttonstate=false;//存储测试按钮状态
void updateTextEditWithNewValue(QTextEdit* textEdit, const QString& nodeId, const QString& newValue);
void updateOrAddValue(QTextEdit* textEdit, const QString& nodeId, const QString& newValue);//实时更新节点状态到textedit
void extractNodeIds();//实时更新状态到选择框
void someFunction();//延迟函数,没用上
private slots:
//MQTT
// void uidateLogStateChange();
void showme();
void brokerDisconnected();//连接服务器处理槽函数
void setClientPort(int p);//设置客户端端口槽函数
void on_ui_connectbutton_clicked();//连接按钮槽函数
void on_ui_publishbutton_clicked();//发布按钮函数
void on_ui_subscribebutton_clicked();//订阅按钮槽函数
//OPCUA
void on_connect_clicked();//服务器链接按钮
// void on_readvalue_clicked();//单独读值函数(废弃)
void on_nodelistbox_activated(int index);//节点选择框状态改变槽函数
// void handleReadNodeAttributesFinished(const QList<QOpcUaReadResult> &results, QOpcUa::UaStatusCode serviceResult);
void handleWriteNodeAttributesFinished(const QList<QOpcUaWriteResult> &results, QOpcUa::UaStatusCode serviceResult);//修改值处理槽函数
//void refreshstate(const QList<QOpcUaReadResult> &results, QOpcUa::UaStatusCode serviceResult);
void on_ui_confirmtochange_clicked();//确认更改值按钮
void readNodeVlue();//实时读取节点状态
void on_ui_getnodestate_clicked();//实时查询按钮
void handleReadNodeAttributesFinished1(const QList<QOpcUaReadResult> &results, QOpcUa::UaStatusCode serviceResult);//读值处理槽函数
void handleWriteNodeAttributesFinished2(const QList<QOpcUaWriteResult> &results, QOpcUa::UaStatusCode serviceResult);//bool测试处理函数
void on_ui_booltestbutton_clicked();//bool测试按钮
void on_ui_disconnectbutton_clicked();//断开服务器按钮
void handleReadNodeAttributesFinished4(const QList<QOpcUaReadResult> &results, QOpcUa::UaStatusCode serviceResult);//读值处理槽函数
为了防止递归,要把大部分的连接信号与槽的函数放在构造函数里,其中包括对mqtt收到的消息进行节点修改判断,使用substr函数记得对大小进行判断不然程序会出故障
#include "widget.h"
#include "ui_widget.h"
#include <QOpcUaProvider>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//MQTT
ui->ui_spinBoxport->setRange(0, 65535); // 允许输入 0 到 65535
m_client = new QMqttClient(this);
m_client->setHostname(ui->ui_lineEditHost->text());
m_client->setPort(ui->ui_spinBoxport->value());
QObject::connect(m_client,&QMqttClient::stateChanged,this,&Widget::updateLogStateChange);
QObject::connect(m_client,&QMqttClient::disconnected,this,&Widget::brokerDisconnected);
QObject::connect(m_client, &QMqttClient::messageReceived, this, [this](const QByteArray &message, const QMqttTopicName &topic) {
const QString content = QDateTime::currentDateTime().toString()
+ QLatin1String(" Received Topic: ")
+ topic.name()
+ QLatin1String(" Message: ")
+ message
+ QLatin1Char('\n');
ui->ui_editlog->insertPlainText(content);
recivemessage = message;
recivetopic = topic.name();
if(recivemessage == "R/NODESTATE")
{
sendnodestateontime();
}
else if (recivemessage.length() >= 2) {
std::string firstTwoChars = recivemessage.substr(0, 2);
std::string nodeid = recivemessage.substr(2,9);
std::string value = recivemessage.substr(12,2);
QString qnodeid = QString::fromStdString(nodeid);
QString qvalue = QString::fromStdString(value);
if(firstTwoChars == "W/")
{
QList<QOpcUaWriteItem> request;
if (ui->nodevalue->text()=="true")
{
request.append(QOpcUaWriteItem(qnodeid, QOpcUa::NodeAttribute::Value, qvalue, QOpcUa::Types::Boolean));
}
else if (ui->nodevalue->text()=="false")
{
request.append(QOpcUaWriteItem(qnodeid, QOpcUa::NodeAttribute::Value, qvalue, QOpcUa::Types::Boolean));
}
else
{
request.append(QOpcUaWriteItem(qnodeid, QOpcUa::NodeAttribute::Value, qvalue, QOpcUa::Types::Int16));
qDebug()<<"**************************";
}
qDebug()<<"**************************";
//发起写入请求
bool success = client->writeNodeAttributes(request);
if (!success) {
qDebug() << "Failed to initiate write operation";
ui->ui_statebox->setText("Failed to initiate write operation");
return;
}
// 连接信号到槽函数以处理写入结果
// disconnect(client, &QOpcUaClient::writeNodeAttributesFinished, this, &Widget::handleWriteNodeAttributesFinished);
connect(client, &QOpcUaClient::writeNodeAttributesFinished, this, &Widget::handleWriteNodeAttributesFinished4);
}
}
});
QObject::connect(m_client, &QMqttClient::pingResponseReceived, this, [this]() {
const QString content = QDateTime::currentDateTime().toString()
+ QLatin1String(" PingResponse")
+ QLatin1Char('\n');
ui->ui_editlog->insertPlainText(content);
});
connect(ui->ui_lineEditHost, &QLineEdit::textChanged, m_client, &QMqttClient::setHostname);
connect(ui->ui_spinBoxport, QOverload<int>::of(&QSpinBox::valueChanged), this, &Widget::setClientPort);
updateLogStateChange();
//OPCUA
ui->nodelistbox->setCurrentIndex(-1);
selectedText = ui->nodelistbox->currentText();
ui->singlelight->setStyleSheet("background-color:red;");//服务器初始状态
QObject::connect(ui->nodelistbox,QOverload<int>::of(&QComboBox::activated),this,&Widget::on_nodelistbox_activated);//节点信号的list
QObject::connect(ui->ui_getnodestate,&QPushButton::clicked,[this](){timer->start(1000);});
QObject::connect(timer,&QTimer::timeout,this,&Widget::readNodeVlue);
std::vector<std::pair<std::string,int>> nodeandvalue;
}
从基础的连接服务器按钮说起,参考官方的文档官网链接在此,基本就依葫芦画瓢即可
QOpcUaProvider provider;
if (provider.availableBackends().isEmpty())
return;
QOpcUaClient *client = provider.createClient(provider.availableBackends()[0]);
if (!client)
return;
// Connect to the stateChanged signal. Compatible slots of QObjects can be used instead of a lambda.
QObject::connect(client, &QOpcUaClient::stateChanged, [client](QOpcUaClient::ClientState state) {
qDebug() << "Client state changed:" << state;
if (state == QOpcUaClient::ClientState::Connected) {
QOpcUaNode *node = client->node("ns=0;i=84");
if (node)
qDebug() << "A node object has been created";
}
});
QObject::connect(client, &QOpcUaClient::endpointsRequestFinished,
[client](QList<QOpcUaEndpointDescription> endpoints) {
qDebug() << "Endpoints returned:" << endpoints.count();
if (endpoints.size())
client->connectToEndpoint(endpoints.first()); // Connect to the first endpoint in the list
});
client->requestEndpoints(QUrl("opc.tcp://127.0.0.1:4840")); // Request a list of endpoints from the server
我写的:
// 连接服务器按钮O
void Widget::on_connect_clicked()
{
QOpcUaProvider provider; // 创建实例
if (provider.availableBackends().isEmpty())
return;
if (!client) {
client = provider.createClient(provider.availableBackends()[0]);
if (!client)
return;
}//返回第一个后端标识符
QObject::connect(client, &QOpcUaClient::stateChanged, [this](QOpcUaClient::ClientState state) {
qDebug() << "Client state changed:" << state;
if (state == QOpcUaClient::ClientState::Connected) {
QOpcUaNode *node = client->node("ns=4;i=4");
ui->ui_statebox->setText("Connect Success");
if (node)
qDebug() << "A node object has been created";
ui->singlelight->setStyleSheet("background-color: green;");
}
// 修正else if语句
else if (state == QOpcUaClient::ClientState::Closing || state == QOpcUaClient::ClientState::Disconnected)
{
ui->singlelight->setStyleSheet("background-color:red");
ui->ui_statebox->setText("Connect Failed");
}
}); // 检查连接状态并且获取特定的节点返回状态
QObject::connect(client, &QOpcUaClient::endpointsRequestFinished,
[this](QList<QOpcUaEndpointDescription> endpoints) {
qDebug() << "Endpoints returned:" << endpoints.count();
if (!endpoints.isEmpty())
client->connectToEndpoint(endpoints.first()); // 收到请求,打印端点数并链接第一个端点
});
QString url = ui->ui_urlad->text();
client->requestEndpoints(QUrl(url)); // 发起请求
}
单独读值按钮,下面是个单独读值的方法,不过这样的效率有点低,我之前当测试用,现在废弃了,也是参考官方的例子,每发一个request都会返回一个result来得到你请求的参数,具体实现如下:
//读值按钮
// void Widget::on_readvalue_clicked() {
// // 声明请求列表并添加读取项
// QList<QOpcUaReadItem> request;
// request.push_back(QOpcUaReadItem(selectedText, QOpcUa::NodeAttribute::Value));
// // 发起读取请求
// bool success = client->readNodeAttributes(request);
// if (!success) {
// // 处理请求未成功发出的情况
// qDebug() << "Failed to initiate read operation";
// return;
// }
// // 断开可能已经存在的连接,避免重复连接
// disconnect(client, &QOpcUaClient::readNodeAttributesFinished, this, &Widget::handleReadNodeAttributesFinished);
// // 连接信号到槽函数以处理读取结果
// QObject::connect(client, &QOpcUaClient::readNodeAttributesFinished,this, &Widget::handleReadNodeAttributesFinished);
// }
//读值处理函数(废弃)
// void Widget::handleReadNodeAttributesFinished(const QList<QOpcUaReadResult> &results, QOpcUa::UaStatusCode serviceResult) {
// if (serviceResult == QOpcUa::UaStatusCode::Good) {
// const QOpcUaReadResult &result = results[0];
// if (result.statusCode() == QOpcUa::UaStatusCode::Good) {
// qDebug() << "Value read for :" << selectedText << "is" << result.value();
// QString strvalue = result.value().toString();//读值转成字符串显示
// //ui->nodevalue->setText(strvalue);
// QString node = selectedText;
// ui->ui_nodelable->setText(node);
// } else {
// qDebug() << "Failed to read value for '" << selectedText << "', error:" << result.statusCode();
// }
// } else {
// qDebug() << "Read operation failed, serviceResult:" << serviceResult;
// }
// }
修改节点值按钮,修改具体的节点值,并写入服务器,需要判断输入的是布尔值还是int值,具体实现代码如下:
// 写入确认槽按钮O
void Widget::on_ui_confirmtochange_clicked()
{
QList<QOpcUaWriteItem> request;
if (ui->nodevalue->text()=="true")
{
request.append(QOpcUaWriteItem(selectedText, QOpcUa::NodeAttribute::Value, ui->nodevalue->text(), QOpcUa::Types::Boolean));
}
else if (ui->nodevalue->text()=="false")
{
request.append(QOpcUaWriteItem(selectedText, QOpcUa::NodeAttribute::Value, ui->nodevalue->text(), QOpcUa::Types::Boolean));
}
else
{
w_nodevalue = ui->nodevalue->text().toInt();
request.append(QOpcUaWriteItem(selectedText, QOpcUa::NodeAttribute::Value, w_nodevalue, QOpcUa::Types::Int16));
}
qDebug()<<selectedText<<w_nodevalue<<"**************************";
// 发起写入请求
bool success = client->writeNodeAttributes(request);
if (!success) {
qDebug() << "Failed to initiate write operation";
ui->ui_statebox->setText("Failed to initiate write operation");
return;
}
// 连接信号到槽函数以处理写入结果
disconnect(client, &QOpcUaClient::writeNodeAttributesFinished, this, &Widget::handleWriteNodeAttributesFinished);
connect(client, &QOpcUaClient::writeNodeAttributesFinished, this, &Widget::handleWriteNodeAttributesFinished);
}
// 写入结果处理函数O
void Widget::handleWriteNodeAttributesFinished(const QList<QOpcUaWriteResult> &results, QOpcUa::UaStatusCode serviceResult)
{
if (serviceResult == QOpcUa::UaStatusCode::Good) {
// 写入成功
qDebug() << "Write operation succeeded";
ui->ui_statebox->setText( "Write operation succeeded");
} else {
// 写入失败,处理错误
qDebug() << "Write operation failed, serviceResult:" << serviceResult;
ui->ui_statebox->setText( "Write operation failed, serviceResult:"+serviceResult);
}
}
读取实时节点值按钮,这玩意比较麻烦,我查了官方文档半天也没查到资料能够让我一下获取所有节点的值,于是干脆自己写了一个,能够实现获取到服务器节点的实时值,不过我的情况有些特殊,我只需要获得一个命名空间下的所有分支节点的值即可,如果你想要用这种方法实现获取多个命名空间下的分支节点值,需要自己修改代码,但如果你的节点名称是字符串类,那么我这方法就不适用了;首先是用一个qtimer类的定时器,定时查询服务器的节点值:(方法可能比较蠢,你有更好的方法可以留言给我谢谢你)
void Widget::on_ui_getnodestate_clicked()
{
timer->start(500);
}
QObject::connect(timer,&QTimer::timeout,this,&Widget::readNodeVlue);//我放在构造函数里了
不断发送请求获取当前命名空间下的所有分支节点的节点值,查询一次,需要查询的分支节点就自加1,直到查询不到为止:
//读值O
void Widget::readNodeVlue()
{
QString nodeid = QString("ns=4;i=%1").arg(nodeindex);//设置根节点
QList<QOpcUaReadItem> request;
request.push_back(QOpcUaReadItem(nodeid,QOpcUa::NodeAttribute::Value));
bool success = client->readNodeAttributes(request);
if (!success) {
qDebug() << "Failed to initiate read operation for node:" << nodeid;
ui->ui_statebox->setText("Failed to initiate read operation for node:"+nodeid);
return;
}
// 递增索引,准备下一次查询
++nodeindex;
disconnect(client, &QOpcUaClient::readNodeAttributesFinished, this, &Widget::handleReadNodeAttributesFinished1);
// 连接信号到槽函数以处理读取结果
QObject::connect(client, &QOpcUaClient::readNodeAttributesFinished,this, &Widget::handleReadNodeAttributesFinished1);
}
请求处理函数,需要把节点号和节点值单独提取出来并且拼接,方便我显示到QTextEdit上,if判断是我一个bool类型的测试按钮,可以暂时不理;代码如下
// 读值处理函数(槽函数)O
void Widget::handleReadNodeAttributesFinished1(const QList<QOpcUaReadResult> &results, QOpcUa::UaStatusCode serviceResult) {
if (serviceResult == QOpcUa::UaStatusCode::Good) {
// 遍历所有读取结果
for (const QOpcUaReadResult &result : results) {
if (result.statusCode() == QOpcUa::UaStatusCode::Good) {
// 成功读取值,输出到 debug
QString nodeid = result.nodeId();
QString strValue = result.value().toString();
QString nas = nodeid+":"+strValue;
if(nas=="ns=4;i=3:false")
{
ui->ui_booltestbutton->setStyleSheet("background-color:red;");
buttonstate = false;
}
else if(nas=="ns=4;i=3:true")
{
ui->ui_booltestbutton->setStyleSheet("background-color:green;");
buttonstate = true;
}
addUniqueValue(nodeid,strValue);
//updateTextEditWithNewValue(ui->ui_nodestatevalue, nodeid, strValue);
} else {
// 读取特定节点失败,输出错误信息
qDebug() << "Failed to read value for node ID:" << result.nodeId() << ", error:" << result.statusCode();
nodeindex=3;
}
}
} else {
// 读取操作失败,输出服务结果错误
qDebug() << "Read operation failed with serviceResult:" << serviceResult;
ui->ui_statebox->setText("Read operation failed with serviceResult:"+serviceResult);
}
}
用下列这个特定的函数,将节点号和节点值一同更新到QTextEdit上
//用Qset函数筛除要更新的节点值的变化更新到qtextedit上O
void Widget::updateOrAddValue(QTextEdit* textEdit, const QString& nodeId, const QString& newValue) {
QString textToFind = nodeId + ":"; // 构建查找文本,假设格式为 "nodeId:"
bool found = false;
QTextCursor cursor(textEdit->document());
cursor.movePosition(QTextCursor::Start);
while (!cursor.atEnd()) {//查询到底
cursor.select(QTextCursor::LineUnderCursor);
if (cursor.selectedText().contains(textToFind)) {
// 找到包含节点ID的行,更新整行数值
cursor.removeSelectedText(); // 先删除选中的文本
n = nodeId;
QString nav2 = nodeId + ": " + newValue;
cursor.insertText(nav2); // 然后插入新文本
found = true;
break;
}
cursor.movePosition(QTextCursor::NextBlock);
}
if (!found) {
// 如果没有找到,添加新行到文本末尾
textEdit->append(nodeId + ": " + newValue);
}
}
我自己都可以实时获取到节点号和节点值,那我干脆就把这些获取到的节点号和值从QTextEdit上全部更新到我的Qcombobox上,用一个函数,能够使用正则表达式查询我需要的nodeid更新到我的QCombobox上,以方便我选择我具体要更改的节点,代码如下:
//正则表达式查询节点id添加到combobox控件O
void Widget::extractNodeIds() {
QStringList nodeIds;
// 正则表达式查找所有 nodeId
QRegularExpression nodeIdPattern("ns=4;i=\\d+");
QRegularExpressionMatchIterator matches = nodeIdPattern.globalMatch(ui->ui_nodestatevalue->toPlainText());
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
nodeIds.append(match.captured());
}
// 清空现有的 QComboBox 项目
ui->nodelistbox->clear();
// 添加新的项目到 QComboBox
ui->nodelistbox->addItems(nodeIds);
}
为了防止因为一直刷新所导致的选择不到想要的节点的问题,需要记住当前选择好的节点号,再赋值给它就解决了;代码如下:
//更新或添加新值到列表,更新id到qcomboboxO
void Widget::addUniqueValue(const QString& nodeId, const QString& newValue) {
// 更新或添加值到 QTextEdit
updateOrAddValue(ui->ui_nodestatevalue, nodeId, newValue);
// 更新 QComboBox 以包含新的 nodeId
QString currentSelectedText = ui->nodelistbox->currentText();
extractNodeIds();
ui->nodelistbox->setCurrentText(currentSelectedText);//为了防止刷新掉原来选中的节点
}
断开连接按钮,代码实现如下:
//断开连接按钮O
void Widget::on_ui_disconnectbutton_clicked()
{
if (client && client->state() != QOpcUaClient::ClientState::Disconnected) {
client->disconnectFromEndpoint(); // 断开连接
ui->ui_statebox->setText("Disconnected"); // 更新状态信息
ui->singlelight->setStyleSheet("background-color: gray;"); // 更新UI元素颜色
// 可以在这里添加其他需要在断开连接时执行的代码
}
}
代码写的有点狗屎,包括怎么获取节点名称我都没写出来,还用的UAexpert查询的名称,但是我写的软件对我来说够用了,见谅,你凑合看看吧,我太懒了,剩下的mqtt代码和基于ohos_mqtt的库和arkts语言编写的终端app的流程晚点再写,效果图如下: