概要

本项目为原创,代码免费开源,贴在最下方
本项目提供完整项目压缩包资源,可自行下载,同时欢迎交流

Qt版本6.81,项目类型:QMainWindow,用到的库有:QFileDialog、QMessageBox、QDebug、QString、QTimer、QSerialPort、QSerialPortInfo、QFile、QElapsedTimer、QtConcurrent、 QMutex,都是Qt自带的库,项目运行结果如下:

在这里插入图片描述

功能说明:参考项目运行界面显示的功能。

注:本项目在作者另一篇文章【Qt原创开源项目】多功能串口调试助手2.1 的基础上增加数据帧解析功能,目前只能解析特定的一种数据帧格式数据,具体格式如下图:

在这里插入图片描述
注:该格式是PSINS导航开发板输出的数据帧格式,开发板由西北工业大学严恭敏教授开发,这里不做过多介绍,后面将会更新专用于PSINS开发板的采数软件版本,以及功能更全面的通用版本。

项目组成

在这里插入图片描述

main.cpp文件不需要修改,Sport.pro需要加一行代码,见技术细节第一条。mainwindow.h和mainwindow.cpp的完整代码贴在最后面。

技术细节

基础功能实现的相关技术细节见先前版本文章:
【Qt原创开源项目】多功能串口调试助手1.0
【Qt原创开源项目】多功能串口调试助手2.1

  • 数据帧的识别与处理
void SerialProcessor::processReceivedData(const QByteArray &data)
{
    rxBuff.append(data); // 先将数据复制到缓冲区
    // 检测到的第一个帧头位于已接收到的数据中的位置
    int frameStart = -1;
    // 帧处理循环
    while(static_cast<size_t>(rxBuff.size()) >= sizeof(DataFrame))
    {
        for(int i = 0; i<= rxBuff.size() - static_cast<int>(sizeof(DataFrame)); i++)
        {
            if(static_cast<uint8_t>(rxBuff[i]) == 0xAA &&
                static_cast<uint8_t>(rxBuff[i+1]) == 0x55 &&
                static_cast<uint8_t>(rxBuff[i+2]) == 0xAA &&
                static_cast<uint8_t>(rxBuff[i+3]) == 0x56)
            {
                frameStart = i;
                break;
            }
        }

        // 找到有效帧头
        if(frameStart >=0)
        {
            // 检查是否包含完整一帧
            if(frameStart + sizeof(DataFrame) <= static_cast<size_t>(rxBuff.size()))
            {
                // 提取帧数据
                QByteArray frameData = rxBuff.mid(frameStart, sizeof(DataFrame));

                // 处理帧数据
                frameProcess(frameData);

                // 从缓冲区移除已处理数据
                rxBuff.remove(0, frameStart + sizeof(DataFrame));
            }
            else
            {
                // 数据不足一帧,等待更多数据
                // 移除帧头前的无用数据后跳出while循环,
                // 下一次满足进入while循环的数据正好是一帧完整数据。
                rxBuff.remove(0, frameStart);
                break;
            }
        }
        else
        {
            // 未找到帧头,消除无效数据
            int removeCount = rxBuff.size() - sizeof(DataFrame) + 1;
            if(removeCount > 0 && removeCount <= rxBuff.size())
            {
                rxBuff.remove(0, removeCount);
            }
            else
            {
                // 如果没有足够数据,只保留可能构成帧头的部分
                if(rxBuff.size() > 4)
                {
                    rxBuff.remove(0, rxBuff.size() - 4);
                }
                break;
            }
        }
    }
}

帧处理完成时发送完成信号给主线程,用于ui更新显示

void SerialProcessor::frameProcess(const QByteArray &frameData)
{
    const DataFrame* frame = reinterpret_cast<const DataFrame*>(frameData.constData());

/*  校验和功能不可用
    // 计算校验和(跳过帧头的4个字节)
    uint32_t calculatedChecksum = calculateCRC32(frameData.constData() + 4, sizeof(DataFrame) - 8);
    uint32_t receivedChecksum = qFromLittleEndian(frame->CheckSum);
    // 验证校验和
    if(calculatedChecksum != receivedChecksum)
    {
        qWarning() << "校验和错误!接收:" << receivedChecksum << "计算:" << calculatedChecksum;
        emit checksumError();
        return;
    }
*/
    // 拷贝帧数据
     memcpy(&m_currentFrame, frame, sizeof(DataFrame));

    // 发出帧处理完成信号
    emit frameProcessed();

}
  • 将经纬度信息的整数部分与小数部分(4个字节的16进制数据)合并成一个double型数据
