码上通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 - 写多个寄存器

复制代码
原创不易,打字不易,截图不易,撸码不易,整理不易,走过路过,不要错过,欢迎点赞,收藏,转载,复制,抄袭,留言,灌水,请动动你的金手指,祝您早日实现财务自由。
相关推荐
用户805533698032 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner2 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz7 天前
QML Hello World 入门示例
qt
xcyxiner10 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner11 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner11 天前
DicomViewer (添加模型类)3
qt
xcyxiner12 天前
DicomViewer (目录调整) 2
qt
xcyxiner12 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00614 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术14 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript