背景
项目需要用QT实现一个YMODEM文件传输的功能,目标下位机是MCU嵌入式设备,且下位机程序已经经过xshell传输文件的验证。
YMODEM 简介
YMODEM协议是一个文件传输协议,常用于嵌入式设备。本文不对YMODEM做过多的阐述,阅读需建立在你已经对YMODEM有一定了解的基础上。如果要了解YMODEM协议,推荐几个地址:
维基百科 YMODEM
Ymodem 协议详解
YMODEM协议简介
YMODEM协议中文翻译
但要注意的是这些文章都有一些小的细节性的错误,文章的评论区有人指出了,需要注意甄别。
YMODEM通信协议:
发送端----------------------------------------------------------------接收端
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C
SOH 00 FF “foo.c” "1064’’ NUL[118] CRC CRC >>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C
STX 01 FE data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
STX 02 FD data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
STX 03 FC data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
STX 04 FB data[1024] CRC CRC>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
SOH 05 FA data[100] 1A[28] CRC CRC>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
EOT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< NAK
EOT>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< C
SOH 00 FF NUL[128] CRC CRC >>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ACK
核心代码
文件发送的核心代码,主要步骤是:
- 启动YMODEM传输,并建立通道。
- 发送首帧数据,即文件名和文件大小信息。
- 按照1024或者128字节发送文件数据。
- 结束传输。
Ymodem::Code YmodemFileTransmit::callback(Status status, uint8_t* buff, uint32_t* len)
{
switch (status) {
case StatusEstablish: {
if (file->open(QFile::ReadOnly) == true) {
QFileInfo fileInfo(*file);
fileSize = fileInfo.size();
fileCount = 0;
strcpy((char*)buff, fileInfo.fileName().toLocal8Bit().data());
strcpy((char*)buff + fileInfo.fileName().toLocal8Bit().length() + 1,
QByteArray::number(fileInfo.size()).data());
// char source = 0x20;
// strcpy((char*)buff + fileInfo.fileName().toLocal8Bit().length() + 1 +
// QByteArray::number(fileInfo.size()).size(), &source);
*len = YMODEM_PACKET_SIZE;
YmodemFileTransmit::status = StatusEstablish;
transmitStatus(StatusEstablish);
return CodeAck;
} else {
YmodemFileTransmit::status = StatusError;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
}
case StatusTransmit: {
if (fileSize != fileCount) {
if ((fileSize - fileCount) > YMODEM_PACKET_1K_SIZE) {
qint64 r = file->read((char*)buff, YMODEM_PACKET_1K_SIZE);
fileCount += r;
*len = YMODEM_PACKET_1K_SIZE;
} else {
qint64 r = file->read((char*)buff, YMODEM_PACKET_SIZE);
fileCount += r;
*len = YMODEM_PACKET_SIZE;
}
progress = (int)(fileCount * 100 / fileSize);
YmodemFileTransmit::status = StatusTransmit;
transmitProgress(progress);
transmitStatus(StatusTransmit);
return CodeAck;
} else {
YmodemFileTransmit::status = StatusTransmit;
transmitStatus(StatusTransmit);
return CodeEot;
}
}
case StatusFinish: {
file->close();
YmodemFileTransmit::status = StatusFinish;
writeTimer->start(WRITE_TIME_OUT);
return CodeAck;
}
case StatusAbort: {
file->close();
YmodemFileTransmit::status = StatusAbort;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
case StatusTimeout: {
YmodemFileTransmit::status = StatusTimeout;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
default: {
file->close();
YmodemFileTransmit::status = StatusError;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
}
}
文件接收的核心代码,主要步骤是:
- 开启YMODEM文件接收,建立通道。
- 解析首帧数据,即解析文件名和文件大小信息。
- 按照发送者的发送大小(1024或者128字节)解析文件数据。
- 接收完成,结束传输。
Ymodem::Code YmodemFileReceive::callback(Status status, uint8_t* buff, uint32_t* len)
{
switch (status) {
case StatusEstablish: {
QByteArray b = QByteArray((char*)buff, 133).toHex();
qDebug() << "recvbuff:" << b;
if (buff[0] != 0) {
int i = 0;
char name[128] = {0};
char size[128] = {0};
for (int j = 0; buff[i] != 0 && buff[i] != 0x20; i++, j++) {
qDebug() << buff[i];
name[j] = buff[i];
}
i++;
//SOH 00 FF foo.c 3232 NUL[118] CRCH CRCL
//0或者空格,xshell 在文件名称和文件大小中间发送的是空格,即0x20,标准YMODEM协议要求文件名称以'\0'(也就是0)结尾,所以如果要实现和xshell互通,此处要兼容
for (int j = 0; buff[i] != 0 && buff[i] != 0x20; i++, j++) {
qDebug() << buff[i];
size[j] = buff[i];
}
fileName = QString::fromLocal8Bit(name);
fileSize = QString(size).toULongLong();
fileCount = 0;
qDebug() << "StatusEstablish::fileName:" << fileName ;
qDebug() << "StatusEstablish::fileSize:" << fileSize ;
file->setFileName(filePath + fileName);
if (file->open(QFile::WriteOnly) == true) {
YmodemFileReceive::status = StatusEstablish;
receiveStatus(StatusEstablish);
return CodeAck;
} else {
YmodemFileReceive::status = StatusError;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
} else {
YmodemFileReceive::status = StatusError;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
}
case StatusTransmit: {
if ((fileSize - fileCount) > *len) {
qint64 w = file->write((char*)buff, *len);
fileCount += *len;
} else {
qint64 w = file->write((char*)buff, fileSize - fileCount);
fileCount += fileSize - fileCount;
}
progress = fileSize == 0 ? 0 : (int)(fileCount * 100 / fileSize);
YmodemFileReceive::status = StatusTransmit;
receiveProgress(progress);
receiveStatus(StatusTransmit);
return CodeAck;
}
case StatusFinish: {
file->close();
YmodemFileReceive::status = StatusFinish;
writeTimer->start(WRITE_TIME_OUT);
return CodeAck;
}
case StatusAbort: {
file->close();
YmodemFileReceive::status = StatusAbort;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
case StatusTimeout: {
YmodemFileReceive::status = StatusTimeout;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
default: {
file->close();
YmodemFileReceive::status = StatusError;
writeTimer->start(WRITE_TIME_OUT);
return CodeCan;
}
}
}
在进行YMODEM 上位机开发时,有两个坑需要注意,否则大概率掉坑里。
1号坑:
下位机那边反馈,使用xshell给MCU发送文件正常,使用我们的软件一开始就卡住。
经过对xshell抓包分析,发现xshell默认配置会在发送YMODEM前先发送"rb -E"指令,我们的下位机收到无法解析的指令后,会默认回复’C’,所以xshell收到’C’后开始了正常的文件发送。
但是根据YMODEM协议标准(可以看上面的通信协议),在启动文件传输后,发送端是静止的,只需要等待接收端发送第一个’C’, 所以我们的上位机并没有做,导致启动发送就卡住了。修改代码,在开始发送前,先发送任意字符,触发接收端的’C’回复,修改后测试正常了。
所以如果打开也涉及到YMODEM开发,需要注意和你们的下位机同事沟通清楚’C’字符在什么时候回复,有没有按照标准来。
2号坑:
在YMODEM传输文件名和文件大小时,YMODEM协议中明确要求了文件名必须以null(即’\0’)作为结束符,求证:
- https://en.wikipedia.org/wiki/YMODEM
- https://pauillac.inria.fr/~doligez/zmodem/ymodem.txt
见 wikipedia 的 References 文章中的 Chapter 1 ,
The pathname shall be a null terminated ASCII string as described below.
但抓包发现xshell在文件名和文件大小中间填充的是空格(即0x20),如果这个时候接收端说xshell和我是互通的来证明自己没问题,就欲哭无泪了。因为他很有可能解析文件名和文件大小时判断的是0x20,而不是’\0’。所以,此处也要跟下位机沟通清楚,他们是如何解析文件名和文件大小的,如果没有判断’\0’,文件名和文件大小肯定就不对了。
结束补充
Demo截图:
参考:
SerialPortYmodem
对其在xshell兼容方面作了改动和优化。
Demo及源代码下载地址:
https://download.csdn.net/download/u012534831/88625925
其他代码我打包上传到csdn资源中,关注公号后在后台留言需要下载的资源,我看到后免费发给你,并可以得到我的免费解答。 原创不易,谢谢支持。
关注公众号 QTShared,带你探索更多QT相关知识。