double SerialProcessor::combinePosition(int32_t integer, uint32_t fraction) const
{
    return integer + static_cast<double>(fraction) / 100000000.0;
}
  • CRC32校验功能(暂不可用)
uint32_t SerialProcessor::calculateCRC32(const char* data, int length) const
{
    // 简单的CRC32实现(实际应用中应使用更健壮的算法)
    uint32_t crc = 0xFFFFFFFF;
    for(int i = 0; i < length; i++)
    {
        crc ^= static_cast<uint8_t>(data[i]);
        for(int j = 0; j < 8; j++)
        {
            if(crc & 1)
            {
                crc = (crc >> 1) ^ 0xEDB88320;
            }
            else
            {
                crc >>= 1;
            }
        }
    }
    return ~crc;
}
  • 一点小小的bug

存在bug:当初始界面放置在第二页(解析数据页)时,运行程序后只打开串口(不点击tabwidget的页头),会出现卡顿崩溃问题。
解决方法:将第一页的显示数据勾选框设置为默认关闭。
疑问:第一页默认显示,即使当第二页不显示,也会崩溃。
但这个问题只在刚运行程序时出现,如果运行程序后,切换一下页头,或者点击一下页头(会让另一页的checkbox取消勾选),再打开串口,程序可正常运行。
猜测:可能跟tabwiget的初始化设置有关。

完整代码

  • mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>
#include <QString>
#include <QTimer>
#include <QFile>
#include <QElapsedTimer> // 高精度计时模块
#include <QtConcurrent>  //并发编程模块
#include <QMutex> // 互斥锁

#include "serialprocessor.h"

QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

    friend void frameProcess(MainWindow *window, const QByteArray &frameData);

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    long receiveNum, sendNum;

private slots:
    void on_Key_open_COM_clicked();
    void on_Key_send_COM_clicked();
    void on_Key_send_COM_clear_clicked();
    void on_Key_receive_COM_clear_clicked();
    void on_Key_save_COM_clicked();
    void on_Key_clearBytes_clicked();
    void on_checkBox_timeSend_clicked();
    void COM_DataReceive();
    void on_Key_capture_COM_clicked();

    void onFrameProcessed();
    void onChecksumError();

// 点击下拉框刷新串口
    void on_tabWidget_tabBarClicked(int index);

public slots:
    bool eventFilter(QObject*, QEvent*);

private:
    Ui::MainWindow *ui;
    void UiInit();
    void String2Hex(QString str, QByteArray &senddata);
    char ConvertHexChar(char ch);
    QTimer* timeSend;

    const int MAX_DISPLAY_LINES = 30;

    bool isCapturing = false;       // 捕获状态标志
    QFile captureFile;              // 捕获文件对象
    QTextStream fileStream;         // 文件写入流
    QElapsedTimer captureTimer;     // 计时统计
    QString FilePath;               // 捕获数据保存路径
    qint64 totalCaptured = 0;       // 已捕获字节数
    QMutex fileMutex;               // 互斥锁成员变量

    void startCapture(const QString &fileName);
    void stopCapture();

    void frameDisplay();
    SerialProcessor *serialProcessor;

};
#endif // MAINWINDOW_H

  • mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "QSerialPort"      //串口
#include "QSerialPortInfo"  //串口信息

#include <QLabel>

/* 定义串口指针,实例化串口 */
QSerialPort* COM = new QSerialPort();

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

    UiInit();

    /* 数据接收信号槽链接 */
    // 谁发出信号,什么信号(成功读取的信号),谁接收信号,执行信号的槽
    connect(COM, SIGNAL(readyRead()), this, SLOT(COM_DataReceive()));

    serialProcessor = new SerialProcessor(this);
    connect(serialProcessor, &SerialProcessor::frameProcessed, this, &MainWindow::onFrameProcessed);
    connect(serialProcessor, &SerialProcessor::checksumError, this, &MainWindow::onChecksumError);
}

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


