Android藍牙BLE集成多設備最佳實踐
背景
公司開發了一款健康類APP,用戶可以通過APP連接外部藍牙BLE設備采集血糖,血壓,體重等多個常見健康類指標。因此APP需要同時集成多款設備(多個品牌的血糖儀,血壓計,體脂秤等)。每個廠家的設備對接協議是不同的,甚至連同一個設備的不同版本,協議都會有差距。在一個APP中跟多個設備對接,甚至在同一個界面中需要處理多個設備,界面跟協議混在一起,成為一個比較頭疼的問題。本文對如何在APP中支持多設備集成,并且在需要對接新設備的時候,容易擴展現有代碼提供了一個比較好的實踐思路。
原有實現
原來在界面Activity類中,集成多個設備時候的代碼通常如下所示(為了達到說明目的,代碼做了很多簡化,實際情況中設備對接的協議代碼邏輯要復雜的多):
private DeviceType mConnectedDeviceType; //連接設備類型
/**
- 處理集成設備發過來的數據
- @param uuid
- @param data
*/
private void processDeviceData(UUID uuid,byte[] data){
//首先判斷當前連接設備類型,再分別處理
if (mConnectedDeviceType == DeviceType.A){
//如果當前連接設備類型為A, 根據本地硬編碼的協議處理A類設備
processDeviceA(UUID uuid,byte[] data);
}else if (mConnectedDeviceType == DeviceType.B){
//如果當前連接設備類型為B, 處理B類設備
processDeviceB(UUID uuid,byte[] data);
}else if (mConnectedDeviceType == DeviceType.C){
//如果當前連接設備類型為C, 處理C類設備
processDeviceC(UUID uuid,byte[] data);
}
...
}
/**
- 處理A類設備的協議交互代碼
- @param uuid
- @param data
*/
private void processDeviceA(UUID uuid,byte[] data){
int step = parseCmdA(uuid, data);
if (step == 0x1){
showResultA(data); //顯示測量結果
}else if (step == 0x2){
writeCmd("cmd2a"); //向連接設備下發數據
}else if (step == 0x3){
writeCmd("cmd3a");
}
}
/**
- 處理B類設備的協議交互代碼
- @param uuid
- @param data
*/
private void processDeviceB(UUID uuid,byte[] data){
int step = parseCmdB(uuid, data);
if (step == 0x1){
showResultB(data); //顯示測量結果
}else if (step == 0x2){
writeCmd("cmd2b"); //向連接設備下發數據
}else if (step == 0x3){
writeCmd("cmd3b");
}
}
/**
- 處理C類設備的協議交互代碼
- @param uuid
- @param data
*/
private void processDeviceC(UUID uuid,byte[] data){
int step = parseCmdC(uuid, data);
if (step == 0x1){
writeCmd("cmd2c"); //向連接設備下發數據
}else if (step == 0x2){
showResultC(data); //顯示測量結果
}else if (step == 0x3){
writeCmd("cmd3c");
}
}</code></pre>
如何解決
以上代碼可以看出界面跟設備協議是強耦合在一起的。如果需要集成更多的設備那怎么辦?原有的類代碼勢必變得更復雜,難以維護。因此我們需要把設備間的協議交互邏輯與界面進行解耦,以保持單一職責的設計原則:界面只進行步驟和測量結果的更新展示,交互邏輯可以放到其他類中。在這個地方我們可以使用Adapter作為設備適配器,把設備間的交互封裝到Adapter里面去,集成不同設備的時候調用不同的Adapter處理即可。
我們如何設計Adapter呢?雖然每個設備的交互協議不一樣,但是其中一些操作卻是共性的,比如一開始總是要連接設備,連接成功后設置指定UUID的notification或者indication,然后向外部設備寫入數據(下發指令),或者等待外部設備數據變化上報,交互完成后再斷開設備。
因此我們可以把這些共性操作抽象成DeviceAdapter接口。DeviceAdatper接口主要包含上述的常用操作:
UUID[] notificationUUIDs() //設置notification的UUID
UUID[] indicatorUUIDs() //設置indicator的UUID
void connectThenStart(BleDevice bleDevice) //連接設備并進行協議交互
void disconnect() //斷開設備
void writeCharacteristic(UUID uuid, byte[] data) //向指定UUID的Characteristic寫入數據
void readCharacteristic(UUID uuid) //從指定UUID的Characteristic中讀取數據
void executeCmd(int cmd) throws EasyBleException //執行命令接口
void processData(UUID uuid, byte[] data) //解析外部設備發過來的數據
經過進一步的調研我們發現,設備的連接,斷開連接,設置notification/indication,寫入數據,讀取數據,這些操作本身都是完全一樣的,不同的是我們對協議數據本身的解析。所以這些操作我們可以用一個默認的抽象類DefaultAdapter來實現,DefaultAdapter實現DeviceAdapter接口,把對數據解析的功能延遲到子類去進行。針對A設備創建DeviceAdapterA繼承于DefaultAdapter,B設備創建DeviceAdapterB繼承于DefaultAdapter,不同的設備用不同的Adapter去處理。
如圖所示:
(點擊放大圖像)

