這裡不記錄具體程式碼規則,後面會給出參考文章,別人已經寫很詳細了,我就單純記錄下踩過的坑吧;
1. 版本支援
Android 從 4.3(API Level 18) 開始支援低功耗藍芽(Bluetooth low energy),但是隻支援作為中心裝置(Central)模式,這就意味著 Android 裝置只能主動掃描和連結其他外圍裝置(Peripheral),從 Android 5.0(API Level 21) 開始兩種模式都支援。 P.S. 不過也不是5.0以上就全部都支援,之前測試到魅族M2貌似就開不起peripheral模式,畢竟硬體相關,很難保證,我同事之前開發時候甚至碰到過某些裝置會固定少發一個位元組,也是坑啊...
2. 踩過的坑
2.1 開啟peripheral模式
之前以為開啟了手機藍芽和gps功能, 手機就能被central裝置搜尋到, 那是經典藍芽, 要想啟用BLE功能並作為peripheral從機,需要使用 BluetoothLeAdvertiser
開啟廣播模式:
P.S. BLE連結不會彈出連線請求,比經典藍芽方便,畢竟不打擾使用者,另外,查到的資料說,BLE central大概最多同時連結7臺裝置左右;
/**
* 開啟廣播模式,用於本機被其他central裝置搜尋到
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun startAdvertising() {
if (isBluetoothEnable()
&& !isAdvertising
&& isSupportAdvertisement
&& mBluetoothLeAdvertiser != null
&& mGattServer != null) {
val success = mGattServerCallBack.setupServices(mGattServer)
Logger.d("startAdvertising result = $success ", TAG)
if (success) {
mBluetoothLeAdvertiser?.startAdvertising(createAdSettings(true, 0), createAdData(), mAdCallback)
}
} else {
Logger.d("startAdvertising fail", TAG)
}
}
複製程式碼
2.2 藍芽地址動態變化
參考這篇 Google在Android6.0上修改了獲取裝置標識資訊功能:
// 以下方法固定返回: 02:00:00:00:00:00
WifiInfo.getMacAddress()
BluetoothAdapter.getAddress()
複製程式碼
坑爹的是,假設central裝置掃描得到peripheral的藍芽地址記為: A , 連線同一臺peripheral裝置時獲取的藍芽地址記為B, A跟B還不一致,又動態變化了,真是坑啊:
之所以會想要記錄裝置藍芽地址,是想作為唯一識別符號,在轉傳資訊時,不要再回傳到資料來源方, 比如 A 傳送資料給 B, B再往其他裝置轉傳時,就不需要回傳給A了,但是地址動態變化的話,我就沒轍了,有解決方案的話麻煩告知我一下;
// 低功耗藍芽掃描回撥
var mLeScanCallback: ScanCallback? = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
// Logger.d("scan successful $result")
// 這裡通過ScanResult獲取到的藍芽地址A,跟通過手機系統設定頁面檢視得到的藍芽地址是不同的,而且每次重新開啟peripheral模式後,同一臺手機的藍芽地址就又變化了
//
// 另外,同一臺裝置會在短時間內被掃描到很多次,因此不是需要對裝置進行過濾判斷
addBleDevice(result)
}
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
super.onBatchScanResults(results)
results?.forEach { addBleDevice(it) }
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
if (ScanCallback.SCAN_FAILED_ALREADY_STARTED != errorCode) {
isScanningBle = false
}
Logger.d("scan failed errorCode = $errorCode")
}
}
複製程式碼
2.3 自定義characteristic UUID
之前以為只要符合uuid模式: 00000000-0000-0000-0000-000000000000
(8-4-4-4-12)隨便定義即可, 後來看了 這篇 才發現不是這樣的,能自定義的只是其中一部分,有興趣的可以去研究下 BLE文件;
0000????-0000-1000-8000-00805f9b34fb
????就表示4個可以自定義16進位制數
2.4 跟iOS通訊時迴圈寫入資料失敗
我們是通過 Characteristic
來寫入的, 它有個屬性來指明傳送時不需要響應: BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
, 而我在跟iOS互動時,貌似這個欄位雙方設定不一致,導致傳送後一直沒收到響應,然後iOS就一直重發;
因此,需要在作為peripheral模式時,新增的characteristic需要設定為: BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
;
另外,作為central裝置往其他裝置傳送訊息時,也需要新增該屬性:
- Android和iOS使用同一套BLE協議,因此可以通訊,如果是wifi direct的話,就不行了;
- Android 4.3雖然也支援central模式,但是查到的文章有說在跟iOS引數互動時有問題,而我使用4.3來搜尋其他Android裝置也經常找不到,因此就直接不考慮了,從5.0開始;
/**
* 接收資料時,通過本類回撥處理
*/
class GattServerCallBack : BluetoothGattServerCallback() {
companion object {
private val TAG = "GattServerCallBack"
}
private var mGattServer: BluetoothGattServer? = null
/**
* 初始化需要用來轉傳資料的 service/characteristic
* */
private val mRelayService by lazy {
val service = BluetoothGattService(UUID.fromString(BleConstant.RELAY_SERVICE_UUID), BluetoothGattService.SERVICE_TYPE_PRIMARY)
val characteristic = BluetoothGattCharacteristic(
UUID.fromString(BleConstant.RELAY_CHARACTERISTIC_UUID),
BluetoothGattCharacteristic.PROPERTY_READ
or BluetoothGattCharacteristic.PROPERTY_WRITE
or BluetoothGattCharacteristic.PROPERTY_NOTIFY
or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, // 這裡設定不需要回應,也可選擇需要響應模式
BluetoothGattCharacteristic.PERMISSION_READ
or BluetoothGattCharacteristic.PERMISSION_WRITE)// 可寫模式,不同ble裝置間通過本characteristic來傳輸資料
characteristic.setValue(BlePara.adCharacteristicValue)
val addCharacteristic = service.addCharacteristic(characteristic)
Logger.d("addCharacteristic result = $addCharacteristic", TAG)
service
}
/**
* 廣播開始後,設定一個用於接收訊息的service
* 後續有資料傳入時,會觸發 [org.lynxz.ble_lib.callbacks.GattServerCallBack.onCharacteristicWriteRequest]
* */
fun setupServices(gattServer: BluetoothGattServer?): Boolean {
if (gattServer == null) {
return false
}
// 設定一個GattService以及BluetoothGattCharacteristic
mGattServer = gattServer
val service = mGattServer?.getService(UUID.fromString(BleConstant.RELAY_SERVICE_UUID))
if (service == null) {
val addResult = mGattServer?.addService(mRelayService)
Logger.d(" -> 新增自定義service...result = $addResult", TAG)
} else {
Logger.d(" -> 新增自定義service... service已存在,不用重複新增", TAG)
}
return true
}
override fun onCharacteristicWriteRequest(device: BluetoothDevice?, requestId: Int, characteristic: BluetoothGattCharacteristic?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) {
super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value)
// 按需傳送響應
var responseResult = true
if (responseNeeded) responseResult = mGattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null) ?: false
Logger.d("responseNeeded = $responseNeeded ,send response result = $responseResult , receive data length = ${value?.size}")
}
}
複製程式碼
// 作為central裝置,通過characteristic傳送資料時
val service = gatt.getService(UUID.fromString("*********")) ?: return false
val relayChar = service.getCharacteristic(UUID.fromString("*********")) ?: return false
val headPackage = ByteArray(20)
relayChar.value = headPackage
relayChar.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
val result = gatt.writeCharacteristic(relayChar)
複製程式碼
2.5 傳送超過20位元組資料
擴充套件閱讀 BLE預設單次傳輸長度為20位元組, 對於超過該長度的資料,有兩種方式進行處理:
- 修改MTU值(最大為512位元組) 在跟iOS互動的時候,發現它一次性可以往Android傳送512位元組(Android使用預設設定),後來才發現Android裝置間也可以重新指定該值,不過使用這種方式的話,我測試到有這種現象: mtu設定回撥成功,central裝置傳送資料也成功,但peripheral裝置卻不能完整接收到,比如我設定512位元組,但收到的可能只有140位元組,因此我沒有采用這種方式:
mGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
val device = gatt.device
Logger.d("onConnectionStateChange newState = $newState ${device.address}")
if (BluetoothGatt.STATE_CONNECTED == newState) {
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
Logger.d("設定mtu結果 : ${gatt.requestMtu(BlePara.mtu)}"
} else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
gatt.close()
}
}
// mtu設定成功後才去搜尋service/characteristic,然後才可以傳輸資料
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
Logger.d(" mtu = $mtu $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.discoverServices();
}
}
}
複製程式碼
- 對資料進行分包操作,新增控制資訊
分為三部分,每個分包固定20位元組: a. head包,包含一些控制資訊,如傳送的資料長度,用於整合資料包 b. 使用者要傳送的資料內容(可加密); c. tail包,所有資料傳送完成後,傳送一個結束資訊(主要是避免head包傳送失敗時,接收方一直在等待傳送結束,當然,若是tail包也傳送失敗,則需要通過接收超時機制來控制) P.S. 跟iOS的同學交流後發現,iOS裝置間單次最大也只是能傳送512位元組,因此應該也有分包的需求;
2.6 分包傳送時間間隔過長的問題
stack overflow 連續通過characteristic寫入資料時,相鄰分包之間需要間隔一下,之前測試發現100ms失敗率比較大,200ms就比較ok,但是也有一定概率失敗,而且,單包20位元組 ,我要傳輸的資料基本都要400位元組左右,總耗時(包括連線等)就可能達到5s以上,感覺時間還是太長,兩種方式來避免:
- 修改
requestConnectionPriority()
值為BluetoothGatt.CONNECTION_PRIORITY_HIGH
這樣設定後,分包之間設定為20ms就沒再發現有出問題過(至少我手頭的機型沒出錯過)
private var mGattCallback: BluetoothGattCallback? = null
mGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)
val device = gatt.device
Logger.d("onConnectionStateChange newState = $newState ${device.address}")
if (BluetoothGatt.STATE_CONNECTED == newState) {
Logger.d("onConnectionStateChange STATE_CONNECTED = $newState ,gatt == mGatt? = ${gatt == mGatt}")
// 傳送大資料時設定如此,有人建議傳送完成後要設定成預設的: CONNECTION_PRIORITY_BALANCED
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// REFACTOR: 17/06/2017 可以設定mtu大小,若啟用此方式,則請在onMtuChanged()回撥成功後再搜尋及傳送資料,但Android之間測試發現接收方有些只能收到152個位元組,暫時不考慮,後續研究
// Logger.d("設定mtu結果 : ${gatt.requestMtu(BlePara.mtu)}"
// 連線成功,開始搜尋service
gatt.discoverServices()
} else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
// gatt連線斷開
Logger.d("onConnectionStateChange STATE_DISCONNECTED = $newState")
gatt.close()
}
}
}
複製程式碼
- 新增錯誤重傳機制,重傳時間間隔增加 傳送分包時不可避免可能出錯,若預設分包間隔為20ms,傳送失敗後,可嘗試重傳一次,重傳時的時間間隔略微設定大些,如200ms,這樣仍能有效減小總髮送時間;
var result = true // 傳送資料是否成功
val delay = 20 // 分包之間的延時,單位:毫秒
try {
// 注意,這裡需要延時一下,不然測試發現,基本上只能收到其中幾幀的資料,失敗的概率比較大
Thread.sleep(delay.toLong())
var i = 0
while (i < size) {
var to = i + 20
if (to >= size) {
to = size
}
val slice = Arrays.copyOfRange(encryptedContentBytes, i, to)
relayChar.value = slice
var sliceResult = gatt.writeCharacteristic(relayChar)
Logger.d("傳送第 $i ~ $to 塊資料的結果: $sliceResult", TAG)
// 傳送失敗時,嘗試重傳一次就好
if (!sliceResult) {
Thread.sleep(200)
sliceResult = gatt.writeCharacteristic(relayChar)
Logger.d(" =>重傳第 $i ~ $to 塊資料的結果: $sliceResult", TAG)
}
result = result and sliceResult
i = to
Thread.sleep(delay.toLong())
// 由於只重傳一次, 因此如果某個資料分包重傳失敗,則不必要再傳後續資料,直接返回失敗
if (!result) {
break
}
}
} catch (e: Exception) {
e.printStackTrace()
result = false
}
複製程式碼
2.7 藍芽抓包,日誌檢視
之前跟iOS互動出錯後,app層回撥可看到的資訊比較少, 查到的資料 又都說有某個控制引數出錯, 沒發現characteristic設定有問題前,就想著要抓包看看具體的引數互動, 未找到實時抓包的簡單方法, 倒是可以通過Android手機的hcidump功能來獲取日誌,然後通過 wireshark 來檢視:
- 檢視hci日誌檔案路徑
// 我使用nexus 6p 7.1.1系統,配置檔案位於如下位置:
adb shell cat /etc/bluetooth/bt_stack.conf
// 檔案中有一條配置資訊,指示了log檔案所在路徑
BtSnoopFileName=/sdcard/btsnoop_hci.log
複製程式碼
- 抓取/匯出hci日誌
// 先清除原先的日誌
adb shell rm /sdcard/btsnoop_hci.log
// 通過手機系統開啟日誌功能: settings-developer options -- enable bluetooth hci snoop log
// 抓取結束後,匯出log檔案到pc上
adb pull /sdcard/btsnoop_hci.log
複製程式碼
不過, 一開始做ble沒經驗,可以先下載些軟體來測試下ble功能,這裡推薦一個 nRF24L01 , 具體請參考 這篇文章, 好用, 搜尋/連線/傳送資料等功能一應俱全, 寫完 peripheral 模式後,用它測試下,確認ok了,再來做central模式;
3. 參考資料
- BLE 官方文件
- android ble常見問題收集
- BLE開發的各種坑
- ble address動態變化
- wireshark bluetooth簡要描述
- Debugging Bluetooth With An Android App 介紹了款測試軟體,使用了,覺得不錯...
- Android BLE中傳輸資料的最大長度怎麼破 看完這篇才知道為啥單個分包20位元組,Android傳iOS單次最多可用512位元組....,注意:需要在裝置連線成功後再來設定,最大512,但是即使設定成功也沒法直接傳送,需要在回撥 onMtuChanged() 顯示成功後,再寫資料即可;
- Android BLE MTU調整
- 低功耗藍芽介紹 介紹了hci日誌中的 host / controller 含義,以及協議幀結構