背景
最近公司的硬件設備需要升級程序,里面內(nèi)置的是藍牙4.0模塊進行通信逸月。產(chǎn)品已上市栓撞,而且出于成本考慮,以及升級的方便,不用拆機就可以升級瓤湘,決定繼續(xù)使用BLE來進行設備程序升級瓢颅。
協(xié)議
在單片機上進行文件傳輸常用的協(xié)議主要有以下幾個:
Xmodem
Xmodem is one of the most widely used file transfer protocols. The original Xmodem protocol uses 128-byte packets and a simple "checksum" method of error detection. A later enhancement, Xmodem-CRC, uses a more secure Cyclic Redundancy Check (CRC) method for error detection. Xmodem protocol always attempts to use CRC first. If the sender does not acknowledge the requests for CRC, the receiver shifts to the checksum mode and continues its request for transmission.
簡而言之,xmodem就是一個被廣泛使用的文件傳輸協(xié)議弛说,最初的xmodem協(xié)議使用的是128字節(jié)包大小并使用簡單的檢驗和來驗證數(shù)據(jù)是否出錯挽懦。后來就有了加強版的xmodem-crc,它使用的是crc來校驗數(shù)據(jù)正確性木人。只是它默認的還是原本的檢驗和信柿。
Xmodem-1K
Xmodem 1K is essentially Xmodem CRC with 1K (1024 byte) packets. On some systems and bulletin boards it may also be referred to as Ymodem. Some communication software programs, most notably Procomm Plus 1.x, also list Xmodem-1K as Ymodem. Procomm Plus 2.0 no longer refers to Xmodem-1K as Ymodem.
Xmodem-1K就是使用1024字節(jié)的包大小并加上CRC校驗的更高級的Xmodem。在有些系統(tǒng)和軟件里被稱為Ymodem醒第。
Ymodem
Ymodem is essentially Xmodem 1K that allows multiple batch file transfer. On some systems it is listed as Ymodem Batch.
Ymodem就是支持批量文件傳輸?shù)腦modem-1K協(xié)議渔嚷。
以上這些,都類似于TCP協(xié)議淘讥,每次傳輸都會校驗圃伶,但是速度會慢些。
Ymodem-g
Ymodem-g is a variant of Ymodem. It is designed to be used with modems that support error control. This protocol does not provide software error correction or recovery, but expects the modem to provide the service. It is a streaming protocol that sends and receives 1K packets in a continuous stream until instructed to stop. It does not wait for positive acknowledgement after each block is sent, but rather sends blocks in rapid succession. If any block is unsuccessfully transferred, the entire transfer is canceled.
Ymodem-g是Ymodem的一個變體蒲列,它支持錯誤校正窒朋。不過它并不自己處理,而是希望調(diào)制解調(diào)器來提供一個服務去處理蝗岖。這是一個流式的協(xié)議侥猩,數(shù)據(jù)發(fā)送出去并不等待正確響應再發(fā)下一包,而是一個包接一個包地發(fā)送抵赢。只要有一個包發(fā)送出錯欺劳,整個傳輸過程就將被取消。
顯然铅鲤,這個就類似于UDP協(xié)議划提,快,但是不保證穩(wěn)定性邢享。
Zmodem
Zmodem is generally the best protocol to use if the electronic service you are calling supports it. Zmodem has two significant features: it is extremely efficient and it provides crash recovery.
Like Ymodem-g, Zmodem does not wait for positive acknowledgement after each block is sent, but rather sends blocks in rapid succession. If a Zmodem transfer is canceled or interrupted for any reason, the transfer can be resurrected later and the previously transferred information need not be resent.
Zmodem一般被認為是最佳的協(xié)議鹏往。它有兩個很大的特性,就是它非常高效并且支持出錯時的自動恢復骇塘,并且不需要從頭開始伊履,就是它可以斷點續(xù)傳。
以上幾個協(xié)議介紹來自這里款违。
如何選擇
像我們的硬件設備升級唐瀑,比較關注的有幾個方面:
- 穩(wěn)定性 容錯率很低,一旦出錯插爹,就會比較麻煩哄辣。因為我們的設備在最初選擇硬件的時候就沒有預留多少空間可供備份请梢。
- 要適合與BLE這樣的設備進行交互。
綜上柔滔,YModem是Xmodem的升級版溢陪,它保證穩(wěn)定性。Zmodem支持快速傳輸睛廊,這個很好形真。
最終選擇是:Ymodem協(xié)議。
協(xié)議定制
基于我們具體的需求超全,在原有的基礎上加了一下前后的處理咆霜。
* 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
代碼實現(xiàn)
首先梳理一下它應該具有哪些模塊:
- 協(xié)議的核心實現(xiàn)
主要是負責數(shù)據(jù)傳輸過程中有關協(xié)議的部分,如在數(shù)據(jù)包上加入頭嘶朱,CRC蛾坯,驗證返回的正確性以及超時重發(fā)等。 - 一個協(xié)議工具類疏遏,封裝包數(shù)據(jù)的提供
- 一個文件數(shù)據(jù)的讀取模塊:它是耗時任務脉课,應該在子線程進行。
- 各種執(zhí)行狀態(tài)的監(jiān)聽
下面直接上代碼
協(xié)議的核心實現(xiàn)Ymodem
:
/**
* 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);
}
}
}
協(xié)議包工具類:
/**
* 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;
}
}
文件數(shù)據(jù)讀取類
/**
* 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();
}
}
各種狀態(tài)監(jiān)聽接口
/**
* 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();
完整代碼在Github上财异。
如果對您有幫助倘零,歡迎star、fork戳寸!
參考
Wikipedia YMODEM
xmodem呈驶、ymodem、zmodem
aesirot ymodem on github