1. 程式人生 > >Android BLE藍芽oad升級實踐之路

Android BLE藍芽oad升級實踐之路

最近專案中用到了ble的藍芽升級功能,看到網上基本找不到android的oad升級資源,只有一個demo原始碼包(文章最後會放置這個檔案),網上基本都是OTA升級介紹,正好有空,來說說我的填坑之路,最近做了個實驗發現可以大大提高藍芽升級速度,遂做這次補充,補充在最後。

1.OAD升級原理
oad升級有2個檔案,都是bin格式的檔案,imagA和imagB,兩個映象檔案,為了防止藍芽升級出錯,需要先查詢當前藍芽的映象型別是哪個,如果是A映象就用B檔案去升級,如果是B映象就用A去升級。

2.藍芽映象型別和版本查詢
查詢型別和版本需要使用相關的service和characteristic,下面是我的專案中使用到的,具體是那個還要看自己的情況
BT_OAD_SERVICE
@”F000FFC0-0451-4000-B000-000000000000” //服務
BT_OAD_IMAGE_NOTIFY
@”F000FFC1-0451-4000-B000-000000000000” //查詢版本,傳送升級通知
BT_OAD_IMAGE_BLOCK_REQUEST
@”F000FFC2-0451-4000-B000-000000000000” //傳送升級檔案

Descriptor描述符UUID 00002901-0000-1000-8000-00805f9b34fb
Descriptor描述符UUID 00002902-0000-1000-8000-00805f9b34fb

Note that: F000FFC1:這特徵值用於傳送版本查詢動作和傳送升級通知給藍芽裝置。F000FFC2:這個特徵值用於傳送升級檔案
FFC1和FFC2都有這2個描述符,IOS不需要設定描述符資訊就可以接受notify資訊,但是android不設定的話我沒能收到資訊,所以為FFC1和FFC2設定notification的時候需要同時設定描述符的值,示例如下:

//設定notification
mBluetoothGatt.setCharacteristicNotification
(characteristic, enabled); // 判斷是否是藍芽升級服務,這裡有其他的服務,是專案其他需求,看格式就行,自己是那個服務和characteristic就判斷哪個,UUID_QUERY_IMAGE就是FFC1,UUID_UPDATE_BT就是FFC2,這兩個是我定義的字串常量,是對應的characteristic的UUID if(UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid()) || UUID_QUERY_IMAGE.equals(characteristic.getUuid()) || UUID_UPDATE_BT.equals
(characteristic.getUuid())) { //00002902開頭的描述符UUID是我專案用到的 BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); }

這樣就設定好了notification,現在傳送給藍芽裝置訊息,就可以收到回執了,有訊息返回會呼叫onCharacteristicChanged(luetoothGatt gatt,
BluetoothGattCharacteristic characteristic),在這裡接受到回執,可以判斷下是哪個characteristic的回執資訊,有時候FFC1發訊息,FFC2也會回執訊息。

3,傳送查詢映象版本資訊
接下來我們需要發訊息查詢映象版本和型別,示例如下

 new Thread(new Runnable() {
            @Override
            public void run() {
            //啟動一個執行緒執行查詢任務
                byte[] data = new byte[1];
                data[0] = 0;
                //傳送0,傳送成功後,間隔200ms左右傳送1,query_cha是FFC1的characteristic。
                query_cha.setValue(data);
                boolean success = false;
                success = mBluetoothLeService.writeCharacteristic(query_cha);
                Logger.d(success+"");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //傳送1
                data[0] = 1;
                query_cha.setValue(data);
                success = mBluetoothLeService.writeCharacteristic(query_cha);
            }
        }).start();

傳送訊息成功,藍芽裝置會回覆0000,但是這對於我們沒太大用處,傳送完0,1,回覆我們01 00 00 7C 42 42 42 42類似這樣的資訊,一共8個位元組(我們後面發的升級通知給藍芽裝置其實也就是發這種形式的資訊),這些都是16進製表示。第一個位元組01是映象版本,7C*4其實就是映象檔案的真正大小,42是映象型別,ASCII碼代表的是字母”B”,也就是說藍芽裝置現在是映象B,我們升級的話就要發給他映象A檔案。我的專案這邊,藍芽裝置發來的版本資訊有個計算規則,如果是映象A,版本號就除以2,比如版本為2,我這邊就是除以2,算出版本為1,再和我這邊定義版本號做對比,如果是映象B就先-1再除以2,比如發來的版本號是1我這邊算出是0。具體應該是根據嵌入式那邊的來。

4.傳送升級通知
獲得了版本號和映象型別,對比版本後如果可以升級,就先載入升級檔案,查詢到是映象B就用A去升級,反之亦然。

private boolean loadFile(String filepath, boolean isAsset) {
        boolean fSuccess = false;

        // Load binary file
        try {
            // 判斷是否是Asset資料夾下的檔案,是的話開啟檔案
            InputStream stream;
            if (isAsset) {
                stream = getAssets().open(filepath);
            } else {
                File f = new File(filepath);
                stream = new FileInputStream(f);
            }
            /**獲取檔案大小,有時候available()無法得到檔案大小,但是這裡還可以獲得,不同的類裡面可能對available()方法重寫的不同,有的可能不是獲取檔案大小*/
            dataSize = stream.available();
            //根據檔案大小建立陣列,將內容存到陣列中
            mFileBuffer = new byte[dataSize];
            stream.read(mFileBuffer, 0, dataSize);
            stream.close();
        } catch (IOException e) {
            // Handle exceptions here
            return false;
        }
        /**要傳送的升級通知訊息,FILE_HEADER_SIZE這裡設定的是8,其實應該就發8個位元組,但是demo中又來了個+2+2,索性就按照他的來*/
        byte[] prepareBuffer = new byte[FILE_HEADER_SIZE + 2 + 2];
        /**設定要傳送的內容,從檔案陣列下標4將內容複製到prepareBuffer陣列中01 00 00 7C 42 42 42 42一定包含這樣型別的位元組資訊,其實就是通知藍芽裝置傳送的映象版本01,大小7C(嵌入式那邊7C*4就是檔案長度,我猜就是通過這個我們最後才不用傳送結束訊號,裝置那邊就能自動判斷是否傳送結束,有興趣的可以打印出來要傳送的檔案的前12個位元組看看)。*/
        System.arraycopy(mFileBuffer, 4, prepareBuffer, 0, FILE_HEADER_SIZE);
        //將資訊傳送到藍芽裝置
        query.setValue(prepareBuffer);
        fSuccess = mBluetoothLeService.writeCharacteristic(query);
        return fSuccess;
    }

5.傳送升級檔案

//整個過程開啟了一個非同步任務類,用handler更新介面
 protected Void doInBackground(Void... params) {
            int nowSize = 0;
            mHandler.sendEmptyMessage(0);
            boolean over = loadFile(finalPath, true);  /**傳送升級通知,判斷是否傳送成功,間隔一段時間開發傳送檔案*/
            if (over) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mHandler.sendEmptyMessage(1);   //傳送檔案

/**用於傳送升級檔案的characteristic在升級過程中會每傳送一個包就回復一次,很耽誤效率,所以最好先設定成不回覆訊息 */               update.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);

/**EACH_PACKAGE_SIZE代表每個資料包資料部分的大小,這裡按照demo中設定的一樣16,計算一共需要傳送多少個包*/
                int blockNum = dataSize / EACH_PACKAGE_SIZE;
                //計算有沒有不滿16位元組的資料包,有的話加上這個包
                int lastNum = dataSize % EACH_PACKAGE_SIZE;        //檢測是否有最後一個不滿足16位元組的包
                int lastBlockNum = blockNum;       //總包數
                if (lastNum != 0) {
                    lastBlockNum = blockNum + 1;
                }
                Logger.d("分的包數" + lastBlockNum);
                /**這裡設定的升級鎖,如果為真,如果有其他傳送給藍芽的訊息一概丟棄,避免對升級造成影響*/
                ComService.UPDATE_ROM_LOCK = true;
                for (int i = 0; i < lastBlockNum; i++) {
                    //中途失敗就取消非同步任務,每次都判斷下是否取消
                    if (isCancelled()) {
                        return null;
                    }
                    /**最終要傳送的包還要加2個頭位元組,表示傳送的包的索引,索引從0開始,索引從0開始,索引從0開始,重要的話說三遍,這是折磨我很久的坑,但是不知道是不是都是需要從0開始,我之前從1開始一直不行,低位在前,高位在後,比如第257個包,temp[0]就是1,temp[1]是1,都是16進製表示,還原到2進位制就是0000 0001 0000 0001,這就是頭2個位元組組合後代表的數字257*/
                    byte[] temp = new byte[2 + EACH_PACKAGE_SIZE];
                    //低位
                    temp[0] = (byte) (i & 0xff);
                    //高位
                    temp[1] = (byte) (i >> 8 & 0xff);
                    Logger.d("正在發包" + i + "/" + lastBlockNum);
                    //每次偏移資料部分大小,將檔案陣列複製到傳送的包中。
                    System.arraycopy(mFileBuffer, i * EACH_PACKAGE_SIZE, temp, 2, EACH_PACKAGE_SIZE);
                    Logger.d("傳送包內容" + ByteUtil.bytetoShortHex(temp));
                    update.setValue(temp);
                    boolean b = mBluetoothLeService.writeCharacteristic(update);
                    Logger.d("傳送檔案結果" + b);
                    if (b) {
                        nowSize = nowSize + EACH_PACKAGE_SIZE;
                        Message message = Message.obtain();
                        message.what = 4;
                        message.arg1 = (int) ((i + 1) / (float) lastBlockNum * 100);

                        mHandler.sendMessage(message);  //更新進度條
                        try {

/**這裡需要注意傳送的間隔時間 不能太短 不能太短  不能太短! demo中預設設定是20ms,但是android實際測試完全不行,後來我改成50ms升級成功了,但是第二天再試又出錯了,又改成了60ms,升級有成功了,但是隻是在小米試過,其他的不知道60ms會不會出問題,還是ios強大,間隔20ms完全沒問題!
這裡的mHandler都是用來更新介面的,比如重新整理進度條。*/                                Thread.sleep(SEND_TIME_INTERNAL);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        mHandler.sendEmptyMessage(2);
                        cancel(true);
                        break;
                    }
                }


            } else {
                mHandler.sendEmptyMessage(2);    //錯誤相關
                cancel(true);
            }

6.更新成功識別和總結
完成了上面的步驟就可以等著藍芽自己更新了,更新完成我這邊藍芽會自動斷開一次,可以以此判斷是否升級成功,當然更好的方法還是再次查詢藍芽版本和映象型別,映象型別從A變B或者從B變A就表示成功了。總結oad升級需要一下幾步

找到對應服務和特徵值,可能還需要找到描述符
為特徵設定notification,為描述符設定開啟notification資訊。
傳送查詢版本和映象資訊
根據查到的版本和映象型別決定傳送給裝置的檔案,
提取要傳送的檔案的幾個位元組資料,從索引4開始,可以先試試提取8個位元組看看能行不,傳送給裝置
間隔200-300ms開始傳送檔案,利用FFC2傳送,最好設定FFC2為無回執

8,近期實驗提高藍芽升級速度
上次一個道友說通過接收到oncharacteristicWrite回撥再發送下一個包太慢,當時也沒太好的辦法就推薦間隔固定時間發下一包,最近發現,可以吧傳送升級檔案的那個characteristic設定成沒有回執,setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);前面的程式碼就是這麼設定,但是發現還是會接收到裝置的回執資訊,但是再多一個操作就可以做到無回執,characteristic.setCharacteristicNotification(Characteristic cha,boolean enabled),設定為false,還有他所屬的Descriptor設定成關閉notification的狀態,就是和上面開始notification反著來,關閉notification,然後發現設定成每包傳送間隔為20ms都能升級成功 直接提高2倍速度,有時候設定間隔10ms都能成功,大家可以試試10-30ms,做到相容性和速度的平衡。