1 概述
XMODEM协议是一种使用拨号调制解调器的个人计算机通信中广泛使用的异步文件运输协议。这种协议以128字节块的形式传输数据,并且每个块都使用一个校验和过程来进行错误检测。使用循环冗余校验的与XMODEM相应的一种协议称为XMODEM-CRC。还有一种是XMODEM-1K,它以1024字节一块来传输数据。YMODEM也是一种XMODEM的实现。它包括XMODEM-1K的所有特征,另外在一次单一会话期间为发送一组文件,增加了批处理文件传输模式。
本文利用C++实现XYModem-1K协议,并利用Qt串口类QSerialPort实现数据读写。
3 实现
3.5 XYModemRecvFile
该模块实现XYModem协议接收文件。
流程图:
 
3.5.1 XYModemRecvFile定义
class QSerialPort;
class XYModemRecvFile : public QObject, public YModem
{
    Q_OBJECT
public:
    explicit XYModemRecvFile(QSerialPort *serial, QObject *parent = nullptr);
public slots:
    void startYModem(QString const& fileName);
    void startXModem(QString const& fileName);
    void stop();
    void cancel();
signals:
    void gotFileSize(quint64 filesize);
    void progressInfo(quint32 blockNumber, quint64 bytesOfRecv);
    void error(QString const& e);
    void finished();
protected:
    uint32_t write(uint8_t const *data, uint32_t size) override;
    uint32_t read(uint8_t *data, uint32_t size) override;
    uint8_t get_code(bool isWait = true) override;
    uint32_t do_recv(uint8_t code);
private:
    bool singled() { return signal_; }
    void doSignal() { signal_ = true; };
    void doError(QString const& text);
    const char* data() { return reinterpret_cast<const char*>(data_ + ID); }
    uint64_t get_filesize(const char* data, uint32_t size);
private:
     QSerialPort* serial_;
     volatile bool signal_;
     uint8_t data_[DATA_SIZE2];
};
 
公共接口:
- startYModem 开始YModem协议接收文件
 - startXModem 开始XModem协议接收文件
 - stop 停止发送
 - cancel 取消发送
 
信号:
- gotFileSize 文件大小信号
 - progressInfo 传输进度信号
 - error 出错信号
 - finished 传输结束信号
 
重载接口:
- write 向串口写数据
 - read 从串口读取数据
 - get_code 读取操作码
 - do_recv 接收数据并验证
 
3.5.2 XYModemRecvFile实现
3.5.2.1 startYModem
void XYModemRecvFile::startYModem(QString const& fileName)
{
    uint8_t code = wait_start();
    if(code == MAX)
    {
        doError("cannot get start code!");
        return;
    }
    uint32_t size = do_recv(code);
    if(size == 0)
    {
        doError("cannot get start data!");
        return;
    }
    uint64_t fileSize = get_filesize(data(), size);
    QFile file(fileName);
    if(!file.open(QIODevice::WriteOnly))
    {
        doError(QString("%1 cannot be opened!").arg(fileName));
        return;
    }
    emit gotFileSize(fileSize);
    quint32 blockNumber = 0;
    uint64_t bytesOfRecv = 0;
    while(!singled())
    {
        code = get_code();
        if(code == EOT)
        {
            tx_code(ACK);
            break;
        }
        size = do_recv(code);
        if(size == 0)
            tx_code(NAK);
        else
        {
            if(file.write(data(), size) != size)
            {
                doError("write file error!");
                return;
            }
            blockNumber++;
            bytesOfRecv += size;
            tx_code(ACK);
            emit progressInfo(blockNumber, bytesOfRecv);
        }
    }
    emit finished();
    serial_->moveToThread(QApplication::instance()->thread());
}
 