/* 发送数据 */
void MainWindow::on_Key_send_COM_clicked()
{
    QByteArray data_buf;
    QString data_str;

    data_buf = ui->textEdit->toPlainText().toLatin1();

    int count;
    // 发送成功返回发送字节长度,发送失败返回-1
    //count = COM->write(ui->textEdit->toPlainText().toLatin1()); // 串口写数据(发送),获取发送框的数据发送
    // 如果要正确发送中文字符,使用.toLocal8Bit()
    if( !data_buf.isEmpty())
    {
        data_str = QString(data_buf);
        if(ui->checkBox_send_Hex->isChecked())
        {
            QByteArray data_hex;
            String2Hex(data_str, data_hex);
            count = COM->write(data_hex);
        }
        else
        {
            count = COM->write(data_buf);
        }
        sendNum += count;
        ui->sendBytes->setText(QString::number(sendNum));
        ui->sendBytes->setAlignment(Qt::AlignCenter);
    }
}

/* 清空发送 */
void MainWindow::on_Key_send_COM_clear_clicked()
{
    ui->textEdit->clear(); // 清空发送区文本框
}

/* 串口开关 */
void MainWindow::on_Key_open_COM_clicked()
{
//    QSerialPort::BaudRate baudRate; // 波特率
    QSerialPort::DataBits dataBits; // 数据位
    QSerialPort::StopBits stopBits; // 停止位
    QSerialPort::Parity   checkBits;// 校验位

    if(ui->comboBox_dataBits->currentText() == "8")
        dataBits = QSerialPort::Data8;
    else if(ui->comboBox_dataBits->currentText() == "7")
        dataBits = QSerialPort::Data7;
    else if(ui->comboBox_dataBits->currentText() == "6")
        dataBits = QSerialPort::Data6;
    else if(ui->comboBox_dataBits->currentText() == "5")
        dataBits = QSerialPort::Data5;

    if(ui->comboBox_stopBits->currentText() == "1")
        stopBits = QSerialPort::OneStop;
    else if(ui->comboBox_stopBits->currentText() == "1.5")
        stopBits = QSerialPort::OneAndHalfStop;
    else if(ui->comboBox_stopBits->currentText() == "2")
        stopBits = QSerialPort::TwoStop;

    if(ui->comboBox_checkBits->currentText() == "无校验")
        checkBits = QSerialPort::NoParity;
    else if(ui->comboBox_checkBits->currentText() == "奇校验")
        checkBits = QSerialPort::OddParity;
    else if(ui->comboBox_checkBits->currentText() == "偶校验")
        checkBits = QSerialPort::EvenParity;

    // 串口属性初始化
    COM->setPortName(ui->comboBox_COMnumber->currentText());
    COM->setBaudRate(ui->comboBox_baudRate->currentText().toInt());
    //COM->setBaudRate(baudRate);
    COM->setDataBits(dataBits);
    COM->setStopBits(stopBits);
    COM->setParity(checkBits);

    // 串口操作——打开/关闭
    if(ui->Key_open_COM->text() == "打开串口")
    {
        if(COM->open(QIODevice::ReadWrite) == true) // 串口打开成功
        {
            ui->Key_send_COM->setEnabled(true);
            ui->checkBox_timeSend->setEnabled(true);
            ui->lineEdit->setEnabled(true);

            ui->Key_open_COM->setText("关闭串口");
            ui->Key_open_COM->setStyleSheet("background:lightgreen;border-right:2px solid gray;border-bottom:2px solid gray");
            ui->comStatus->setText("打 开");
            ui->comStatus->setAlignment(Qt::AlignCenter);
        }
        else
        {
            QMessageBox::critical(this, "错误提示", "打开串口失败!\n请检查串口连接是否正常");
        }
    }
    else if(ui->Key_open_COM->text() == "关闭串口")
    {
        ui->Key_send_COM->setEnabled(false);
        ui->checkBox_timeSend->setEnabled(false);
        ui->lineEdit->setEnabled(false);
        // 关闭定时发送
        if(ui->checkBox_timeSend->isChecked())
        {
            ui->checkBox_timeSend->setCheckState(Qt::Unchecked);
            timeSend->stop();
        }
        COM->close();
        ui->Key_open_COM->setText("打开串口");
        ui->Key_open_COM->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");
        ui->comStatus->setText("关 闭");
        ui->comStatus->setAlignment(Qt::AlignCenter);
        ui->statusbar->showMessage("串口已关闭");
    }
}

