QT5.14串口调试助手:上位机接收数据解析数据帧+多通道波形显示+数据保存为csv文件

由于业务需要,在上个月做了一个关于qt的设计,在设计中主要需要解决的问题就是接收单片机采集到的数据并在上位机将数字实时的通过波形显示出来,然后上位机要有保存下数据文件的功能,便于后续的软件读取数据做进一步的分析处理

QT第一步:安装软件环境

安装qt5.14,可以在这个网站下载安装包。
下载版本: qt-opensource-windows-x86-5.14.2.exe

安装时需要勾选MinGW 相关选项

安装教程不在重复赘述,网上有很多的例子

第二步:初始QT

qt作为一种开源的UI程序设计框架可以便捷的通过qt提供的各种组件以低代码的方式组件自己需要的ui界面,这对于初步入门的设计人员十分的友好,同时qt官方对每个类、方法、变量的文档说明都非常详细并且提供了实例代码入门非常简单。
安装好qt后直接使用qt官方的Qt Creator程序进行开发,当然你可以使用MSVS进行开发,这还需要在MSVC中安装一下qt的官方插件。我使用的是VS2019+qt5.14.2,在UI设计界面上VS和qt还存在一定的兼容性问题,有好多次出现闪退的问题。所以工程不是特别大的时候还是建议老老实实就用Qt Creator进行开发。
在这里插入图片描述
安装好后可以看到qt提供很多的模板程序,当然也可以都不使用,直接从空白模板开始我们的工程

第三步:了解信号与槽机制

Qt利用信号与槽(signals/slots)机制取代传统的callback来进行对象之间的沟通。当操作事件发生的时候,对象会发提交一个信号(signal);而槽(slot)则是一个函数接受特定信号并且执行槽本身设置的动作。信号与槽之间,则透过QObject的静态方法connect来链接。
信号在任何执行点上皆可发射,甚至可以在槽里再发射另一个信号,信号与槽的链接不限定为一对一的链接,一个信号可以链接到多个槽或多个信号链接到同一个槽,甚至信号也可连接到信号。
以往的callback缺乏类型安全,在调用处理函数时,无法确定是传递正确类型的参数。但信号和其接受的槽之间传递的资料类型必须要相符合,否则编译器会提出警告。信号和槽可接受任何数量、任何类型的参数,所以信号与槽机制是完全类型安全。

信号与槽机制也确保了低耦合性,发送信号的类别并不知道是哪个槽会接受,也就是说一个信号可以调用所有可用的槽。此机制会确保当在"连接"信号和槽时,槽会接受信号的参数并且正确执行。

上面的解释来自维基百科,说的简单点呢就是说:当你在qt界面中放置了一个按钮,当你运行程序并移动鼠标点击这个按钮的时候。点击按钮这个动作就是一个信号,当然有了信号我们就要执行命令,我们通过软件定义将这个信号连接上一个槽,这个槽函数执行点击动作所需要的对行功能。信号与槽可以是一一对应也可以是一对多、多对一。
在这里我们可以看到:

QObject::connect(ui->button, SIGNAL(clicked()), this, SLOT(senddata()));

上面这个函数就是一个槽函数,他把button按钮的点击信号链接到了发送数据的响应函数上,只要鼠标点击一次就会发送一次。

在这里插入图片描述
在qt中每一个对象都有对应的属性,这些属性的值就对应了这个对象的大小,相对位置,名称等等。

在使用qt时也可以选择pyqt,其中也同样拥有界面设计的功能,但以下的程序默认针对c++版本的qt

开始串口助手

这里默认你已经了解了基本的qt操作和c++语法
首先我们需要一个串口类,用来发送和接收数据;
然后我们需要 两个文本框和几个按钮来实现数据的接收和发送,并且设置串口通信的参数;
实现之后我们就需要设置针对串口数据的解析了:

定时接收串口数据

