藍(lán)牙無線技術(shù)是一種全球通用的短距離無線技術(shù),具有耗電量低、成本低、安全性、穩(wěn)定性、易用性等優(yōu)點,尤其在物聯(lián)網(wǎng)設(shè)備上的占有率非常高,因此我們有必要對藍(lán)牙做深入的了解。本文從藍(lán)牙和Android手機(jī)的一次通信過程為引,講解Android藍(lán)牙通信上的一些問題和原理,以及調(diào)用方法。如有描述不當(dāng)?shù)牡胤竭€請指出。
例子
假設(shè)Android手機(jī)A要給藍(lán)牙設(shè)備B發(fā)送一條“hello”的消息,然后B會給A回一條“world”。我們應(yīng)該怎么做呢?
過程
過程可以大致分為三步:
- 尋找設(shè)備
- 連接
- 通信
尋找設(shè)備
A要怎么找到B呢,一般是由設(shè)備B按照一定的周期廣播數(shù)據(jù)包,然后A和B指定好協(xié)議,看廣播數(shù)據(jù)里有沒有協(xié)議約定的數(shù)據(jù),有的話則說明找到了B。
廣播的數(shù)據(jù)包分為四種
- ADV_IND
- ADV_DIRECT_IND
- ADV_NONCONN_IND
- ADV_SCAN_IND
一般發(fā)送的是ADV_IND包(可參考藍(lán)牙協(xié)議分析(5)_BLE廣播通信相關(guān)的技術(shù)分析。
Android設(shè)備通過系統(tǒng)提供的API開始接受廣播數(shù)據(jù),
//要先停止上一次的scan,不然無法啟動新的scan
bluetoothAdapter.getBluetoothLeScanner().stopScan(bleCallback);
bluetoothAdapter.getBluetoothLeScanner().startScan(getFilters(), getSettings(), bleCallback);
然后在callback里獲取廣播數(shù)據(jù)
private ScanCallback bleCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, final ScanResult result) {
}
}
ScanResult就是當(dāng)前接收到的廣播里數(shù)據(jù),在5.0以及5.0以上的api,會幫我們解析廣播的數(shù)據(jù),但是5.0以下的話,只是會把整個廣播數(shù)據(jù)傳回來。下面大致講解下廣播協(xié)議數(shù)據(jù)。
廣播協(xié)議分析:

官方文檔:
https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile
廣播的數(shù)據(jù),按 len 1字節(jié),TYPE1字節(jié), data (len -1)字節(jié)的順序組依次組織,key的含義在上面的表格中已經(jīng)給出。 下面舉個栗子
假設(shè)下面是設(shè)備B的廣播數(shù)據(jù)
02 01 06 05 03 F6 FE F5 FE 0E 09 5B 52 4F 41 44 42 49 54 5D 00 00 00 00 0D FF AA 00 66 EE 06 66 1F FF FF 25 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
解析數(shù)據(jù)
02 01 06 //長度2,type 1 表示設(shè)備功能, 6表示110 -> 普通發(fā)現(xiàn)模式,且不支持BR/EDR
05 03 F6 FE F5 FE //長度5,type 03,表示16位uuid列表, F6FE和F5FE兩組uuid
0E 09 5B 52 4F 41 44 42 49 54 5D 00 00 00 00 長度 0E,即14, type 9,即設(shè)備名稱,后面試試字符串轉(zhuǎn)16進(jìn)制
0D FF AA 00 66 EE 06 66 1F FF FF 25 04 00 , FF表示自定義數(shù)據(jù),即藍(lán)牙設(shè)備的自定義的數(shù)據(jù)
另外,設(shè)備的廣播頻率是可以自己定義的,從幾ms到幾百ms不等,廣播的頻率決定了手機(jī)發(fā)現(xiàn)設(shè)備的快慢,
有個軟件叫"nRF Connect",可以很方便的觀看藍(lán)牙設(shè)備的廣播速度。下載地址
軟件截圖:

