使用QOPCUA和QTMQTT库编写的集成客户端软件(上)

环境: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的流程晚点再写,效果图如下:

  • 12
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值