码上通QT实战18--监控页面10-获取设备数据

1、前言

Modbus功能码概述

Modbus功能码是Modbus协议中用于定义操作类型的数字代码,指示从设备(如传感器、PLC)执行特定操作。功能码分为公共功能码、用户定义功能码和保留功能码三类,涵盖数据读写、诊断等操作。

常见公共功能码列表

读取操作

  • 01 (0x01):读取线圈状态(Read Coils)------获取离散输出(DO)或线圈的开关状态(1位数据)。
  • 02 (0x02):读取离散输入(Read Discrete Inputs)------获取离散输入(DI)的开关状态(1位数据)。
  • 03 (0x03):读取保持寄存器(Read Holding Registers)------获取保持寄存器(如PLC的4xxxx地址)中的16位数据。
  • 04 (0x04):读取输入寄存器(Read Input Registers)------获取输入寄存器(如3xxxx地址)中的16位数据。

写入操作

  • 05 (0x05):写单个线圈(Write Single Coil)------设置单个线圈的开关状态(ON/OFF)。
  • 06 (0x06):写单个寄存器(Write Single Register)------修改单个保持寄存器的值。
  • 15 (0x0F):写多个线圈(Write Multiple Coils)------批量设置多个线圈状态。
  • 16 (0x10):写多个寄存器(Write Multiple Registers)------批量修改多个保持寄存器的值。

诊断与异常

  • 08 (0x08):诊断(Diagnostics)------用于通信测试或设备自检。
  • 异常响应:当主站请求出错时,从站返回功能码+0x80,并附加异常代码(如01表示非法功能码)。

用户定义与保留功能码

  • 65-72 (0x41-0x48)100-110 (0x64-0x6E):用户自定义功能码,由设备厂商自定义用途。
  • 其他未列出的功能码为保留码,通常不建议使用。

功能码应用示例

  • 读取温度值:使用功能码04读取输入寄存器中的传感器数据。
  • 控制继电器:使用功能码05或15写入线圈状态以开关继电器。

注意事项

  • 功能码的具体实现可能因设备厂商而异,需参考设备文档。
  • Modbus TCP和RTU使用相同的功能码,但报文封装格式不同。
  • 异常代码需结合功能码解析,例如0x83表示03功能码请求的寄存器地址无效。

2、这么干

1、数据解析

2、通讯流程

3、代码解析

Modbus功能码解析

Modbus协议中,功能码用于定义客户端请求的操作类型或服务器响应的操作结果。功能码长度为1字节(0x01-0xFF),分为公共功能码、用户自定义功能码和保留功能码三类。以下是常见功能码及其解析:

公共功能码(0x01-0x08)

0x01 - 读取线圈状态

读取远程设备的线圈(DO)状态。请求帧需指定起始地址和线圈数量,响应返回每个线圈的ON/OFF状态(1字节=8线圈,高位在前)。

0x02 - 读取离散输入状态

读取远程设备的离散输入(DI)状态。格式与0x01类似,但仅适用于只读输入。

0x03 - 读取保持寄存器

读取保持寄存器(如AI或可设置参数)的值。请求需指定寄存器起始地址和数量,响应返回寄存器数据(每寄存器2字节)。

0x04 - 读取输入寄存器

读取输入寄存器(如只读AI)的值。格式与0x03相同,但针对只读寄存器。

0x05 - 写单个线圈

强制单个线圈为ON或OFF。请求帧包含目标地址和强制值(0x0000=OFF,0xFF00=ON)。

0x06 - 写单个寄存器

写入单个保持寄存器的值。请求帧包含目标地址和待写入数据(2字节)。

0x07 - 读取异常状态(仅串行链路)

获取设备的8个异常状态位,通常用于诊断。

0x08 - 回环测试(仅串行链路)

用于通信测试,设备需原样返回接收到的数据。

扩展功能码(0x0F-0x10)

0x0F - 写多个线圈

批量设置多个线圈状态。请求帧需包含起始地址、线圈数量、字节计数及线圈值(按位打包)。

0x10 - 写多个寄存器

批量写入多个寄存器的值。请求帧需包含起始地址、寄存器数量、字节计数及寄存器数据(2字节/寄存器)。

异常响应

当服务器检测到错误时,返回异常响应帧:原功能码+0x80,并附加异常码(如0x01=非法功能码,0x02=非法数据地址)。