上圖的Adv. Interval就是廣播的間隔,上文講到的所有東西都可以在這個軟件上觀察到。
連接過程
藍(lán)牙分為5種工作狀態(tài),
- 準(zhǔn)備(standby):就緒狀態(tài),準(zhǔn)備轉(zhuǎn)變?yōu)槠渌麪顟B(tài)
- 廣播(advertising):向外發(fā)送數(shù)據(jù)的狀態(tài)
- 監(jiān)聽掃描(Scanning): 掃描狀態(tài)的時候,在接受到ADV_IND包是,會發(fā)送SCAN_REQ包,可以獲得更多的信息。
- 發(fā)起連接(Initiating):發(fā)起連接狀態(tài),在ADV_IND或者ADV_DIRECT_IND之后,會發(fā)送CONNECT_REQ包,從而建立連接。
- 已連接(Connected):根據(jù)連接時約定的參數(shù),發(fā)送CONNECT_EVENT,保持連接不斷開。
具體工作流程如下:
- 可被連接的設(shè)備(Advertiser),按照一定的周期廣播ADV_IND或者ADV_DIRECT_IND包(可參考“藍(lán)牙協(xié)議分析(5)_BLE廣播通信相關(guān)的技術(shù)分析”)。
- 主動連接的設(shè)備(Initiator),在收到廣播包之后,會回應(yīng)一個CONNECT_REQ請求,該請求攜帶了可決定后續(xù)“通信時序”的參數(shù),例如雙方在哪一個時間點、哪一個Physical Channel收發(fā)數(shù)據(jù),等等。
- Initiator在發(fā)出CONNECT_REQ數(shù)據(jù)包之后,自動轉(zhuǎn)變?yōu)镃onnection狀態(tài),成為Master角色(注意:這是“自動”的,不需要等待另一方的回應(yīng))。同樣,Advertiser在收到CONNECT_REQ請求之后,也自動轉(zhuǎn)變?yōu)镃onnection狀態(tài),成為Slave角色。
- 此后,雙方按照CONNECT_REQ參數(shù)所給出的約定,定時到切換到某一個Physical Channel上,按照Master->Slave然后Slave->Master的順序,收發(fā)數(shù)據(jù),直至連接斷開。
在Android手機(jī)中,連接的代碼非常簡單,只有一個api可以調(diào)用,如下:
private BluetoothGatt connectGattCompat(BluetoothGattCallback bluetoothGattCallback, BluetoothDevice device, boolean autoConnect) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return device.connectGatt(context, autoConnect, bluetoothGattCallback, TRANSPORT_LE);
} else {
return device.connectGatt(context, autoConnect, bluetoothGattCallback);
}
}
之后在bluetoothGattCallback的onConnectionChange的回調(diào)里,就可以知道連接的結(jié)果。
通信
說道通信,就要講到跳頻。跳頻的原理是
藍(lán)牙這邊在通信的時候,會約定一個隨機(jī)的頻率(也不是完全隨機(jī),有一個范圍)。雙方在這個頻率上通信,這樣通信更穩(wěn)定。所以Ble在掃描和連接兩個步驟中,可能耗時比較長,但是開始通信之后,速度和穩(wěn)定性都會加快不少。
而跳頻的參數(shù),是在連接命令里發(fā)過去的。
藍(lán)牙設(shè)備,都會注冊service和characteristic,并且用uuid標(biāo)識。
service 可以理解為一個服務(wù),在BLE從機(jī)中有多個服務(wù),電量信息,系統(tǒng)服務(wù)信息等,每一個service中包含了多個characteristic特征值,每一個具體的characteristic特征值才是BLE通信的主題。
characteristic特征值:BLE主機(jī)從機(jī)通信均是通過characteristic進(jìn)行,可以將其理解為一個標(biāo)簽,通過該標(biāo)簽可以讀取或?qū)懭胂嚓P(guān)信息。
大家可以理解為service就是java里的class,characteristic是class里的public方法。所以我們應(yīng)該先找到class,再調(diào)用其方法。
再往下想,寫和讀肯定是兩個方法,所以也會是兩個characteristic。
service和characteristic的uuid都是通信之前雙方約定好的。android端在上文說的discoverService之后,查看有沒有對應(yīng)的uuid,如果有,就可以發(fā)起通信了。
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
listener.onServicesDiscovered(status);
}
@Override
public void onServicesDiscovered(final int status) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
service = getGatt().getService(ServerUUID);
if (null != service) {
BluetoothGattCharacteristic read_characteristic = service.getCharacteristic(readDataUUID);
if (null != read_characteristic) {
int properties = read_characteristic.getProperties();
if ((properties | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
getGatt().setCharacteristicNotification(read_characteristic, true);
}
}
}
}
}
setCharacteristicNotification 成功后,就可以在onCharacteristicChanged方法收到回調(diào)了。
藍(lán)牙通信的過程,就是往writeCharacteristic里寫數(shù)據(jù),然后監(jiān)聽readCharacteristic的返回。這兩個操作保持有序進(jìn)行,就可以通信了。
過程總結(jié)
針對上文說的例子, 畫個圖來總結(jié)一下