/* 接收数据 */
void MainWindow::COM_DataReceive()
{
    QByteArray data_buf; // 接收缓冲区
    QByteArray DATA_BUF; // 接收缓冲区(用于数据解析)
    QString data_str; // 定义一个字符变量

    data_buf = COM->readAll(); //接收数据存储到缓存区

    if( !data_buf.isEmpty() ) // 如果接收到数据
    {
        // 解析数据
        if(ui->checkBox_frDisplay->isChecked())
        {
            ui->statusbar->showMessage("解析数据中...");
            serialProcessor->processReceivedData(data_buf);
        }
        else
        {
            ui->statusbar->showMessage("接收数据中...");
        }

        data_str = tr(data_buf); //将接收数据从字节数据类型转为字符类型,用于显示
        if(ui->checkBox_receive_Hex->isChecked())
        {
            QString data_hex = data_buf.toHex().data();
            data_hex = data_hex.toUpper(); // 转化为大写
            QString data_hex_str;
            // 将data_hex按每两个字符一组进行分割
            for(int i=0; i < data_hex.length(); i+=2)
            {
                QString str_temp = data_hex.mid(i, 2);
                data_hex_str += str_temp;
                data_hex_str += ' ';
            }

            if(isCapturing) // 捕获十六进制数据
            {
                // 异步写入(使用QtConcurrent防止阻塞UI)
                QtConcurrent::run([this,data_buf,data_hex_str](){
                    QMutexLocker locker(&fileMutex);
                    QMutexLocker fileLocker(&fileMutex);
                    if(fileStream.device())
                    {
                        fileStream << data_hex_str;
                        totalCaptured += data_buf.size();
                    }
                });
            }
            if(ui->checkBox_rcDisplay->isChecked()) // 显示数据
            {
                ui->plainTextEdit->insertPlainText(data_hex_str); // 在文本框显示
                ui->plainTextEdit->moveCursor(QTextCursor::End);  // 光标移到最后一行
            }
        }
        else
        {
            if(isCapturing) // 捕获字符数据
            {
                // 异步写入(使用QtConcurrent防止阻塞UI)
                QtConcurrent::run([this,data_buf,data_str](){
                    QMutexLocker locker(&fileMutex);
                    QMutexLocker fileLocker(&fileMutex);
                    if(fileStream.device())
                    {
                        fileStream << data_str;
                        totalCaptured += data_buf.size();
                    }
                });
            }
            if(ui->checkBox_rcDisplay->isChecked()) // 显示数据
            {
            ui->plainTextEdit->insertPlainText(data_str);    // 在文本框显示
            ui->plainTextEdit->moveCursor(QTextCursor::End); // 光标移到最后一行
            }
        }
    }

    // 自动清理旧内容
    int lineCount = ui->plainTextEdit->document()->lineCount();
    if (lineCount > MAX_DISPLAY_LINES) {
        ui->plainTextEdit->clear();
        QTextCursor trimCursor(ui->plainTextEdit->document());
        trimCursor.setPosition(0);
        trimCursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor,
                                lineCount - MAX_DISPLAY_LINES/2);
        trimCursor.removeSelectedText();
    }

    receiveNum += data_buf.size();
    ui->receiveBytes->setText(QString::number(receiveNum));
    ui->receiveBytes->setAlignment(Qt::AlignCenter);

    // 捕获数据时,底部状态栏显示信息
    if(isCapturing)
    {
        QString msg = QString("已捕获 %1 字节").arg(totalCaptured);
        ui->statusbar->showMessage("正在捕获数据到:" + FilePath + "    " + msg);
    }
}

/* 清空接收 */
void MainWindow::on_Key_receive_COM_clear_clicked()
{
    ui->plainTextEdit->clear();
}

/* 保存数据 */
void MainWindow::on_Key_save_COM_clicked()
{
    // 第四个参数不写就表示所有文件
    QString filename = QFileDialog::getSaveFileName(this, "选择保存位置",
                                                    QCoreApplication::applicationFilePath(), ".txt");
    if(filename.isEmpty())
    {
        //QMessageBox::warning(this, "警告", "请选择一个文件夹");
    }
    else
    {
        QFile file(filename);
        file.open(QIODevice::WriteOnly);
        //ui->textEdit->toPlainText();
        QByteArray ba;
        // 把QString接在QByreArray后面,此时ba没东西,相当于将QByteArray转换成了QString
        ba.append(ui->plainTextEdit->toPlainText().toLatin1());
        file.write(ba);
        file.close();
    }
}