cpp 复制代码
#include "mainwindow.h"
#include "systemutils.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QMouseEvent>
#include <QThread>
#include <QtConcurrent/QtConcurrentRun>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 1.设置窗口无边框和窗口透明
    this->setWindowFlags(Qt::FramelessWindowHint);
    this->setAttribute(Qt::WA_TranslucentBackground);


    // 设置相关阴影效果
    SystemUtils * utils=new SystemUtils();
    utils->SetDropShadowEffect(ui->wdg_root,Qt::gray,10);

    //设置关闭,最小,最大的图标
    // 设置相关字体图标
    QFont font=QFont(QString("zx_icons"),9);
    // 关闭
    ui->pb_close->setFont(font);
    ui->pb_close->setText(QChar(0xe653));
    // 最大化
    ui->pb_max->setFont(font);
    ui->pb_max->setText(QChar(0xe694));
    // 最小化
    ui->pb_min->setFont(font);
    ui->pb_min->setText(QChar(0xe7e6));
    //设置第二行消息的图标
    ui->lbl_message_icon->setFont(font);
    ui->lbl_message_icon->setText(QChar(0xe7ff));

    //设置标题字体
    QFont font2=QFont(QString("钉钉进步体"),14);
    ui->lbl_title->setFont(font2);

    //设置"实时监控"按钮的信号和槽函数
    connect(ui->nb_monitor,SIGNAL(onClicked(int)),SLOT(onNavClicked(int)));
    //设置"趋势图表"导航按钮的信号和槽函数
    connect(ui->nb_trend,SIGNAL(onClicked(int)),SLOT(onNavClicked(int)));
    //设置"异常报警"导航按钮的信号和槽函数
    connect(ui->nb_alarm,SIGNAL(onClicked(int)),SLOT(onNavClicked(int)));
    //设置"系统设置"导航按钮的信号和槽函数
    connect(ui->nb_settings,SIGNAL(onClicked(int)),SLOT(onNavClicked(int)));

    //绑定监控页面Monitor的信号和槽函数关联,即monitor页面中的信号函数onConnect由本页面的槽函数onConnect响应处理
    connect(ui->Monitor,SIGNAL(onConnect(QString,QString,QString,QString,QString)),SLOT(onConnect(QString,QString,QString,QString,QString)));

    //创建线程及对象,处理串口
    thread=new QThread();
    worker=new QWorker();
    //1、本页面的信号函数onOpen由worker对象的槽函数openPort响应处理
    connect(this,SIGNAL(onOpen(QString,QString,QString,QString,QString)),worker,SLOT(openPort(QString,QString,QString,QString,QString)));
    //2、worker对象中的信号函数openCompleted由本页面的onSetopenState槽函数响应处理
    connect(worker,SIGNAL(openCompleted(bool)),this,SLOT(onSetopenState(bool)));
    //3、本页面的信号函数onSend由worker对象的槽函数sendData响应处理
    connect(this,SIGNAL(onSend(QByteArray,int)),worker,SLOT(sendData(QByteArray,int)));
    //4、worker对象中的信号函数sendCompleted由本页面的槽函数onSendCompleted响应处理
    connect(worker,SIGNAL(sendCompleted(QByteArray,int)),this,SLOT(onSendCompleted(QByteArray,int)));


    worker->moveToThread(thread);//把对象worker放在多线程中
    thread->start();//启动线程

    //f1=QtConcurrent::run(this,&MainWindow::onMonitor);//这是qt5.X的写法,这个写法在6.x中不支持
    //实例化异步任务
    f1 = QtConcurrent::run([this]() {
        this->onMonitor(); // 替换为你的成员函数名
    });
}

MainWindow::~MainWindow()
{
    delete ui;
}

//监控事件
void MainWindow::onMonitor()
{
    while(this->isMonitor){
        // 从设备中获取到相关数据
        QByteArray bytes;
        bytes.resize(6);
        bytes[0]=0x01;//设备地址
        bytes[1]=0x03;//功能码
        bytes[2]=0x00;//开始地址
        bytes[3]=0x00;
        bytes[4]=0x00;//请求数量
        bytes[5]=0x03;

        // CRC16算法
        auto crc=this->CRC16(bytes);
        uint8_t hi=uint8_t(crc>>8);
        uint8_t lo=uint8_t(crc);
        bytes.resize(8);
        bytes[6]=lo;//crc校验码
        bytes[7]=hi;

        // 需要给QWorker对象发送一个信号
        emit onSend(bytes,1);//发送信号
        QThread::msleep(5000);
    }
}

//鼠标按下事件
void MainWindow::mousePressEvent(QMouseEvent *event){
    if (event->button() == Qt::LeftButton) {  // 如果按下左边按钮
        m_drag = true;//表示要移动
        // 获取当前光标的位置
        m_dragPos = event->pos();
        // 当前鼠标点相对于桌面屏幕左上角的坐标(0,0),全局坐标;
        m_resizeDownPos = event->globalPosition().toPoint();
        // 获取当前窗口的相关参数,包括位置,大小等等各种参数
        m_mouseDownRect = this->rect();
    }
}

//鼠标移动事件
void MainWindow::mouseMoveEvent(QMouseEvent *event){
    // 如果是鼠标在拖动时,当前窗口是全屏,不做任何处理
    if (isFullScreen()) {
        return;
    }
    if (m_move) {
        move(event->globalPosition().toPoint() - m_dragPos);
        return;
    }

    setCursor(Qt::ArrowCursor);
    if (m_drag && (event->buttons() & Qt::LeftButton)) {
        m_move = true;
        move(event->globalPosition().toPoint() - m_dragPos);
    }
}

