這里不記錄具體代碼規(guī)則,后面會(huì)給出參考文章,別人已經(jīng)寫(xiě)很詳細(xì)了,我就單純記錄下踩過(guò)的坑吧;
1. 版本支持
Android 從 4.3(API Level 18) 開(kāi)始支持低功耗藍(lán)牙(Bluetooth low energy),但是只支持作為中心設(shè)備(Central)模式,這就意味著 Android 設(shè)備只能主動(dòng)掃描和鏈接其他外圍設(shè)備(Peripheral),從 Android 5.0(API Level 21) 開(kāi)始兩種模式都支持。
P.S. 不過(guò)也不是5.0以上就全部都支持,之前測(cè)試到魅族M2貌似就開(kāi)不起peripheral模式,畢竟硬件相關(guān),很難保證,我同事之前開(kāi)發(fā)時(shí)候甚至碰到過(guò)某些設(shè)備會(huì)固定少發(fā)一個(gè)字節(jié),也是坑啊...
2. 踩過(guò)的坑
2.1 開(kāi)啟peripheral模式
之前以為開(kāi)啟了手機(jī)藍(lán)牙和gps功能, 手機(jī)就能被central設(shè)備搜索到, 那是經(jīng)典藍(lán)牙, 要想啟用BLE功能并作為peripheral從機(jī),需要使用 BluetoothLeAdvertiser
開(kāi)啟廣播模式:
P.S. BLE鏈接不會(huì)彈出連接請(qǐng)求,比經(jīng)典藍(lán)牙方便,畢竟不打擾用戶,另外,查到的資料說(shuō),BLE central大概最多同時(shí)鏈接7臺(tái)設(shè)備左右;
/**
* 開(kāi)啟廣播模式,用于本機(jī)被其他central設(shè)備搜索到
*/
@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 藍(lán)牙地址動(dòng)態(tài)變化
參考這篇
Google在Android6.0上修改了獲取設(shè)備標(biāo)識(shí)信息功能:
// 以下方法固定返回: 02:00:00:00:00:00
WifiInfo.getMacAddress()
BluetoothAdapter.getAddress()
坑爹的是,假設(shè)central設(shè)備掃描得到peripheral的藍(lán)牙地址記為: A , 連接同一臺(tái)peripheral設(shè)備時(shí)獲取的藍(lán)牙地址記為B, A跟B還不一致,又動(dòng)態(tài)變化了,真是坑啊:
之所以會(huì)想要記錄設(shè)備藍(lán)牙地址,是想作為唯一標(biāo)識(shí)符,在轉(zhuǎn)傳信息時(shí),不要再回傳到數(shù)據(jù)來(lái)源方, 比如 A 發(fā)送數(shù)據(jù)給 B, B再往其他設(shè)備轉(zhuǎn)傳時(shí),就不需要回傳給A了,但是地址動(dòng)態(tài)變化的話,我就沒(méi)轍了,有解決方案的話麻煩告知我一下;
// 低功耗藍(lán)牙掃描回調(diào)
var mLeScanCallback: ScanCallback? = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
// Logger.d("scan successful $result")
// 這里通過(guò)ScanResult獲取到的藍(lán)牙地址A,跟通過(guò)手機(jī)系統(tǒng)設(shè)置頁(yè)面查看得到的藍(lán)牙地址是不同的,而且每次重新開(kāi)啟peripheral模式后,同一臺(tái)手機(jī)的藍(lán)牙地址就又變化了
//
// 另外,同一臺(tái)設(shè)備會(huì)在短時(shí)間內(nèi)被掃描到很多次,因此不是需要對(duì)設(shè)備進(jìn)行過(guò)濾判斷
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)隨便定義即可, 后來(lái)看了 這篇 才發(fā)現(xiàn)不是這樣的,能自定義的只是其中一部分,有興趣的可以去研究下 BLE文檔;
0000????-0000-1000-8000-00805f9b34fb
????就表示4個(gè)可以自定義16進(jìn)制數(shù)
2.4 跟iOS通訊時(shí)循環(huán)寫(xiě)入數(shù)據(jù)失敗
我們是通過(guò) Characteristic
來(lái)寫(xiě)入的, 它有個(gè)屬性來(lái)指明發(fā)送時(shí)不需要響應(yīng): BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
, 而我在跟iOS交互時(shí),貌似這個(gè)字段雙方設(shè)定不一致,導(dǎo)致發(fā)送后一直沒(méi)收到響應(yīng),然后iOS就一直重發(fā);
因此,需要在作為peripheral模式時(shí),添加的characteristic需要設(shè)置為: BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
;
另外,作為central設(shè)備往其他設(shè)備發(fā)送消息時(shí),也需要添加該屬性:
- Android和iOS使用同一套BLE協(xié)議,因此可以通訊,如果是wifi direct的話,就不行了;
- Android 4.3雖然也支持central模式,但是查到的文章有說(shuō)在跟iOS參數(shù)交互時(shí)有問(wèn)題,而我使用4.3來(lái)搜索其他Android設(shè)備也經(jīng)常找不到,因此就直接不考慮了,從5.0開(kāi)始;
/**
* 接收數(shù)據(jù)時(shí),通過(guò)本類回調(diào)處理
*/
class GattServerCallBack : BluetoothGattServerCallback() {
companion object {
private val TAG = "GattServerCallBack"
}
private var mGattServer: BluetoothGattServer? = null
/**
* 初始化需要用來(lái)轉(zhuǎn)傳數(shù)據(jù)的 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, // 這里設(shè)定不需要回應(yīng),也可選擇需要響應(yīng)模式
BluetoothGattCharacteristic.PERMISSION_READ
or BluetoothGattCharacteristic.PERMISSION_WRITE)// 可寫(xiě)模式,不同ble設(shè)備間通過(guò)本characteristic來(lái)傳輸數(shù)據(jù)
characteristic.setValue(BlePara.adCharacteristicValue)
val addCharacteristic = service.addCharacteristic(characteristic)
Logger.d("addCharacteristic result = $addCharacteristic", TAG)
service
}
/**
* 廣播開(kāi)始后,設(shè)置一個(gè)用于接收消息的service
* 后續(xù)有數(shù)據(jù)傳入時(shí),會(huì)觸發(fā) [org.lynxz.ble_lib.callbacks.GattServerCallBack.onCharacteristicWriteRequest]
* */
fun setupServices(gattServer: BluetoothGattServer?): Boolean {
if (gattServer == null) {
return false
}
// 設(shè)置一個(gè)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已存在,不用重復(fù)添加", 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)
// 按需發(fā)送響應(yīng)
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設(shè)備,通過(guò)characteristic發(fā)送數(shù)據(jù)時(shí)
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 發(fā)送超過(guò)20字節(jié)數(shù)據(jù)
擴(kuò)展閱讀
BLE默認(rèn)單次傳輸長(zhǎng)度為20字節(jié), 對(duì)于超過(guò)該長(zhǎng)度的數(shù)據(jù),有兩種方式進(jìn)行處理:
- 修改MTU值(最大為512字節(jié))
在跟iOS交互的時(shí)候,發(fā)現(xiàn)它一次性可以往Android發(fā)送512字節(jié)(Android使用默認(rèn)設(shè)定),后來(lái)才發(fā)現(xiàn)Android設(shè)備間也可以重新指定該值,不過(guò)使用這種方式的話,我測(cè)試到有這種現(xiàn)象: mtu設(shè)置回調(diào)成功,central設(shè)備發(fā)送數(shù)據(jù)也成功,但peripheral設(shè)備卻不能完整接收到,比如我設(shè)置512字節(jié),但收到的可能只有140字節(jié),因此我沒(méi)有采用這種方式:
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("設(shè)置mtu結(jié)果 : ${gatt.requestMtu(BlePara.mtu)}"
} else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
gatt.close()
}
}
// mtu設(shè)置成功后才去搜索service/characteristic,然后才可以傳輸數(shù)據(jù)
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();
}
}
}
-
對(duì)數(shù)據(jù)進(jìn)行分包操作,添加控制信息
分為三部分,每個(gè)分包固定20字節(jié):
a. head包,包含一些控制信息,如傳送的數(shù)據(jù)長(zhǎng)度,用于整合數(shù)據(jù)包
b. 用戶要傳送的數(shù)據(jù)內(nèi)容(可加密);
c. tail包,所有數(shù)據(jù)發(fā)送完成后,發(fā)送一個(gè)結(jié)束信息(主要是避免head包發(fā)送失敗時(shí),接收方一直在等待發(fā)送結(jié)束,當(dāng)然,若是tail包也發(fā)送失敗,則需要通過(guò)接收超時(shí)機(jī)制來(lái)控制)
P.S. 跟iOS的同學(xué)交流后發(fā)現(xiàn),iOS設(shè)備間單次最大也只是能發(fā)送512字節(jié),因此應(yīng)該也有分包的需求;
2.6 分包發(fā)送時(shí)間間隔過(guò)長(zhǎng)的問(wèn)題
stack overflow
連續(xù)通過(guò)characteristic寫(xiě)入數(shù)據(jù)時(shí),相鄰分包之間需要間隔一下,之前測(cè)試發(fā)現(xiàn)100ms失敗率比較大,200ms就比較ok,但是也有一定概率失敗,而且,單包20字節(jié)
,我要傳輸?shù)臄?shù)據(jù)基本都要400字節(jié)左右,總耗時(shí)(包括連接等)就可能達(dá)到5s以上,感覺(jué)時(shí)間還是太長(zhǎng),兩種方式來(lái)避免:
- 修改
requestConnectionPriority()
值為BluetoothGatt.CONNECTION_PRIORITY_HIGH
這樣設(shè)定后,分包之間設(shè)置為20ms就沒(méi)再發(fā)現(xiàn)有出問(wèn)題過(guò)(至少我手頭的機(jī)型沒(méi)出錯(cuò)過(guò))
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}")
// 發(fā)送大數(shù)據(jù)時(shí)設(shè)置如此,有人建議發(fā)送完成后要設(shè)置成默認(rèn)的: CONNECTION_PRIORITY_BALANCED
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// REFACTOR: 17/06/2017 可以設(shè)置mtu大小,若啟用此方式,則請(qǐng)?jiān)趏nMtuChanged()回調(diào)成功后再搜索及發(fā)送數(shù)據(jù),但Android之間測(cè)試發(fā)現(xiàn)接收方有些只能收到152個(gè)字節(jié),暫時(shí)不考慮,后續(xù)研究
// Logger.d("設(shè)置mtu結(jié)果 : ${gatt.requestMtu(BlePara.mtu)}"
// 連接成功,開(kāi)始搜索service
gatt.discoverServices()
} else if (BluetoothGatt.STATE_DISCONNECTED == newState) {
// gatt連接斷開(kāi)
Logger.d("onConnectionStateChange STATE_DISCONNECTED = $newState")
gatt.close()
}
}
}
- 添加錯(cuò)誤重傳機(jī)制,重傳時(shí)間間隔增加
發(fā)送分包時(shí)不可避免可能出錯(cuò),若默認(rèn)分包間隔為20ms,發(fā)送失敗后,可嘗試重傳一次,重傳時(shí)的時(shí)間間隔略微設(shè)定大些,如200ms,這樣仍能有效減小總發(fā)送時(shí)間;
var result = true // 發(fā)送數(shù)據(jù)是否成功
val delay = 20 // 分包之間的延時(shí),單位:毫秒
try {
// 注意,這里需要延時(shí)一下,不然測(cè)試發(fā)現(xiàn),基本上只能收到其中幾幀的數(shù)據(jù),失敗的概率比較大
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 塊數(shù)據(jù)的結(jié)果: $sliceResult", TAG)
// 發(fā)送失敗時(shí),嘗試重傳一次就好
if (!sliceResult) {
Thread.sleep(200)
sliceResult = gatt.writeCharacteristic(relayChar)
Logger.d(" =>重傳第 $i ~ $to 塊數(shù)據(jù)的結(jié)果: $sliceResult", TAG)
}
result = result and sliceResult
i = to
Thread.sleep(delay.toLong())
// 由于只重傳一次, 因此如果某個(gè)數(shù)據(jù)分包重傳失敗,則不必要再傳后續(xù)數(shù)據(jù),直接返回失敗
if (!result) {
break
}
}
} catch (e: Exception) {
e.printStackTrace()
result = false
}
2.7 藍(lán)牙抓包,日志查看
之前跟iOS交互出錯(cuò)后,app層回調(diào)可看到的信息比較少, 查到的資料 又都說(shuō)有某個(gè)控制參數(shù)出錯(cuò), 沒(méi)發(fā)現(xiàn)characteristic設(shè)置有問(wèn)題前,就想著要抓包看看具體的參數(shù)交互, 未找到實(shí)時(shí)抓包的簡(jiǎn)單方法, 倒是可以通過(guò)Android手機(jī)的hcidump功能來(lái)獲取日志,然后通過(guò) wireshark 來(lái)查看:
- 查看hci日志文件路徑
// 我使用nexus 6p 7.1.1系統(tǒng),配置文件位于如下位置:
adb shell cat /etc/bluetooth/bt_stack.conf
// 文件中有一條配置信息,指示了log文件所在路徑
BtSnoopFileName=/sdcard/btsnoop_hci.log
- 抓取/導(dǎo)出hci日志
// 先清除原先的日志
adb shell rm /sdcard/btsnoop_hci.log
// 通過(guò)手機(jī)系統(tǒng)打開(kāi)日志功能: settings-developer options -- enable bluetooth hci snoop log
// 抓取結(jié)束后,導(dǎo)出log文件到pc上
adb pull /sdcard/btsnoop_hci.log
不過(guò), 一開(kāi)始做ble沒(méi)經(jīng)驗(yàn),可以先下載些軟件來(lái)測(cè)試下ble功能,這里推薦一個(gè) nRF24L01 , 具體請(qǐng)參考 這篇文章, 好用, 搜索/連接/發(fā)送數(shù)據(jù)等功能一應(yīng)俱全, 寫(xiě)完 peripheral 模式后,用它測(cè)試下,確認(rèn)ok了,再來(lái)做central模式;
3. 參考資料
- BLE 官方文檔
- android ble常見(jiàn)問(wèn)題收集
- BLE開(kāi)發(fā)的各種坑
- ble address動(dòng)態(tài)變化
- wireshark bluetooth簡(jiǎn)要描述
-
Debugging Bluetooth With An Android App
介紹了款測(cè)試軟件,使用了,覺(jué)得不錯(cuò)... -
Android BLE中傳輸數(shù)據(jù)的最大長(zhǎng)度怎么破
看完這篇才知道為啥單個(gè)分包20字節(jié),Android傳iOS單次最多可用512字節(jié)....,注意:需要在設(shè)備連接成功后再來(lái)設(shè)置,最大512,但是即使設(shè)置成功也沒(méi)法直接發(fā)送,需要在回調(diào) onMtuChanged() 顯示成功后,再寫(xiě)數(shù)據(jù)即可; - Android BLE MTU調(diào)整
-
低功耗藍(lán)牙介紹
介紹了hci日志中的 host / controller 含義,以及協(xié)議幀結(jié)構(gòu)