YModem在Android上的实现

(一)参考文献


【安卓相关】蓝牙基于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协议传输文件。以下是代码的简要总结:

  1. 常量定义:定义了传输步骤(HELLO、FILE_NAME、FILE_BODY、EOT、END)和一些控制字符(ACK、NAK、CAN、ST_C)以及MD5校验相关的字符串。

  2. 成员变量:包括上下文(Context)、文件路径、文件名、文件MD5值、传输监听器(YModemListener)、计时器助手(TimeOutHelper)、文件流线程(FileStreamThread)、已发送字节数、当前发送的数据包、错误计数等。

  3. 构造函数:初始化Ymodem对象,接受文件路径、文件名、文件MD5值和监听器作为参数。

  4. 传输控制方法

    • start(): 开始传输,调用sayHello()方法。
    • stop(): 停止传输,重置相关变量,释放资源。
  5. 接收数据方法

    • onReceiveData(byte[] respData): 处理从终端接收的数据,根据当前传输步骤调用相应的处理方法。
  6. 数据发送方法

    • sayHello(): 发送HELLO包。
    • sendFileName(): 发送文件名包。
    • startSendFileData(): 开始发送文件数据包。
    • sendEOT(): 发送EOT包。
    • sendEND(): 发送END包。
  7. 响应处理方法

    • handleHello(byte[] value): 处理HELLO包的响应。
    • handleFileName(byte[] value): 处理文件名包的响应。
    • handleFileBody(int character): 处理文件数据包的响应。
    • handleEOT(byte[] value): 处理EOT包的响应。
    • handleEnd(byte[] character): 处理END包的响应。
    • handleOthers(int character): 处理其他响应(如NAK、CAN)。
  8. 失败处理方法

    • handlePackageFail(): 处理数据包发送失败,重试发送,超过最大重试次数则停止传输。
  9. 构建器类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协议的数据打包工具,主要功能包括:

  1. 定义常量

    • HELLO: 自定义的启动信号字符串。
    • SOH: 表示128字节数据包的头部标志。
    • STX: 表示1024字节数据包的头部标志。
    • EOT: 传输结束标志。
    • CPMEOF: 用于填充未满数据包的字节。
  2. 计算CRC16校验

    • 使用CRC16类来计算数据包的CRC校验值。
  3. 生成数据包

    • getYModelHello(): 获取启动信号的字节数组。
    • getFileNamePackage(): 生成包含文件名、文件大小和文件MD5值的数据包。
    • getDataPackage(): 生成带有头部、数据块和CRC校验的数据包,并填充不足部分。
    • getEOT(): 获取传输结束数据包。
    • getEnd(): 获取最后一个填充128字节的数据包。
    • getInputStream(): 从资源文件中获取输入流(可定制其他来源)。
  4. 私有辅助方法

    • 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();

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值