(一)参考文献
【安卓相关】蓝牙基于Ymodem协议发送bin文件,对硬件设备进行升级。 - 简书
当Android BLE遇上YModem - 简书
(二)收发机制
基于我们具体的需求,在原有的基础上加了一下前后的处理。
* MY YMODEM IMPLEMTATION
* *SENDER: ANDROID APP *------------------------------------------* RECEIVER: BLE DEVICE*
* HELLO BOOTLOADER ---------------------------------------------->*
* <---------------------------------------------------------------* C
* SOH 00 FF filename0fileSizeInByte0MD5[90] ZERO[38] CRC CRC----->*
* <---------------------------------------------------------------* ACK C
* STX 01 FE data[1024] CRC CRC ---------------------------------->*
* <---------------------------------------------------------------* ACK
* STX 02 FF data[1024] CRC CRC ---------------------------------->*
* <---------------------------------------------------------------* ACK
* ...
* ...
* <p>
* STX 08 F7 data[1000] CPMEOF[24] CRC CRC ----------------------->*
* <---------------------------------------------------------------* ACK
* EOT ----------------------------------------------------------->*
* <---------------------------------------------------------------* ACK
* SOH 00 FF ZERO[128] ------------------------------------------->*
* <---------------------------------------------------------------* ACK
* <---------------------------------------------------------------* MD5_OK
(三)核心代码模块
首先梳理一下它应该具有哪些模块:
- 协议的核心实现
主要是负责数据传输过程中有关协议的部分,如在数据包上加入头,CRC,验证返回的正确性以及超时重发等。- 一个协议工具类,封装包数据的提供
- 一个文件数据的读取模块:它是耗时任务,应该在子线程进行。
- 各种执行状态的监听
3.1协议的核心实现
/**
* Created by leonxtp on 2017/9/16.
* Modified by leonxtp on 2017/9/16
*/
public class Ymodem implements FileStreamThread.DataRaderListener {
private static final int STEP_HELLO = 0x00;
private static final int STEP_FILE_NAME = 0x01;
private static final int STEP_FILE_BODY = 0x02;
private static final int STEP_EOT = 0x03;
private static final int STEP_END = 0x04;
private static int CURR_STEP = STEP_HELLO;
private static final byte ACK = 0x06; /* ACKnowlege */
private static final byte NAK = 0x15; /* Negative AcKnowlege */
private static final byte CAN = 0x18; /* CANcel character */
private static final byte ST_C = 'C';
private static final String MD5_OK = "MD5_OK";
private static final String MD5_ERR = "MD5_ERR";
private Context mContext;
private String filePath;
private String fileNameString = "LPK001_Android";
private String fileMd5String = "63e7bb6eed1de3cece411a7e3e8e763b";
private YModemListener listener;
private TimeOutHelper timerHelper = new TimeOutHelper();
private FileStreamThread streamThread;
//bytes has been sent of this transmission
private int bytesSent = 0;
//package data of current sending, used for int case of fail
private byte[] currSending = null;
private int packageErrorTimes = 0;
private static final int MAX_PACKAGE_SEND_ERROR_TIMES = 5;
//the timeout interval for a single package
private static final int PACKAGE_TIME_OUT = 6000;
/**
* Construct of the YModemBLE,you may don't need the fileMD5 checking,remove it
*
* @param filePath absolute path of the file
* @param fileNameString file name for sending to the terminal
* @param fileMd5String md5 for terminal checking after transmission finished
* @param listener
*/
public Ymodem(Context context, String filePath,
String fileNameString, String fileMd5String,
YModemListener listener) {
this.filePath = filePath;
this.fileNameString = fileNameString;
this.fileMd5String = fileMd5String;
this.mContext = context;
this.listener = listener;
}
/**
* Start the transmission
*/
public void start() {
sayHello();
}
/**
* Stop the transmission when you don't need it or shut it down in accident
*/
public void stop() {
bytesSent = 0;
currSending = null;
packageErrorTimes = 0;
if (streamThread != null) {
streamThread.release();
}
timerHelper.stopTimer();
}
/**
* Method for the outer caller when received data from the terminal
*/
public void onReceiveData(byte[] respData) {
//Stop the package timer
timerHelper.stopTimer();
if (respData != null && respData.length > 0) {
switch (CURR_STEP) {
case STEP_HELLO:
handleHello(respData);
break;
case STEP_FILE_NAME:
handleFileName(respData);
break;
case STEP_FILE_BODY:
handleFileBody(respData[0]);
break;
case STEP_EOT:
handleEOT(respData);
break;
case STEP_END:
handleEnd(respData);
break;
default:
break;
}
} else {
L.f("The terminal do responsed something, but received nothing??");
}
}
/**
* ==============================================================================
* Methods for sending data begin
* ==============================================================================
*/
private void sayHello() {
streamThread = new FileStreamThread(mContext, filePath, this);
CURR_STEP = STEP_HELLO;
L.f("sayHello!!!");
byte[] hello = YModemUtil.getYModelHello();
if (listener != null) {
listener.onDataReady(hello);
}
}
private void sendFileName() {
CURR_STEP = STEP_FILE_NAME;
L.f("sendFileName");
try {
int fileByteSize = streamThread.getFileByteSize();
byte[] hello = YModemUtil.getFileNamePackage(fileNameString, fileByteSize
, fileMd5String);
if (listener != null) {
listener.onDataReady(hello);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void startSendFileData() {
CURR_STEP = STEP_FILE_BODY;
L.f("startSendFileData");
streamThread.start();
}
//Callback from the data reading thread when a data package is ready
@Override
public void onDataReady(byte[] data) {
if (listener != null) {
currSending = data;
//Start the timer, it will be cancelled when reponse received,
// or trigger the timeout and resend the current package data
timerHelper.startTimer(timeoutListener, PACKAGE_TIME_OUT);
listener.onDataReady(data);
}
}
private void sendEOT() {
CURR_STEP = STEP_EOT;
L.f("sendEOT");
if (listener != null) {
listener.onDataReady(YModemUtil.getEOT());
}
}
private void sendEND() {
CURR_STEP = STEP_END;
L.f("sendEND");
if (listener != null) {
try {
listener.onDataReady(YModemUtil.getEnd());
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* ==============================================================================
* Method for handling the response of a package
* ==============================================================================
*/
private void handleHello(byte[] value) {
int character = value[0];
if (character == ST_C) {//Receive "C" for "HELLO"
packageErrorTimes = 0;
sendFileName();
} else {
handleOthers(character);
}
}
//The file name package was responsed
private void handleFileName(byte[] value) {
if (value.length == 2 && value[0] == ACK && value[1] == ST_C) {//Receive 'ACK C' for file name
packageErrorTimes = 0;
startSendFileData();
} else if (value[0] == ST_C) {//Receive 'C' for file name, this package should be resent
handlePackageFail();
} else {
handleOthers(value[0]);
}
}
private void handleFileBody(int character) {
if (character == ACK) {//Receive ACK for file data
packageErrorTimes = 0;
bytesSent += currSending.length;
try {
if (listener != null) {
listener.onProgress(bytesSent, streamThread.getFileByteSize());
}
} catch (IOException e) {
e.printStackTrace();
}
streamThread.keepReading();
} else if (character == ST_C) {
//Receive C for file data, the ymodem cannot handle this circumstance, transmission failed...
if (listener != null) {
listener.onFailed();
}
} else {
handleOthers(character);
}
}
private void handleEOT(byte[] value) {
if (value[0] == ACK) {
packageErrorTimes = 0;
sendEND();
} else if (value[0] == ST_C) {//As we haven't received ACK, we should resend EOT
handlePackageFail();
} else {
handleOthers(value[0]);
}
}
private void handleEnd(byte[] character) {
if (character[0] == ACK) {//The last ACK represents that the transmission has been finished, but we should validate the file
packageErrorTimes = 0;
} else if ((new String(character)).equals(MD5_OK)) {//The file data has been checked,Well Done!
stop();
if (listener != null) {
listener.onSuccess();
}
} else if ((new String(character)).equals(MD5_ERR)) {//Oops...Transmission Failed...
stop();
if (listener != null) {
listener.onFailed();
}
} else {
handleOthers(character[0]);
}
}
private void handleOthers(int character) {
if (character == NAK) {//We need to resend this package as the terminal failed when checking the crc
handlePackageFail();
} else if (character == CAN) {//Some big problem occurred, transmission failed...
stop();
}
}
//Handle a failed package data ,resend it up to MAX_PACKAGE_SEND_ERROR_TIMES times.
//If still failed, then the transmission failed.
private void handlePackageFail() {
packageErrorTimes++;
if (packageErrorTimes < MAX_PACKAGE_SEND_ERROR_TIMES) {
if (listener != null) {
listener.onDataReady(currSending);
}
} else {
//Still, we stop the transmission, release the resources
stop();
if (listener != null) {
listener.onFailed();
}
}
}
/* The InputStream data reading thread was done */
@Override
public void onFinish() {
sendEOT();
}
//The timeout listener
private TimeOutHelper.ITimeOut timeoutListener = new TimeOutHelper.ITimeOut() {
@Override
public void onTimeOut() {
if (currSending != null) {
handlePackageFail();
}
}
};
public static class Builder {
private Context context;
private String filePath;
private String fileNameString;
private String fileMd5String;
private YModemListener listener;
public Builder with(Context context) {
this.context = context;
return this;
}
public Builder filePath(String filePath) {
this.filePath = filePath;
return this;
}
public Builder fileName(String fileName) {
this.fileNameString = fileName;
return this;
}
public Builder checkMd5(String fileMd5String) {
this.fileMd5String = fileMd5String;
return this;
}
public Builder callback(YModemListener listener) {
this.listener = listener;
return this;
}
public Ymodem build() {
return new Ymodem(context, filePath, fileNameString, fileMd5String, listener);
}
}
}
该代码实现了一个Ymodem类,用于通过Ymodem协议传输文件。以下是代码的简要总结:
常量定义:定义了传输步骤(HELLO、FILE_NAME、FILE_BODY、EOT、END)和一些控制字符(ACK、NAK、CAN、ST_C)以及MD5校验相关的字符串。
成员变量:包括上下文(Context)、文件路径、文件名、文件MD5值、传输监听器(YModemListener)、计时器助手(TimeOutHelper)、文件流线程(FileStreamThread)、已发送字节数、当前发送的数据包、错误计数等。
构造函数:初始化Ymodem对象,接受文件路径、文件名、文件MD5值和监听器作为参数。
传输控制方法:
start()
: 开始传输,调用sayHello()
方法。stop()
: 停止传输,重置相关变量,释放资源。
接收数据方法:
onReceiveData(byte[] respData)
: 处理从终端接收的数据,根据当前传输步骤调用相应的处理方法。
数据发送方法:
sayHello()
: 发送HELLO包。sendFileName()
: 发送文件名包。startSendFileData()
: 开始发送文件数据包。sendEOT()
: 发送EOT包。sendEND()
: 发送END包。
响应处理方法:
handleHello(byte[] value)
: 处理HELLO包的响应。handleFileName(byte[] value)
: 处理文件名包的响应。handleFileBody(int character)
: 处理文件数据包的响应。handleEOT(byte[] value)
: 处理EOT包的响应。handleEnd(byte[] character)
: 处理END包的响应。handleOthers(int character)
: 处理其他响应(如NAK、CAN)。
失败处理方法:
handlePackageFail()
: 处理数据包发送失败,重试发送,超过最大重试次数则停止传输。
构建器类:
Builder
类用于方便地创建Ymodem对象,支持链式调用设置参数。
整体来说,该代码实现了一个Ymodem文件传输协议的客户端,通过分步骤发送文件数据,并处理接收端的各种响应,确保文件能够可靠地传输和校验。
3.2协议包工具类
/**
* Util for encapsulating data package of ymodem protocol
* <p>
* Created by leonxtp on 2017/9/16.
* Modified by leonxtp on 2017/9/16
*/
public class YModemUtil {
/*This is my concrete ymodem start signal, customise it to your needs*/
private static final String HELLO = "HELLO BOOTLOADER";
private static final byte SOH = 0x01; /* Start Of Header with data size :128*/
private static final byte STX = 0x02; /* Start Of Header with data size : 1024*/
private static final byte EOT = 0x04; /* End Of Transmission */
private static final byte CPMEOF = 0x1A;/* Fill the last package if not long enough */
private static CRC16 crc16 = new CRC16();
/**
* Get the first package data for hello with a terminal
*/
public static byte[] getYModelHello() {
return HELLO.getBytes();
}
/**
* Get the file name package data
*
* @param fileNameString file name in String
* @param fileByteSize file byte size of int value
* @param fileMd5String the md5 of the file in String
*/
public static byte[] getFileNamePackage(String fileNameString,
int fileByteSize,
String fileMd5String) throws IOException {
byte seperator = 0x0;
String fileSize = fileByteSize + "";
byte[] byteFileSize = fileSize.getBytes();
byte[] fileNameBytes1 = concat(fileNameString.getBytes(),
new byte[]{seperator},
byteFileSize);
byte[] fileNameBytes2 = Arrays.copyOf(concat(fileNameBytes1,
new byte[]{seperator},
fileMd5String.getBytes()), 128);
byte seq = 0x00;
return getDataPackage(fileNameBytes2, 128, seq);
}
/**
* Get a encapsulated package data block
*
* @param block byte data array
* @param dataLength the actual content length in the block without 0 filled in it.
* @param sequence the package serial number
* @return a encapsulated package data block
*/
public static byte[] getDataPackage(byte[] block, int dataLength, byte sequence) throws IOException {
byte[] header = getDataHeader(sequence, block.length == 1024 ? STX : SOH);
//The last package, fill CPMEOF if the dataLength is not sufficient
if (dataLength < block.length) {
int startFil = dataLength;
while (startFil < block.length) {
block[startFil] = CPMEOF;
startFil++;
}
}
//We should use short size when writing into the data package as it only needs 2 bytes
short crc = (short) crc16.calcCRC(block);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeShort(crc);
dos.close();
byte[] crcBytes = baos.toByteArray();
return concat(header, block, crcBytes);
}
/**
* Get the EOT package
*/
public static byte[] getEOT() {
return new byte[]{EOT};
}
/**
* Get the Last package
*/
public static byte[] getEnd() throws IOException {
byte seq = 0x00;
return getDataPackage(new byte[128], 128, seq);
}
/**
* Get InputStream from Assets, you can customize it from the other sources
*
* @param fileAbsolutePath absolute path of the file in asstes
*/
public static InputStream getInputStream(Context context, String fileAbsolutePath) throws IOException {
return new InputStreamSource().getStream(context, fileAbsolutePath);
}
private static byte[] getDataHeader(byte sequence, byte start) {
//The serial number of the package increases Cyclically up to 256
byte modSequence = (byte) (sequence % 0x256);
byte complementSeq = (byte) ~modSequence;
return concat(new byte[]{start},
new byte[]{modSequence},
new byte[]{complementSeq});
}
private static byte[] concat(byte[] a, byte[] b, byte[] c) {
int aLen = a.length;
int bLen = b.length;
int cLen = c.length;
byte[] concated = new byte[aLen + bLen + cLen];
System.arraycopy(a, 0, concated, 0, aLen);
System.arraycopy(b, 0, concated, aLen, bLen);
System.arraycopy(c, 0, concated, aLen + bLen, cLen);
return concated;
}
}
这段代码实现了YModem协议的数据打包工具,主要功能包括:
定义常量:
HELLO
: 自定义的启动信号字符串。SOH
: 表示128字节数据包的头部标志。STX
: 表示1024字节数据包的头部标志。EOT
: 传输结束标志。CPMEOF
: 用于填充未满数据包的字节。
计算CRC16校验:
- 使用
CRC16
类来计算数据包的CRC校验值。
- 使用
生成数据包:
getYModelHello()
: 获取启动信号的字节数组。getFileNamePackage()
: 生成包含文件名、文件大小和文件MD5值的数据包。getDataPackage()
: 生成带有头部、数据块和CRC校验的数据包,并填充不足部分。getEOT()
: 获取传输结束数据包。getEnd()
: 获取最后一个填充128字节的数据包。getInputStream()
: 从资源文件中获取输入流(可定制其他来源)。
私有辅助方法:
getDataHeader()
: 生成数据包头部,包括起始字节、序列号及其补码。concat()
: 连接多个字节数组。
该工具类主要用于在YModem协议传输过程中打包和封装数据。
3.3文件数据读取类
/**
* Thread for reading input Stream and encapsulating into a ymodem package
* <p>
* Created by leonxtp on 2017/9/16.
* Modified by leonxtp on 2017/9/16
*/
public class FileStreamThread extends Thread {
private Context mContext;
private InputStream inputStream = null;
private DataRaderListener listener;
private String filePath;
private AtomicBoolean isDataAcknowledged = new AtomicBoolean(false);
private boolean isKeepRunning = false;
private int fileByteSize = 0;
public FileStreamThread(Context mContext, String filePath, DataRaderListener listener) {
this.mContext = mContext;
this.filePath = filePath;
this.listener = listener;
}
public int getFileByteSize() throws IOException {
if (fileByteSize == 0 || inputStream == null) {
initStream();
}
return fileByteSize;
}
@Override
public void run() {
try {
prepareData();
} catch (IOException e) {
e.printStackTrace();
}
}
private void prepareData() throws IOException {
initStream();
byte[] block = new byte[1024];
int dataLength;
byte blockSequence = 1;//The data package of a file is actually started from 1
isDataAcknowledged.set(true);
isKeepRunning = true;
while (isKeepRunning) {
if (!isDataAcknowledged.get()) {
try {
//We need to sleep for a while as the sending 1024 bytes data from ble would take several seconds
//In my circumstances, this can be up to 3 seconds.
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
if ((dataLength = inputStream.read(block)) == -1) {
L.f("The file data has all been read...");
if (listener != null) {
onStop();
listener.onFinish();
}
break;
}
byte[] packige = YModemUtil.getDataPackage(block, dataLength, blockSequence);
if (listener != null) {
listener.onDataReady(packige);
}
blockSequence++;
isDataAcknowledged.set(false);
}
}
/**
* When received response from the terminal ,we should keep the thread keep going
*/
public void keepReading() {
isDataAcknowledged.set(true);
}
public void release() {
onStop();
listener = null;
}
private void onStop() {
isKeepRunning = false;
isDataAcknowledged.set(false);
fileByteSize = 0;
onReadFinished();
}
private void initStream() {
if (inputStream == null) {
try {
inputStream = YModemUtil.getInputStream(mContext, filePath);
fileByteSize = inputStream.available();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void onReadFinished() {
if (inputStream != null) {
try {
inputStream.close();
inputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
public interface DataRaderListener {
void onDataReady(byte[] data);
void onFinish();
}
}
这段代码定义了一个名为`FileStreamThread`的类,该类继承自`Thread`,用于读取输入流并将其封装成 Ymodem 数据包。主要功能如下:
1. **构造函数**:初始化线程,接受`Context`、文件路径和`DataRaderListener`作为参数。
2. **获取文件大小**:通过`getFileByteSize`方法获取文件的字节大小。
3. **线程运行**:重写`run`方法,在`run`方法中调用`prepareData`方法读取文件数据并封装成 Ymodem 数据包。
4. **准备数据**:`prepareData`方法中:
- 初始化输入流。
- 读取文件数据,按块读取,并封装成 Ymodem 数据包。
- 调用监听器`listener`的方法将封装好的数据包发送出去。
5. **继续读取**:当收到终端响应时,调用`keepReading`方法继续读取数据。
6. **释放资源**:`release`方法停止线程,释放资源。
7. **初始化流**:`initStream`方法初始化输入流并获取文件大小。
8. **读取完成**:`onReadFinished`方法关闭输入流并清理资源。
9. **监听器接口**:`DataRaderListener`接口用于处理数据包准备好和读取完成的事件。
总的来说,该类用于读取文件数据并将其按块封装成 Ymodem 数据包,通过监听器接口将数据包传递给外部处理。
3.4各种状态监听接口
/**
* Listener of the transmission process
*/
public interface YModemListener {
/* the data package has been encapsulated */
void onDataReady(byte[] data);
/*just the file data progress*/
void onProgress(int currentSent, int total);
/* the file has been correctly sent to the terminal */
void onSuccess();
/* the task has failed with several remedial measures like retrying some times*/
void onFailed();
}
(四)具体使用步骤:
初始化
ymodem = new Ymodem.Builder()
.with(this)
.filePath("assets://demo.bin")
.fileName("demo.bin")
.checkMd5("lsfjlhoiiw121241l241lgljaf")
.callback(new YModemListener() {
@Override
public void onDataReady(byte[] data) {
//send this data[] to your ble component here...
}
@Override
public void onProgress(int currentSent, int total) {
//the progress of the file data has transmitted
}
@Override
public void onSuccess() {
//we are well done with md5 checked
}
@Override
public void onFailed() {
//the task has failed for several times of trying
}
}).build();
ymodem.start();
开始传输
ymodem.start();
当接收到设备响应
ymodem.onReceiveData(data);
停止
ymodem.stop();