由于对电脑端接收数据很难做到硬件级的收中断,收到1bit数据就中断处理一次所以我们设置一个定时器,让程序检测当有数据来时就打开定时器开始定时,定时一段时间后关闭中断并接收保存这段时间内的所有数据。
这一段时间根据串口发送一帧数据的时间做合理设置,一般来时这个时候收到的数据里包含着好几帧完整的数据,整段数据的头和尾可能并不是我们设置的帧头和帧尾,所以我们需要从中解析出需要的数据:
这里我使用了关键字索引,从接收到的字符串中查找到第一个我设置的帧头数据,然后从这个位置开始向后继续检索第一个我设置的帧尾,如果帧长度满足要求则把这一段字符串从中提取出来做单独处理:

//串口接收数据帧格式为:帧头'*' 帧尾'#' 数字间间隔符号',' 符号全为英文格式
void Widget::Read_Date()
{
    int bufferlens = 0;     //帧长
    QString str = ui->Receive_text_window->toPlainText();
    timerserial->stop();//停止定时器,

    qDebug()<<buffer;

    QByteArray bufferbegin = "*";   //定义帧头
    int index=0;
    QByteArray bufferend = "#";     //定义帧尾
    int indexend = 0;
    QByteArray buffercashe;         //缓存数据

    index = buffer.indexOf(bufferbegin,index);  //查找帧头
    indexend = buffer.indexOf(bufferend,indexend);  //查找帧尾
    if((index<buffer.size())&&(indexend<buffer.size()))
    {
        bufferlens = indexend - index + 1;
        buffercashe = buffer.mid(index,bufferlens);
    }

    char recvdata[buffercashe.size()];
    memset(recvdata,0,sizeof(recvdata));
    memcpy(recvdata,buffercashe.data(),bufferlens-1);
    recvdata[buffercashe.size()-1]=35;      //# 的ascii是35
    //qDebug()<<"cash size: "<<buffercashe.size();
    //std::cout<<"recvdata size: "<< sizeof (recvdata)<<std::endl;
    //std::cout<<"recvdata : " <<recvdata<<std::endl;
    if(recvdata[0]=='*'&&recvdata[buffercashe.size()-1]=='#')   //帧检查
    {
        str_to_num(recvdata);       //更新数据并缓存到保存区
        str+="succeed:";
        str+=tr(buffercashe);
        str += "  ";
        ui->Receive_text_window->clear();
        ui->Receive_text_window->append(str);
    }
    else
    {
        str+="error! ";
        str+=tr(buffercashe);
        str += "  ";
        ui->Receive_text_window->clear();
        ui->Receive_text_window->append(str);
    }
    buffer.clear();
}

void Widget::serial_timerstart()
{
    timerserial->start(4);
    buffer.append(serialport->readAll());
}

在上面的程序中,当串口发现有数据进来则引用Widget::serial_timerstart()开始定时接收。当定时到之后开始处理接收到的数据。如果数据正常后则把数据保存到缓冲区并更新当前的波形数据。

波形显示

这里显示模型数据使用了很简单的QChart()实例,定义QSplineSeries()对象,然后不断的更新QSplineSeries对象的数据列表,做好坐标轴的处理后就可以实现出动态曲线的效果了。