//鼠标释放事件
void MainWindow::mouseReleaseEvent(QMouseEvent *event){
    Q_UNUSED(event)
    m_drag = false;
    if (m_move) {
        m_move = false;
    }
    setCursor(Qt::ArrowCursor);
}

//关闭事件
void MainWindow::on_pb_close_clicked()
{
    QMessageBox::StandardButton ret=QMessageBox::question(this,tr("关闭系统运行"),tr("您确定要退出系统吗?"),QMessageBox::Yes | QMessageBox::No,QMessageBox::Yes );
    if(ret== QMessageBox::Yes){
        this->isMonitor=false;
        this->close();
    }

}
//最大化
void MainWindow::on_pb_max_clicked()
{
    this->showFullScreen();  // 全屏展示
}
//最小化
void MainWindow::on_pb_min_clicked()
{
    this->showMinimized();
}

//窗体双击事件
void MainWindow::mouseDoubleClickEvent(QMouseEvent *event)
{
    Q_UNUSED(event); // 标记参数未使用,消除警告
    if (isFullScreen()) {
        showNormal();  // 如果是全屏,就恢复到非全屏
    } else {
        showFullScreen();  // 否则就变成全屏的
    }
}

//导航按钮的点击响应事件
void MainWindow::onNavClicked(int index)
{
    ui->sw_pages->setCurrentIndex(index);
}

//连接串口设备
void MainWindow::onConnect(QString port, QString baud, QString parity, QString data, QString stop)
{
    //执行串口对象的连接动作,动作必须在后台线程中处理
    //触发信号,让qworker这个对象来接收信号
    emit onOpen(port,baud,parity,data,stop);
}

//串口连接状态
void MainWindow::onSetopenState(bool state)
{
    //接收到worker对象中的打开状态信号
    ui->Monitor->setOpenState(state);

}

//计算CRC校验码
uint16_t MainWindow::CRC16(QByteArray bytes)
{
    int len=bytes.size();
    uint16_t wcrc=0XFFFF;//预置16位crc寄存器,初值全部为1
    uint8_t temp;//定义中间变量
    int i=0,j=0;//定义计数

    for(i=0;i<len;i++)//循环计算每个数据
    {
        temp=bytes.at(i);
        wcrc^=temp;
        for(j=0;j<8;j++){
            //判断右移出的是不是1,如果是1则与多项式进行异或。
            if(wcrc&0X0001){
                wcrc>>=1;//先将数据右移一位
                wcrc^=0XA001;//与上面的多项式进行异或
            }
            else//如果不是1,则直接移出
                wcrc>>=1;//直接移出
        }
    }
    temp=wcrc;//crc的值
    return wcrc;
}


//发送完成后
void MainWindow::onSendCompleted(QByteArray bytes, int flag)
{
    if(bytes.length()>3&&(bytes[1]&0x80)==0){//这个条件说明返回的数据没有问题,这里只是一个简单的判断
        bool ok;
        //截取指定位置指定长度的数据,转换成16进制,再转换成10进制
        uint16_t temp=bytes.mid(3,2).toHex().toUInt(&ok,16);
        uint16_t humi=bytes.mid(5,2).toHex().toUInt(&ok,16);
        uint16_t bright=bytes.mid(7,2).toHex().toUInt(&ok,16);
        qDebug()<<"温度:"<<temp;
        qDebug()<<"湿度:"<<humi;
        qDebug()<<"亮度:"<<bright;
    }
}

4、运行测试

5、小结

Modbus协议中,0x01 - 读取线圈状态,0x02 - 读取离散输入状态,0x03 - 读取保持寄存器,0x04 - 读取输入寄存器,0x05 - 写单个线圈,0x06 - 写单个寄存器,0x0F - 写多个线圈,0x10 - 写多个寄存器

复制代码
原创不易,打字不易,截图不易,撸码不易,整理不易,走过路过,不要错过,欢迎点赞,收藏,转载,复制,抄袭,留言,灌水,请动动你的金手指,祝您早日实现财务自由。
相关推荐
星火开发设计2 小时前
C++ multiset 全面解析与实战指南
开发语言·数据结构·c++·学习·set·知识
lsx2024063 小时前
Eclipse 添加书签
开发语言
易营宝3 小时前
高效的跨境电商广告优化系统:易营宝广告投放实操指南
大数据·开发语言·人工智能·php
superman超哥3 小时前
路由的艺术:Rust Web 框架中的高效匹配与类型安全提取
开发语言·rust·编程语言·rust web框架·rust路由
hqwest3 小时前
码上通QT实战22--趋势页面01-准备图表对象
开发语言·qt·qpainter·qss·painevent·qt绘图事件
hqwest3 小时前
码上通QT实战23--趋势页面02-图表模拟数据
开发语言·qt·qpainter·qt绘图·绘制曲线
Echo缘4 小时前
关于在.cpp文件中包含c的头文件,编译报错问题
c语言·开发语言
人道领域4 小时前
【零基础学java】(反射)
java·开发语言
ghie90904 小时前
GPS抗干扰算法MATLAB实现
开发语言·算法·matlab