QDataStream 解析网络,或串口传来的数据

推荐通用模式(支持复杂类型)

1. 定义结构体 + 序列化操作符
struct ArrayData {
    quint32 timestamp;
    QString sensorName;  // 包含非POD类型
    qreal values[3];

    friend QDataStream &operator<<(QDataStream &out, const ArrayData &d) {
        out << d.timestamp 
            << d.sensorName
            << d.values[0] << d.values[1] << d.values[2];
        return out;
    }

    friend QDataStream &operator>>(QDataStream &in, ArrayData &d) {
        in >> d.timestamp 
           >> d.sensorName
           >> d.values[0] >> d.values[1] >> d.values[2];
        return in;
    }
};
2. 安全解析流程
QDataStream ds(data);
ds.setByteOrder(QDataStream::LittleEndian);

// 使用事务机制处理不完整数据
ds.startTransaction();

ArrayData dat_snd;
ds >> dat_snd;

if (!ds.commitTransaction()) {
    qDebug() << "等待更多数据...";
    return;
}

// 检查数据有效性
if (dat_snd.timestamp == 0) {
    qWarning() << "无效时间戳";
    return;
}

关键注意事项

  1. 类型安全

    • 发送端与接收端必须使用完全相同的类型(如 qint32 vs quint32 会导致错误)
    • 避免使用 float/double 等平台相关类型,优先使用 qreal
  2. 协议设计

    // 推荐数据包格式
    struct PacketHeader {
        quint32 magic = 0xA0B0C0D0;  // 魔数校验
        quint32 dataSize;            // 数据部分大小
        quint16 version = 1;         // 协议版本
    };
    
  3. 错误处理

    switch (ds.status()) {
        case QDataStream::ReadPastEnd:
            qCritical() << "数据不足";
            break;
        case QDataStream::ReadCorruptData:
            qCritical() << "数据损坏";
            break;
        // ... 其他状态处理
    }
    
  4. 性能优化

    // 预分配缓冲区
    m_buffer.reserve(1024); 
    
    // 批量处理数据
    QVector<ArrayData> batchData;
    while (!ds.atEnd()) {
        ArrayData data;
        ds >> data;
        batchData.append(data);
    }
    

完整网络处理示例

class DataReceiver : public QObject {
    Q_OBJECT
public:
    explicit DataReceiver(QTcpSocket *socket) : m_socket(socket) {
        connect(socket, &QTcpSocket::readyRead, this, &DataReceiver::readData);
    }

private:
    QTcpSocket *m_socket;
    QByteArray m_buffer;
    quint32 m_expectedSize = 0;

    void readData() {
        m_buffer.append(m_socket->readAll());
        
        QDataStream ds(&m_buffer, QIODevice::ReadOnly);
        ds.setByteOrder(QDataStream::LittleEndian);

        while (true) {
            if (m_expectedSize == 0) {
                if (m_buffer.size() < sizeof(PacketHeader)) return;
                PacketHeader header;
                ds >> header;
                
                if (header.magic != 0xA0B0C0D0) {
                    qCritical() << "协议头校验失败";
                    m_socket->disconnectFromHost();
                    return;
                }
                m_expectedSize = header.dataSize;
            }

            if (m_buffer.size() < m_expectedSize) return;

            QByteArray packetData = m_buffer.left(m_expectedSize);
            m_buffer.remove(0, m_expectedSize);
            m_expectedSize = 0;

            processPacket(packetData);
        }
    }

    void processPacket(const QByteArray &data) {
        QDataStream ds(data);
        ds.setByteOrder(QDataStream::LittleEndian);

        ArrayData dat_snd;
        ds >> dat_snd;

        if (ds.status() == QDataStream::Ok) {
            emit newDataReceived(dat_snd);
        } else {
            qWarning() << "无效数据包";
        }
    }

signals:
    void newDataReceived(const ArrayData &data);
};

建议通过以下方式确保可靠性:

  1. 添加 CRC 校验字段
  2. 使用协议版本控制
  3. 实现超时重传机制
  4. 记录数据流日志用于调试