//曲线设置初始化
void Widget::Chart_Init()
{
    //初始化QChart的实例
    chart = new QChart();
    //初始化QSplineSeries的实例
    lineSeries = new QSplineSeries();
    //设置曲线的名称
    lineSeries->setName("曲线1");
    //把曲线添加到QChart的实例chart中
    chart->addSeries(lineSeries);

    //声明并初始化X轴、两个Y轴
    QValueAxis *axisX = new QValueAxis();
    QValueAxis *axisY = new QValueAxis();
    //设置坐标轴显示的范围
    axisX->setMin(0);
    axisX->setMax(MAX_X);
    axisY->setMin(0);
    axisY->setMax(MAX_Y);
    //设置坐标轴上的格点
    axisX->setTickCount(10);
    axisY->setTickCount(10);
    //设置坐标轴显示的名称
    QFont font("Microsoft YaHei",8,QFont::Normal);//微软雅黑。字体大小8
    axisX->setTitleFont(font);
    axisY->setTitleFont(font);
    axisX->setTitleText("X-时间");
    axisY->setTitleText("Y-角度");
    //设置网格不显示
    axisY->setGridLineVisible(false);
    //下方:Qt::AlignBottom,左边:Qt::AlignLeft
    //右边:Qt::AlignRight,上方:Qt::AlignTop
    chart->addAxis(axisX, Qt::AlignBottom);
    chart->addAxis(axisY, Qt::AlignLeft);
    //把曲线关联到坐标轴
    lineSeries->attachAxis(axisX);
    lineSeries->attachAxis(axisY);
    //把chart显示到窗口上
    ui->graphicsView->setChart(chart);
    ui->graphicsView->setRenderHint(QPainter::Antialiasing);      // 设置渲染:抗锯齿,如果不设置那么曲线就显得不平滑
}

//更新曲线函数
void Widget::DrawLine()
{
    if(count > MAX_X)
    {
        //当曲线上最早的点超出X轴的范围时,剔除最早的点,
        lineSeries->removePoints(0,lineSeries->count() - MAX_X);
        // 更新X轴的范围
        chart->axisX()->setMin(count - MAX_X);
        chart->axisX()->setMax(count);
    }
    else{
        chart->axisX()->setMin(0);
        chart->axisX()->setMax(MAX_X);
    }
    //增加新的点到曲线末端
    lineSeries->append(count, (int)Data.Sensor_1);
    count ++;
}

文件保存

首先是说明下csv文件的写入格式:以逗号作为分隔符,\n作为换行符。

那么就可以先写入一个表头,然后根据格式从缓存好的数据容器中逐条加载后逐行写入并打上时间戳。
下面的代码举例我们要保存的数据是五通道的。

/*
    函   数:SaveRecvDataFile
    描   述:保存数据按钮点击槽函数
    输   入:无
    输   出:无
*/
void Widget::SaveRecvDataFile()
{
    if(m_data.size()<1)
    {
        QMessageBox::information(this, "提示","当前数据为空");
        return;
    }
    serialport->clear();        //清空缓存区
    serialport->close();        //关闭串口
    timerDrawLine->stop();      //关闭波形刷新
    ui->send_button->setEnabled(false);		//禁用部分按键
    ui->open_port->setEnabled(true);
    ui->close_port->setEnabled(false);
    ui->save_data->setEnabled(false);

    QString csvFile = QFileDialog::getExistingDirectory(this);      //获取文件保存路径
    if(csvFile.isEmpty())
       return;
    QDateTime current_date_time =QDateTime::currentDateTime();      //获取系统时间
    QString current_date =current_date_time.toString("MM_dd_hh_mm");    //获取时间字符串
    csvFile += tr("/%1.csv").arg(current_date);
    qDebug()<< csvFile;
    QFile file(csvFile);
    if ( file.exists())
    {
            //如果文件存在执行的操作,此处为空,因为文件不可能存在
    }
    file.open( QIODevice::ReadWrite | QIODevice::Text );    //以读写模式读取文件
    QTextStream out(&file);
    out<<tr("Time,")<<tr("sensor1,")<<tr("sensor2,")<<tr("sensor3,")<<tr("sensor4,")<<tr("sensor5,\n");     //写入表头
    // 创建 CSV 文件
    for (const auto &data : m_data) {           //测试格式: *111,222,333,444,555#
        out << data << "\n";        //顺序将缓冲区数据写入文件
    }
    file.close();
    QVector<QString>().swap(m_data);        //清空缓存区数据
    QMessageBox::information(this, "提示","数据保存成功");
}

好的,那我们基本实现了最初的三个功能,下面附上一张完整效果的演示截图:

在这里插入图片描述

完整源码将在项目结束后公开在博客里,有需要的话可以点个关注哦

下次离开还是先见一面吧,即使什么也不说

猜你喜欢

转载自blog.csdn.net/weixin_47407066/article/details/130042313