/* 给串口号下拉框添加事件过滤器,鼠标点击时刷新串口 */
bool MainWindow::eventFilter(QObject* object, QEvent* event)
{
    if(event->type() == QEvent::MouseButtonPress)
    {
        if(object == ui->comboBox_COMnumber)
        {
            QComboBox* combobox = qobject_cast<QComboBox* >(object);
            // combobox->clear();
            // combobox->addItem("list");

            combobox->clear();
            foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
            {
                ui->comboBox_COMnumber->addItem(info.portName());
            }

        }
    }
    return QWidget::eventFilter(object,event);
}

/* 清空计数 */
void MainWindow::on_Key_clearBytes_clicked()
{
    receiveNum = 0;
    sendNum = 0;
    ui->receiveBytes->setText(QString::number(receiveNum));
    ui->receiveBytes->setAlignment(Qt::AlignCenter);
    ui->sendBytes->setText(QString::number(sendNum));
    ui->sendBytes->setAlignment(Qt::AlignCenter);
}

void MainWindow::String2Hex(QString str, QByteArray &senddata)
{
    int hexdata,lowhexdata;
    int hexdatalen = 0;
    int len = str.length();
    senddata.resize(len/2);
    char lstr,hstr;
    for(int i=0; i<len; )
    {
        //char lstr,
        hstr=str[i].toLatin1();
        if(hstr == ' ')
        {
            i++;
            continue;
        }
        i++;
        if(i >= len)
            break;
        lstr = str[i].toLatin1();
        hexdata = ConvertHexChar(hstr);
        lowhexdata = ConvertHexChar(lstr);
        if((hexdata == 16) || (lowhexdata == 16))
            break;
        else
            hexdata = hexdata*16+lowhexdata;
        i++;
        senddata[hexdatalen] = (char)hexdata;
        hexdatalen++;
    }
    senddata.resize(hexdatalen);
}

char MainWindow::ConvertHexChar(char ch)
{
    if((ch >= '0') && (ch <= '9'))
        return ch-0x30;
    else if((ch >= 'A') && (ch <= 'F'))
        return ch-'A'+10;
    else if((ch >= 'a') && (ch <= 'f'))
        return ch-'a'+10;
    else return (-1);
}

/* 定时发送 */
void MainWindow::on_checkBox_timeSend_clicked()
{
    int state = ui->checkBox_timeSend->checkState();
    if(state == Qt::Checked) // 勾选
    {
        if(ui->lineEdit->text()==NULL)
        {
            QMessageBox::about(this, tr("提示"), tr("请设置时间间隔"));
            ui->checkBox_timeSend->setCheckState(Qt::Unchecked);
        }
        else
        {
            // 勾选定时发送时,发送按键无法选中
            ui->Key_send_COM->setEnabled(false);
            ui->lineEdit->setEnabled(false);
            int time=ui->lineEdit->text().toInt();
            timeSend = new QTimer();
            timeSend->setInterval(time);
            connect(timeSend, &QTimer::timeout, this, [=](){on_Key_send_COM_clicked();});
            timeSend->start();
        }
    }
    else // 取消勾选
    {
        timeSend->stop();
        ui->Key_send_COM->setEnabled(true);
        ui->lineEdit->setEnabled(true);
    }
}

/* 捕获数据 */
void MainWindow::on_Key_capture_COM_clicked()
{
    if(!isCapturing)
    {
        // 首次点击:选择文件开始捕获
        QString fileName = QFileDialog::getSaveFileName(this, tr("选择保存文件"), QCoreApplication::applicationFilePath(), "");
        FilePath = fileName;
        if(!fileName.isEmpty())
        {
            startCapture(fileName);
        }
    }
    else
    {
        // 再次点击:停止捕获
        stopCapture();
    }
}

/* 开始捕获 */
void MainWindow::startCapture(const QString &fileName)
{
    captureFile.setFileName(fileName);
    if(!captureFile.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QMessageBox::critical(this, "错误", "无法创建文件!");
        return;
    }

    isCapturing = true;
    fileStream.setDevice(&captureFile);
    captureTimer.start();
    totalCaptured = 0;

    ui->Key_capture_COM->setText("停止捕获");
    ui->Key_capture_COM->setStyleSheet("background:lightcoral");
    // ui->statusbar->showMessage("正在捕获数据到:" + fileName);
}
/* 停止捕获 */
void MainWindow::stopCapture()
{
    if(captureFile.isOpen())
    {
        fileStream.flush();
        captureFile.close();
    }

    isCapturing = false;
    qint64 elapsed = captureTimer.elapsed();

    QString msg = QString("数据捕获完成!共捕获 %1 字节,耗时 %2 秒").arg(totalCaptured).arg(elapsed/1000.0, 0, 'f', 1);

    ui->Key_capture_COM->setText("捕获数据");
    ui->Key_capture_COM->setStyleSheet("");
    ui->statusbar->showMessage(msg);
}