藍(lán)牙采坑總結(jié)
scan過程
據(jù)測試,Android5.0以下的手機(jī),是不支持128位的uuid的,只支持16位的。如果過濾的uuid填入128位的話,是搜索不到設(shè)備的。所以要分別判斷。
部分手機(jī)(三星遇到過),在藍(lán)牙關(guān)閉情況下,仍然可以使用ble功能,但是經(jīng)常會有問題,所以請務(wù)必打開藍(lán)牙再開始通信。
部分手機(jī),如果1次scan不到,后續(xù)都不會scan到了,這時要停止scan,重新啟動一次。
連接錯誤
國內(nèi)Android手機(jī)的藍(lán)牙不知道為何,非常不穩(wěn)定。在連接的過程中,會有各種各樣的錯誤,我用了github上總結(jié)的錯誤,
https://github.com/Twelvelines/AndroidMuseumBleManager/blob/ef5e866c0aa8955320bac7ee9884f60be5bbe1d5/bluetooth-manager-lib/src/main/java/com/blakequ/bluetooth_manager_lib/connect/GattError.java
或者
https://blog.csdn.net/ocean20/article/details/65431478
這些錯誤會頻繁的遇到,所以為了成功率更高,需要在連接的時候增加重試機(jī)制。
遇到這些錯誤,立刻斷開連接,等待1-2秒后再次嘗試連接。
具體見以下代碼:
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
BleLog.d("onConnectionStateChange", "status: " + status + " newState: " + newState);
if (status != BluetoothGatt.GATT_SUCCESS) {
BleLog.e(TAG, "連接狀態(tài)異常->" + GattError.parseConnectionError(status));
disconnectInternal(false);
onConnectError(status);
}
onConnectError的回調(diào)里,可以直接去重試連接。這樣可以提高成功率。但是重新連接之前一定要close當(dāng)前連接,否則會出現(xiàn)各種問題。
但是重試隨之而來的問題是老的gatt對象可能仍然會回調(diào)(系統(tǒng)回調(diào)不知道什么時候回來),所以在接收回調(diào)的時候,最好判斷一下當(dāng)前的gatt對象是否是最新的。
然后還有就是通信問題,通信相對于連接來說,是穩(wěn)定很多的,但是仍然會有一定的幾率,收不到消息。所以硬件設(shè)備和手機(jī)的藍(lán)牙代碼,最好有一方有重試機(jī)制,一般來說手機(jī)重試就足夠了。
參考文獻(xiàn)
- 蝸窩科技 http://www.wowotech.net
- BluetoothKit作者博文 BluetoothKit
- 簡書
后續(xù)會對android連接的源碼分析一波