函数流程:
- 发送C码并等待操作码
 - 如果是无效操作码,发送错误信号并返回
 - 接收第一数据包
 - 如果收到数据大小为0,发送错误信号并返回
 - 从第一个数据包中获取文件大小
 - 打开文件,如果打开文件失败发送错误信号并返回
 - 发送文件大小信号gotFileSize
 - 操作码是EOT发送ACK,结束文件传送并返回
 - 读取数据
 - 如果没读到数据则发送NAK码要求发送发重传
 - 保存数据,如果保存失败发送错误信号并返回
 - 发送ACK码,发送进度信号
 - 重复上述5步,直到文件发送完毕或停止
 - 发送传输结束信号finished
 
3.5.2.2 startXModem
void XYModemRecvFile::startXModem(QString const& fileName)
{
    Q_UNUSED(fileName)
    uint8_t code = wait_start();
    if(code == MAX)
    {
        doError("cannot get start code!");
        return;
    }
    QFile file(fileName);
    if(!file.open(QIODevice::WriteOnly))
    {
        doError(QString("%1 cannot be opened!").arg(fileName));
        return;
    }
    quint32 blockNumber = 0;
    uint64_t bytesOfRecv = 0;
    do
    {
        uint32_t size = do_recv(code);
        if(size == 0)
            tx_code(NAK);
        else
        {
            if(file.write(data(), size) != size)
            {
                doError("write file error!");
                return;
            }
            tx_code(ACK);
            blockNumber++;
            bytesOfRecv += size;
            emit progressInfo(blockNumber, bytesOfRecv);
        }
        code = get_code();
        if(code == EOT)
        {
            tx_code(ACK);
            break;
        }
    }while(!singled());
    emit finished();
    serial_->moveToThread(QApplication::instance()->thread());
}
 
函数流程:
- 发送C码并等待操作码
 - 如果是无效操作码,发送错误信号并返回
 - 打开文件,如果打开文件失败发送错误信号并返回
 - 读取数据
 - 如果没读到数据则发送NAK码要求发送发重传
 - 保存数据,如果保存失败发送错误信号并返回
 - 发送ACK码,发送进度信号
 - 获取操作码
 - 操作码是EOT发送ACK,结束文件传送并返回
 - 重复上述6步,直到文件发送完毕或停止
 - 发送传输结束信号finished
 
3.5.2.3 do_recv
uint32_t XYModemRecvFile::do_recv(uint8_t code)
{
    uint32_t size = 0;
    if(code == SOH)
        size = read(data_, DATA_SIZE1);
    else if(code == STX)
        size = read(data_, DATA_SIZE2);
    if(size < ID + CRC16)
        return 0;
    uint16_t crc = (data_[size - 2] << 8) | data_[size - 1];
    if(crc16(data_ + ID, size - ID - CRC16) == crc)
        return size - ID + CRC16;
    return 0;
}
 
函数流程:
- 根据code是SOH还是STX读取不同数据包
 - 如果读到数据太小返回0
 - 对收到数据做CRC校准与数据包末尾CRC比较,如果相同返回读取数据字节数
 - 失败返回0
 
3.5.2.4 write/read
uint32_t XYModemSendFile::write(uint8_t const *data, uint32_t size)
{
    return serial_->write((const char *)data, size);
}
uint32_t XYModemSendFile::read(uint8_t *data, uint32_t size)
{
    return serial_->read((char *)data, size);
}
 
函数说明:
- 直接调用串口读写接口实现读写接口。
 
3.4.2.5 get_code
uint8_t XYModemSendFile::get_code(bool isWait)
{
    while(!singled())
    {
        if(serial_->waitForReadyRead(10))
        {
            uint8_t data[1] = { 0 };
            read(data, sizeof(data));
            return data[0];
        }
        if(!isWait)
            break;
    }
    return MAX;
}
 
函数流程:
- 等待串口输入
 - 如果有输入则读取一字节做为code码并返回.
 - 如果一直没有输入直到调用stop后返回无效码MAX.
 
3.4.2.6 doError
void XYModemSendFile::doError(QString const& text)
{
    emit error(text);
    serial_->moveToThread(QApplication::instance()->thread());
    emit finished();
}
 
函数流程:
- 发送错误信号
 - 发送结束信号
 



