解耦關鍵
Adapter設計完成后,那調用模塊(Client)又是如何知道針對A設備,用DeviceAdapterA處理;針對B設備用DeviceAdapterB處理的呢?
我們需要做到兩點:
- Adapter需要告訴客戶:它能處理哪些設備。
- 需要把adapter管理起來,連接設備后需要能找到相匹配的adapter去處理設備。
解決第1點很簡單,我們只需要在DeviceAdapter增加一個方法用來標識它能處理哪些設備,方法如下:
String[] supportedNames() //返回的String數組代表它能處理的設備名組合
第2點解決起來要復雜些,我們需要增加一個管理類BleCenterManager(門面模式),BleCenterManager的主要職責為:管理維護adapter,并對不同設備找到相匹配的adapter進行處理,主要包含如下方法:
public void startScan() //開始藍牙掃描
public void stopScan() //停止藍牙掃描
public void connectThenStart(BleDevice device) //連接并處理設備
public void addDeviceAdapterFactory(DeviceAdapter.Factory factory) //增加Adapter相應的Factory
Adapter創建
在深入講解BleCenterManager之前,我們可以先談談adapter的創建。adapter主要由客戶代碼根據交互協議創建,初始化的過程可能各不相同。因此BleCenterManager最好不直接創建adapter,委托相應的Factory進行,也就是通常所說的工廠方法模式。客戶提供adapter的時候,需提供與之對應的Factory,BleCenterManager負責管理這些factories,創建adapter的時候只需要調用factory.buildDeviceAdapter()方法即可。Factory針對抽象編程,設計為抽象類,核心代碼如下:
abstract class Factory{
protected BleCenterManager mBleCenterManager;
public Factory(BleCenterManager bleCenterManager) {
mBleCenterManager = bleCenterManager;
}
public abstract DeviceAdapter buildDeviceAdapter();
@Override
public String toString() {
return "Factory{}"+getClass().getName();
}
}
Factory與adapter之間關系如下:
(點擊放大圖像)

查找Adapter進行處理
Adapter和Factory設計完后,通過bleCenterManager.addDeviceAdapterFactory()方法添加到BleCenterManager內部的factory列表,添加factory的同時,factory創建對應的adapter并加入到adapter列表。添加完之后,BleCenterManager是如何找到device相匹配的adapter進行處理的呢?答案很簡單,逐一遍歷adapter列表,查找adapter的supportedNames()方法返回的String列表是否包含設備名。查找到第一個就返回,如果列表遍歷后查找不到就拋出異常。核心代碼如下:
查找Adapter
private DeviceAdapter findAppropriateDeviceAdapter(BleDevice bleDevice) throws EasyBleException {
//先判斷factory是否為空
if (mDeviceAdapterFactories == null || mDeviceAdapterFactories.isEmpty()){
throw new EasyBleException("Device adapter factories empty!");
}
//遍歷adapter列表
for (DeviceAdapter adapter:mDeviceAdapters){
String[] nameList = adapter.supportedNames();
if (nameList != null && nameList.length > 0){
for (String name:nameList){
//查找到名字符合的就返回
if (bleDevice.getDeviceName().equalsIgnoreCase(name)){
return adapter;
}
}
}
String[] nameRegExpList = adapter.supportedNameRegExps();
if (nameRegExpList != null && nameRegExpList.length >0){
for (String nameRegExp:nameRegExpList){
if (Pattern.matches(nameRegExp,bleDevice.getDeviceName())){
return adapter;
}
}
}
}
throw new EasyBleUnsupportedDeviceException(bleDevice);
}
查找到adapter后,調用adapter.connectThenStart()方法進行后續協議交互處理。
看完Adapter這部分,很多人都會覺得有些熟悉,這個設計跟Retrofit的CallAdapter很類似。對的,好的設計都是相通的,只是換了個形式,都是常用設計模式:適配器,工廠,單例等的組合。
結束語
通過Adapter與BleCenterManager的結合實現了協議邏輯與APP界面的解耦。上文中的代碼只是基本核心示例代碼,完整代碼已經開源到Github: https://github.com/nziyouren/EasyBle ,歡迎大家contribute。目前庫還處于初級階段,后續逐步會加一些功能,比如從網絡加載adapter,如何在APP不升級版本的情況下,動態擴展集成能力。
來自:http://www.infoq.com/cn/articles/android-bluetooth-ble