/* Ui界面初始化 */
void MainWindow::UiInit()
{
    this->setWindowTitle("Sport串口调试助手");

    // ui->groupBox->setStyleSheet("border:1px solid gray");
    // ui->groupBox_2->setStyleSheet("border:1px solid gray");
    // ui->groupBox_3->setStyleSheet("border:1px solid gray");
    // ui->groupBox_4->setStyleSheet("border:1px solid gray");

    ui->label->setStyleSheet("border:none");
    ui->label_2->setStyleSheet("border:none");
    ui->label_3->setStyleSheet("border:none");
    ui->label_4->setStyleSheet("border:none");
    ui->label_5->setStyleSheet("border:none");
    ui->label_6->setStyleSheet("border:none");
    ui->label_7->setStyleSheet("border:none");
    ui->label_8->setStyleSheet("border:none");
    ui->label_9->setStyleSheet("border:none");
    ui->checkBox_receive_Hex->setStyleSheet("border:none");
    ui->checkBox_send_Hex->setStyleSheet("border:none");
    ui->checkBox_timeSend->setStyleSheet("border:none");

    ui->Key_open_COM->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");
    ui->Key_clearBytes->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");
    ui->Key_save_COM->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");
    ui->Key_send_COM->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");
    ui->Key_send_COM_clear->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");
    ui->Key_receive_COM_clear->setStyleSheet("border-right:2px solid gray;border-bottom:2px solid gray");

    ui->Key_send_COM->setEnabled(false);
    ui->checkBox_timeSend->setEnabled(false);
    ui->lineEdit->setEnabled(false);

    receiveNum = 0;
    sendNum = 0;
    ui->receiveBytes->setText(QString::number(receiveNum));
    ui->receiveBytes->setAlignment(Qt::AlignCenter);
    ui->sendBytes->setText(QString::number(sendNum));
    ui->sendBytes->setAlignment(Qt::AlignCenter);

    ui->comboBox_COMnumber->installEventFilter(this);

    foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
    { // 添加串口名
        ui->comboBox_COMnumber->addItem(info.portName());
    }
    foreach(qint32 baud, QSerialPortInfo::standardBaudRates())
    { // 添加波特率
        ui->comboBox_baudRate->addItem(QString::number(baud));
    }
    ui->comboBox_baudRate->addItem("230400");
    ui->comboBox_baudRate->addItem("460800");
    ui->comboBox_baudRate->addItem("921600");
    ui->comboBox_baudRate->setCurrentText("460800");
}

/* 数据解析槽函数 */
void MainWindow::onFrameProcessed()
{
    frameDisplay();
}

/* 校验失败槽函数 */
void MainWindow::onChecksumError()
{
    ui->statusbar->showMessage("校验出现错误!");
}

