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,做到相容性和速度的平衡。