#include "widget.h" #include "ui_widget.h" #include <QFileDialog> #include <QDateTime> #include <QTextStream> #include <QSerialPortInfo> #include <QMessageBox> #include <QTimer> #include <QSerialPort> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) , sendIndex(0) , totalBytes(0) , currentState(STATE_IDLE) { ui->setupUi(this); QStringList serialNamePort; serialport = new QSerialPort(this); timerSerial = new QTimer(this); ackTimer = new QTimer(this); // 连接信号和槽 connect(serialport, SIGNAL(readyRead()), this, SLOT(serialport_readyread())); connect(timerSerial, SIGNAL(timeout()), this, SLOT(TimerUpdate())); connect(ackTimer, &QTimer::timeout, this, &Widget::handleAckTimeout); // 搜索一个文件夹中的串口号 foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { serialNamePort << info.portName(); } ui->comboBox->addItems(serialNamePort); } Widget::~Widget() { delete ui; } void Widget::on_openseriButton_clicked() { QSerialPort::BaudRate baudrate = QSerialPort::Baud115200; // 设置波特率115200 QSerialPort::DataBits databits = QSerialPort::Data8; // 数据位8 QSerialPort::StopBits stopbits = QSerialPort::OneStop; // 停止位1 QSerialPort::Parity checkbits = QSerialPort::NoParity; // 无校验 serialport->setPortName(ui->comboBox->currentText()); // 设置串口号 serialport->setBaudRate(baudrate); // 设置波特率 serialport->setDataBits(databits); // 设置数据位 serialport->setStopBits(stopbits); // 设置停止位 serialport->setParity(checkbits); // 设置校验位 if (serialport->open(QIODevice::ReadWrite)) { // 判断是否打开成功 QMessageBox::information(this, "提示", "打开成功"); } else { QMessageBox::critical(this, "提示", "打开失败"); } } void Widget::on_refuseButton_clicked() { QStringList serialNamePort; ui->comboBox->clear(); foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { serialNamePort << info.portName(); } ui->comboBox->addItems(serialNamePort); } void Widget::on_closeButton_2_clicked() { sendIndex=0; timerSerial->stop(); serialport->close(); } void Widget::on_pushButton_clicked() { QFileDialog* f = new QFileDialog(this); f->setWindowTitle("选择数据文件*.bin"); f->setNameFilter("*.bin"); f->setViewMode(QFileDialog::Detail); ui->lineEdit->clear(); ui->textEdit->clear(); QString filePath; if (f->exec() == QDialog::Accepted) filePath = f->selectedFiles()[0]; ui->lineEdit->setText(filePath); ui->textEdit->append("文件路径:" + filePath); // 文件信息 QFileInfo info(filePath); ui->textEdit->append(QString("文件大小:%1 byte").arg(info.size())); ui->textEdit->append(QString("文件名称:%1").arg(info.fileName())); ui->textEdit->append(QString("创建时间:%1").arg(info.created().toString("yyyy-MM-dd hh:mm:ss"))); ui->textEdit->append(QString("修改时间:%1").arg(info.lastModified().toString("yyyy-MM-dd hh:mm:ss"))); // 文件内容 QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { ui->textEdit->append("文件打开失败"); return; } QByteArray fileContent = file.readAll(); file.close(); delete f; // 以十六进制格式显示文件内容 // QString hexContent = fileContent.toHex(' '); // ui->textEdit->append("文件内容(十六进制格式):"); // ui->textEdit->append(hexContent); // 准备发送的数据 dataToSend = fileContent; sendIndex = 0; totalBytes = dataToSend.size(); } const quint16 HEAD_CODE = 0xA55A; const quint8 TYPE = 0xE1; const quint8 CMD = 0x12; // FLASH_WRITE_BUFF_SEG const int PAYLOAD = 128; // 每帧真正写 Flash 的字节数 // void Widget::on_pushButton_2_clicked() // { // // 判断文件是否已选择 // if (totalBytes == 0) { // ui->textEdit->append("未选择文件文件为空"); // return; // } // // 判断串口是否已打开 // if (!serialport->isOpen()) { // ui->textEdit->append("串口未打开"); // return; // } // timerSerial->start(100); // 每秒发送一次 // } void Widget::TimerUpdate() { if (sendIndex >= totalBytes) { ui->textEdit->append("文件发送完成"); timerSerial->stop(); return; } /*------------------------------------------------- * 1. 计算当前块号(从 0 开始) *------------------------------------------------*/ quint16 block = sendIndex / PAYLOAD; // 第几块 /*------------------------------------------------- * 2. 取本帧要发的 128 字节(最后一帧可能不足 128) *------------------------------------------------*/ int bytesToSend = qMin(PAYLOAD, totalBytes - sendIndex); QByteArray userData = dataToSend.mid(sendIndex, bytesToSend); /*------------------------------------------------- * 3. 构造完整帧 *------------------------------------------------*/ QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); ds << HEAD_CODE; // 2 字节 ds << quint8(5 + userData.size()); // Length = 固定5 + 数据长度 + 校验和 ds << TYPE; // 1 字节 ds << CMD; // 1 字节 ds << block; // 2 字节(Data0+Data1) ds.writeRawData(userData.constData(), userData.size()); // 128 更少 /*------------------------------------------------- * 4. 计算校验和 * SUM = TYPE + CMD + Data0 + Data1 + ... + DataN *------------------------------------------------*/ quint8 sum = TYPE + CMD; sum += (block & 0xFF); // Data0 sum += ((block >> 8) & 0xFF); // Data1 for (int i = 0; i < userData.size(); ++i) sum += static_cast<quint8>(userData.at(i)); ds << sum; // 1 字节校验和 /*------------------------------------------------- * 5. 真正写到串口 *------------------------------------------------*/ qint64 bytesWritten = serialport->write(frame); if (bytesWritten == -1) { ui->textEdit->append("发送失败: " + serialport->errorString()); timerSerial->stop(); return; } /*------------------------------------------------- * 6. 更新进度 *------------------------------------------------*/ sendIndex += bytesToSend; // 注意:是用户数据长度,不是帧长度 ui->textEdit->append(QString("已发送 %1 / %2 字节") .arg(sendIndex).arg(totalBytes)); double percentage = (sendIndex * 100.0 / totalBytes); ui->progressBar->setValue(static_cast<int>(percentage)); ui->progressBar->setFormat(QString::number(percentage, 'f', 1) + "%"); /*------------------------------------------------- * 7. 可选:等待真正发完(非阻塞) *------------------------------------------------*/ serialport->flush(); // 如之前讨论,可保留改为 waitForBytesWritten } // 点击发送按钮 - 启动握手 void Widget::on_pushButton_2_clicked() { if (currentState != STATE_IDLE) { ui->textEdit->append("传输已在进行中"); return; } if (totalBytes == 0) { ui->textEdit->append("未选择文件文件为空"); return; } if (!serialport->isOpen()) { ui->textEdit->append("串口未打开"); return; } // 计算总块数 totalBlocks = (totalBytes + PAYLOAD - 1) / PAYLOAD; ui->textEdit->append(QString("文件总块数: %1").arg(totalBlocks)); // 发送握手请求 sendHandshakeRequest(); // 进入等待握手响应状态 currentState = STATE_WAIT_HANDSHAKE; retryCount = 0; ackTimer->start(1000); // 1秒超时 } // 发送握手请求帧 void Widget::sendHandshakeRequest() { QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); ds << HEAD_CODE; // 2字节帧头 ds << quint8(0x05); // 长度字段 (固定5字节) ds << quint8(TYPE); // 1字节类型 ds << quint8(0x11); // 1字节命令 ds << quint8(0x88); // 长度字段 (固定5字节) ds << quint8(0x88); // 长度字段 (固定5字节) ds << calculateChecksum(frame); // 1字节校验和 serialport->write(frame); ui->textEdit->append("发送握手请求..."); } // 处理单片机响应 void Widget::serialport_readyread() { QByteArray data = serialport->readAll(); processReceivedData(data); } // 处理接收到的数据 void Widget::processReceivedData(const QByteArray& data) { static QByteArray buffer; buffer.append(data); ui->textEdit->append("收到数据"); // 检查是否收到完整帧 (最小帧长=6字节) while (buffer.size() >= 4) { // 检查帧头 if (static_cast<quint8>(buffer[0]) != (0x5A) || static_cast<quint8>(buffer[1]) != (0xA5)) { buffer.remove(0, 1); // 移除非帧头字节 continue; } // 获取长度字段 quint8 length = static_cast<quint8>(buffer[2]); // 检查是否收到完整帧 if (buffer.size() < static_cast<int>(length + 3)) { break; // 等待更多数据 } // 提取完整帧 QByteArray frame = buffer.left(length + 3); buffer.remove(0, length + 3); // 验证校验和 if (!validateChecksum(frame)) { ui->textEdit->append("校验和错误,丢弃帧"); continue; } // 解析帧类型 quint8 frameType = static_cast<quint8>(frame[4]); ui->textEdit->append("收到数据"); // 根据当前状态处理帧 switch (currentState) { case STATE_WAIT_HANDSHAKE: if (frameType == HANDSHAKE_ACK) { handleHandshakeAck(); } break; case STATE_WAIT_START_ACK: if (frameType == HANDSHAKE_ACK) { // 确认开始传输 startTransfer(); } break; case STATE_TRANSFERRING: if (frameType == DATA_REQUEST) { handleDataRequest(frame); } break; default: break; } } } // 处理握手确认 void Widget::handleHandshakeAck() { ackTimer->stop(); ui->textEdit->append("收到握手确认"); // 发送开始传输帧 sendStartFrame(); currentState = STATE_TRANSFERRING; ackTimer->start(1000); // 1秒超时 } // 发送开始传输帧 void Widget::sendStartFrame() { QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); ds << HEAD_CODE; // 2字节帧头 ds << quint8(0x05); // 长度字段 (固定7字节) ds << quint8(TYPE); // 1字节类型 ds << quint8(0x11); // 1字节命令 ds << quint8(0x99); // 2字节总块数 ds << quint8(0x99); // 2字节总块数 ds << calculateChecksum(frame); // 1字节校验和 serialport->write(frame); ui->textEdit->append("发送开始传输帧..."); } // 处理数据请求 void Widget::handleDataRequest(const QByteArray& frame) { // 解析请求的块号 quint16 blockNumber; QDataStream ds(frame.mid(5, 2)); // 跳过帧头(2)+长度(1)+类型(1)+命令(1) ds.setByteOrder(QDataStream::BigEndian); ds >> blockNumber; ui->textEdit->append(QString("收到数据请求: 块号 %1").arg(blockNumber)); // 检查块号是否有效 if (blockNumber >= totalBlocks) { sendErrorFrame("无效块号"); return; } // 发送请求的数据块 sendDataFrame(blockNumber); // 如果是最后一块,准备结束传输 if (blockNumber == totalBlocks - 1) { currentState = STATE_COMPLETED; sendEndFrame(); } } // 发送数据帧 void Widget::sendDataFrame(quint16 blockNumber) { // 计算数据偏移 int offset = blockNumber * PAYLOAD; int bytesToSend = qMin(PAYLOAD, totalBytes - offset); QByteArray payload = dataToSend.mid(offset, bytesToSend); // 构造帧 QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); // 计算长度 (TYPE+CMD+块号+数据+校验和) quint8 length = 1 + 1 + 2 + payload.size() + 1; ds << HEAD_CODE; // 2字节帧头 ds << length; // 1字节长度 ui->textEdit->append(QString("发送长度 %1").arg(length)); ds << quint8(TYPE); // 1字节类型 ds << quint8(DATA_FRAME); // 1字节命令 ds << blockNumber; // 2字节块号 ds.writeRawData(payload.constData(), payload.size()); // 数据 // 计算校验和 quint8 sum = 0; const char* dataStart = frame.constData() + 3; // 从长度字段后开始 for (int i = 0; i < length; i++) { sum += static_cast<quint8>(dataStart[i]); } ds << sum; // 发送帧 serialport->write(frame); // 更新进度 double percentage = ((blockNumber + 1) * 100.0) / totalBlocks; ui->progressBar->setValue(static_cast<int>(percentage)); ui->progressBar->setFormat(QString::number(percentage, 'f', 1) + "%"); } // 发送结束帧 void Widget::sendEndFrame() { QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); ds << HEAD_CODE; // 2字节帧头 ds << quint8(0x05); // 长度字段 (固定5字节) ds << quint8(TYPE); // 1字节类型 ds << quint8(END_FRAME); // 1字节命令 ds << quint8(0x01); // 1字节命令 ds << quint8(0x01); // 1字节命令 ds << calculateChecksum(frame); // 1字节校验和 serialport->write(frame); ui->textEdit->append("发送结束帧..."); ui->textEdit->append("文件传输完成"); // 重置状态 currentState = STATE_IDLE; } // 超时处理 void Widget::handleAckTimeout() { switch (currentState) { case STATE_WAIT_HANDSHAKE: if (retryCount < 3) { ui->textEdit->append("握手响应超时,重试..."); sendHandshakeRequest(); retryCount++; } else { ui->textEdit->append("握手失败,超过重试次数"); currentState = STATE_IDLE; } break; case STATE_WAIT_START_ACK: if (retryCount < 3) { ui->textEdit->append("开始确认超时,重试..."); sendStartFrame(); retryCount++; } else { ui->textEdit->append("开始传输失败,超过重试次数"); currentState = STATE_IDLE; } break; default: ackTimer->stop(); break; } } // 发送错误帧 void Widget::sendErrorFrame(const QString& message) { QByteArray frame; QDataStream ds(&frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::BigEndian); QByteArray msgData = message.toUtf8(); quint8 length = 1 + 1 + msgData.size() + 1; ds << HEAD_CODE; // 2字节帧头 ds << length; // 长度字段 ds << TYPE; // 1字节类型 ds << ERROR_FRAME; // 1字节命令 ds.writeRawData(msgData.constData(), msgData.size()); // 错误信息 // 计算校验和 quint8 sum = 0; const char* dataStart = frame.constData() + 3; // 从长度字段后开始 for (int i = 0; i < length; i++) { sum += static_cast<quint8>(dataStart[i]); } ds << sum; serialport->write(frame); ui->textEdit->append("发送错误帧: " + message); // 重置状态 currentState = STATE_IDLE; ackTimer->stop(); } // 计算校验和 quint8 Widget::calculateChecksum(const QByteArray& frame) { if (frame.size() < 4) return 0; quint8 sum = 0; // 从长度字段开始计算 (偏移2字节) for (int i = 3; i < frame.size(); i++) { sum += static_cast<quint8>(frame[i]); } return sum; } // 验证校验和 bool Widget::validateChecksum(const QByteArray& frame) { if (frame.size() < 5) return false; quint8 calculatedSum = 0; // 从长度字段开始计算 (偏移2字节),跳过最后的校验和字节 for (int i = 3; i < frame.size()-1; i++) { calculatedSum += static_cast<quint8>(frame[i]); } quint8 receivedSum = static_cast<quint8>(frame[frame.size() - 1]); return calculatedSum == receivedSum; } void Widget::startTransfer() { // 实现代码 // 例如:发送握手请求 // sendHandshakeRequest(); } 这段代码校验是正确的,协议也是正确的,最后的结束帧不是立即发出的,而是,mcu要过最后一帧数据后,又要数据,再发结束帧,比如有0-128包数据(从零开始,128是最后一包),128包已经发完,单片机又要129包,再发结束帧
07-25
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

enyp80

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值