/* 更新显示解析数据 */
void MainWindow::frameDisplay()
{
    DataFrame frame = serialProcessor->currentFrame();
    // 字节序转换,将数据从小端字节序转换为当前系统的字节序(默认下位机使用小端字节序)
    double insLat = serialProcessor->combinePosition(qFromLittleEndian(frame.Lat1), qFromLittleEndian(frame.Lat2));
    double insLon = serialProcessor->combinePosition(qFromLittleEndian(frame.Lon1), qFromLittleEndian(frame.Lon2));
    double gpsLat = serialProcessor->combinePosition(qFromLittleEndian(frame.GpsLat1), qFromLittleEndian(frame.GpsLat2));
    double gpsLon = serialProcessor->combinePosition(qFromLittleEndian(frame.GpsLon1), qFromLittleEndian(frame.GpsLon2));

    ui->label_Gx->setText(QString::number(qFromLittleEndian(frame.Gx), 'f', 4));
    ui->label_Gy->setText(QString::number(qFromLittleEndian(frame.Gy), 'f', 4));
    ui->label_Gz->setText(QString::number(qFromLittleEndian(frame.Gz), 'f', 4));
    ui->label_Ax->setText(QString::number(qFromLittleEndian(frame.Ax), 'f', 4));
    ui->label_Ay->setText(QString::number(qFromLittleEndian(frame.Ay), 'f', 4));
    ui->label_Az->setText(QString::number(qFromLittleEndian(frame.Az), 'f', 4));
    ui->label_Mx->setText(QString::number(qFromLittleEndian(frame.Mx), 'f', 4));
    ui->label_My->setText(QString::number(qFromLittleEndian(frame.My), 'f', 4));
    ui->label_Mz->setText(QString::number(qFromLittleEndian(frame.Mz), 'f', 4));
    ui->label_InsLon->setText(QString::number(insLon, 'f', 6));
    ui->label_InsLat->setText(QString::number(insLat, 'f', 6));
    ui->label_InsHgt->setText(QString::number(qFromLittleEndian(frame.Hgt), 'f', 6));
    ui->label_VE->setText(QString::number(qFromLittleEndian(frame.VE), 'f', 4));
    ui->label_VN->setText(QString::number(qFromLittleEndian(frame.VN), 'f', 4));
    ui->label_VU->setText(QString::number(qFromLittleEndian(frame.VU), 'f', 4));
    ui->label_GpsLon->setText(QString::number(gpsLon, 'f', 6));
    ui->label_GpsLat->setText(QString::number(gpsLat, 'f', 6));
    ui->label_GpsHgt->setText(QString::number(qFromLittleEndian(frame.GpsHgt), 'f', 6));
    ui->label_GpsVE->setText(QString::number(qFromLittleEndian(frame.GpsVE), 'f', 4));
    ui->label_GpsVN->setText(QString::number(qFromLittleEndian(frame.GpsVN), 'f', 4));
    ui->label_GpsVU->setText(QString::number(qFromLittleEndian(frame.GpsVU), 'f', 4));
    ui->label_Pitch->setText(QString::number(qFromLittleEndian(frame.Pitch), 'f', 4));
    ui->label_Roll->setText(QString::number(qFromLittleEndian(frame.Roll), 'f', 4));
    ui->label_Yaw->setText(QString::number(qFromLittleEndian(frame.Yaw), 'f', 4));

    ui->label_BaroHgt->setText(QString::number(qFromLittleEndian(frame.BaroHgt), 'f', 2));
    ui->label_Temp->setText(QString::number(qFromLittleEndian(frame.Temp), 'f', 1));
    //ui->label_TimeStamp->setText(QString::number(qFromLittleEndian(frame.TimeStamp), 'f', 1));
}

/* tabBar点击切换页面槽函数 */
void MainWindow::on_tabWidget_tabBarClicked(int index)
{
    switch(index)
    {
        case 0:
            ui->checkBox_rcDisplay->setChecked(true);
            ui->checkBox_frDisplay->setChecked(false);
            break;
        case 1:
            ui->checkBox_rcDisplay->setChecked(false);
            ui->checkBox_frDisplay->setChecked(true);
            break;
        default:
            break;

    }
}


  • serialprocessor.h
#ifndef SERIALPROCESSOR_H
#define SERIALPROCESSOR_H

#include <QObject>
#include <cstdint> // 包含标准整数类型定义
#include <QtEndian>

struct DataFrame{
    uint8_t Head;
    uint32_t TimeStamp;

    float Gx, Gy, Gz;
    float Ax, Ay, Az;
    float Mx, My, Mz;
    float BaroHgt;
    float Pitch, Roll, Yaw;
    float VE, VN, VU;
    float Lat1, Lat2, Lon1, Lon2, Hgt; // 惯导位置
    float GpsVE, GpsVN, GpsVU;
    float GpsLat1, GpsLat2, GpsLon1, GpsLon2, GpsHgt;
    float GpsStatus;
    float GpsDelay;
    float Temp;
    uint32_t CheckSum;
};

class SerialProcessor : public QObject
{
    Q_OBJECT
public:
    explicit SerialProcessor(QObject *parent = nullptr);
    // 处理接收到的原始串口数据
    void processReceivedData(const QByteArray &data);
    // 获取当前帧数据
    DataFrame currentFrame() const;
    // 合并整数部分和小数部分
    double combinePosition(int32_t integer, uint32_t fraction) const;

signals:
    // 当完整帧被解析时发出信号
    void frameProcessed();
    // 校验错误信号
    void checksumError();

private:
    // 处理一帧数据
    void frameProcess(const QByteArray &frameData);
    // 计算CRC32校验和
    uint32_t calculateCRC32(const char *data, int length) const;

    QByteArray rxBuff; //接收数据缓冲区
    DataFrame m_currentFrame; // 当前解析的帧数据
};

#endif // SERIALPROCESSOR_H

  • serialprocessor.cpp
#include "serialprocessor.h"
#include <QDebug>

SerialProcessor::SerialProcessor(QObject *parent) : QObject(parent)
{
    // 初始化数据帧
    memset(&m_currentFrame, 0, sizeof(DataFrame));
}

void SerialProcessor::processReceivedData(const QByteArray &data)
{
    rxBuff.append(data); // 先将数据复制到缓冲区
    // 检测到的第一个帧头位于已接收到的数据中的位置
    int frameStart = -1;
    // 帧处理循环
    while(static_cast<size_t>(rxBuff.size()) >= sizeof(DataFrame))
    {
        for(int i = 0; i<= rxBuff.size() - static_cast<int>(sizeof(DataFrame)); i++)
        {
            if(static_cast<uint8_t>(rxBuff[i]) == 0xAA &&
                static_cast<uint8_t>(rxBuff[i+1]) == 0x55 &&
                static_cast<uint8_t>(rxBuff[i+2]) == 0xAA &&
                static_cast<uint8_t>(rxBuff[i+3]) == 0x56)
            {
                frameStart = i;
                break;
            }
        }

        // 找到有效帧头
        if(frameStart >=0)
        {
            // 检查是否包含完整一帧
            if(frameStart + sizeof(DataFrame) <= static_cast<size_t>(rxBuff.size()))
            {
                // 提取帧数据
                QByteArray frameData = rxBuff.mid(frameStart, sizeof(DataFrame));

                // 处理帧数据
                frameProcess(frameData);

                // 从缓冲区移除已处理数据
                rxBuff.remove(0, frameStart + sizeof(DataFrame));
            }
            else
            {
                // 数据不足一帧,等待更多数据
                // 移除帧头前的无用数据后跳出while循环,
                // 下一次满足进入while循环的数据正好是一帧完整数据。
                rxBuff.remove(0, frameStart);
                break;
            }
        }
        else
        {
            // 未找到帧头,消除无效数据
            int removeCount = rxBuff.size() - sizeof(DataFrame) + 1;
            if(removeCount > 0 && removeCount <= rxBuff.size())
            {
                rxBuff.remove(0, removeCount);
            }
            else
            {
                // 如果没有足够数据,只保留可能构成帧头的部分
                if(rxBuff.size() > 4)
                {
                    rxBuff.remove(0, rxBuff.size() - 4);
                }
                break;
            }
        }
    }
}

void SerialProcessor::frameProcess(const QByteArray &frameData)
{
    const DataFrame* frame = reinterpret_cast<const DataFrame*>(frameData.constData());

/*  校验和功能不可用
    // 计算校验和(跳过帧头的4个字节)
    uint32_t calculatedChecksum = calculateCRC32(frameData.constData() + 4, sizeof(DataFrame) - 8);
    uint32_t receivedChecksum = qFromLittleEndian(frame->CheckSum);
    // 验证校验和
    if(calculatedChecksum != receivedChecksum)
    {
        qWarning() << "校验和错误!接收:" << receivedChecksum << "计算:" << calculatedChecksum;
        emit checksumError();
        return;
    }
*/
    // 拷贝帧数据
     memcpy(&m_currentFrame, frame, sizeof(DataFrame));

    // 发出帧处理完成信号
    emit frameProcessed();

}

double SerialProcessor::combinePosition(int32_t integer, uint32_t fraction) const
{
    return integer + static_cast<double>(fraction) / 100000000.0;
}

DataFrame SerialProcessor::currentFrame() const
{
    return m_currentFrame;
}

uint32_t SerialProcessor::calculateCRC32(const char* data, int length) const
{
    // 简单的CRC32实现(实际应用中应使用更健壮的算法)
    uint32_t crc = 0xFFFFFFFF;
    for(int i = 0; i < length; i++)
    {
        crc ^= static_cast<uint8_t>(data[i]);
        for(int j = 0; j < 8; j++)
        {
            if(crc & 1)
            {
                crc = (crc >> 1) ^ 0xEDB88320;
            }
            else
            {
                crc >>= 1;
            }
        }
    }
    return ~crc;
}

  • mainwindow.ui
    在这里插入图片描述
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