Android Kotlin&BLE 低功耗藍(lán)牙筆記

  • BLE 與經(jīng)典藍(lán)牙的區(qū)別
  • BLE 的 Kotlin 下實(shí)踐
  • BluetoothGattCallback 不回調(diào)異常
  • 一些不常見的問(wèn)題和暴力解決的方法
  • 經(jīng)典藍(lán)牙自動(dòng)配對(duì),關(guān)閉系統(tǒng)配對(duì)彈窗

經(jīng)典藍(lán)牙(Classic Bluetooth)& 低功耗藍(lán)牙(Bluetooth Low Energy)


  • 經(jīng)典藍(lán)牙可以用與數(shù)據(jù)量比較大的傳輸,如語(yǔ)音,音樂,較高數(shù)據(jù)量傳輸?shù)取?/p>

  • BLE 特點(diǎn)就如其名,功耗更低的同時(shí),對(duì)數(shù)據(jù)包做出了限制。所以適用于實(shí)時(shí)性要求比較高,但是數(shù)據(jù)速率比較低的產(chǎn)品,如鼠標(biāo),鍵盤,傳感設(shè)備的數(shù)據(jù)發(fā)送等。

藍(lán)牙 4.0 支持單模和雙模兩種部署方式,其中單模即是我們說(shuō)的 BLE,而雙模指的是 Classic Bluetooth + BLE 。
實(shí)際上,BLE 和經(jīng)典藍(lán)牙的使用等各方面都像是沒有關(guān)聯(lián)的兩個(gè)東西,甚至因?yàn)?BLE 的通訊機(jī)制不同,所以是不能向下兼容的;經(jīng)典藍(lán)牙則可以兼容到藍(lán)牙 3.0 / 2.1。

BLE


同樣,有條件一定要去看官方文檔,然而這一次并沒有中文版,或許可以找一些國(guó)內(nèi)大佬們翻譯的版本。
還有就是大佬 JBD 寫的 Android BLE 藍(lán)牙開發(fā)入門 ,而且還用 RxJava 封裝成一個(gè)庫(kù)可以直接調(diào)用:RxBLE ,是真的厲害,不妨去學(xué)習(xí)學(xué)習(xí)。

  • 概念與常用 API

UUID:每個(gè)服務(wù)和特征都會(huì)有唯一的 UUID ,由硬件決定。
服務(wù)(Service):藍(lán)牙設(shè)備中可以定義多個(gè)服務(wù),相當(dāng)于功能的集合。
特征(Characteristic):一個(gè)服務(wù)可以包含多個(gè)特征,可以通過(guò) UUID 獲取到對(duì)應(yīng)的特征的實(shí)例,通過(guò)這個(gè)實(shí)例就可以向藍(lán)牙設(shè)備發(fā)送 / 讀取數(shù)據(jù)。

BluetoothDeivce:調(diào)用 startLeScan()獲取該實(shí)例,用于連接設(shè)備。
BluetoothManager:藍(lán)牙管理器,調(diào)用 getSystemService() 獲取,用于獲取藍(lán)牙適配器和管理所有和藍(lán)牙相關(guān)的東西。
BluetoothAdapter:藍(lán)牙適配器,通過(guò) BluetoothManager 獲取,用于打開藍(lán)牙、開始掃描設(shè)備等操作。
BluetoothGatt:通用屬性協(xié)議, 定義了BLE通訊的基本規(guī)則,就是通過(guò)把數(shù)據(jù)包裝成服務(wù)和特征的約定過(guò)程。
BluetoothGattCallback:一個(gè)回調(diào)類,非常重要而且會(huì)頻繁使用,用于回調(diào) GATT 通信的各種狀態(tài)和結(jié)果。
BluetoothGattService:服務(wù),通過(guò) BluetoothGatt 實(shí)例調(diào)用 getService(UUID) 獲取
BluetoothGattCharacteristic:特征,通過(guò) BluetoothGattService 實(shí)例調(diào)用 getCharacteristic(UUID) 獲取,是 GATT 通信中的最小數(shù)據(jù)單元。
BluetoothGattDescriptor:特征描述符,對(duì)特征的額外描述,包括但不僅限于特征的單位,屬性等。

  • 聲明權(quán)限
<uses-permission android:name="android.permission.BLUETOOTH"/> 
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 
<!-- Android 5.0 及以上需要添加 GPS 權(quán)限 -->
<uses-feature android:name="android.hardware.location.gps" />
<!-- Android 6.0 及以上需要添加定位權(quán)限 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  • 初始化
fun initBluetoothAdapter(){
    val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    val bluetoothAdapter = bluetoothManager.adapter
    //如果藍(lán)牙沒有打開則向用戶申請(qǐng)
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled)
        bluetoothAdapter.enable()
}
  • 掃描設(shè)備與停止掃描
var mDevice : BluetoothDevice ?= null
//掃描結(jié)果的回調(diào),開始掃描后會(huì)多次調(diào)用該方法
val mLeScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
    //通過(guò)對(duì)比設(shè)備的 mac 地址獲取需要的實(shí)例
    if(device.address == "50:F1:4A:A1:77:00"){
        mDevice = device
    }
}
//開始掃描之前判斷是否開啟了藍(lán)牙,enable 為 false 可以停止掃描
fun scanLeDeviceWithBLE(enable:Boolean = true){
    if (mBluetoothAdapter == null)
        initBluetoothAdapter()

    if (mBluetoothAdapter?.isEnabled as Boolean){
        mBluetoothAdapter?.enable()
    }
    if (enable){
        mScanning = true
        mBluetoothAdapter?.startLeScan(mLeScanCallback)
        TimeUtilWithoutKotlin.Delay(8,TimeUnit.SECONDS).setTodo {
            mBluetoothAdapter?.stopLeScan(mLeScanCallback)
            mScanning = false
        }
    }else {
        //停止掃描,在連接設(shè)備時(shí)最好調(diào)用 stopLeScan()
        mBluetoothAdapter?.stopLeScan(mLeScanCallback)
        mScanning = false
    }
}

其實(shí) startLeScan() 已經(jīng)被聲明為過(guò)時(shí),所以開始掃描可以以廣播的形式接受:

private fun startDiscover() {
    //這種方法需要注冊(cè)接收廣播,獲取掃描結(jié)果。
    val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
    bluetoothAdapter?.startDiscovery()
}
//注冊(cè)廣播,監(jiān)聽 BluetoothDevice.ACTION_FOUND 獲取掃描結(jié)果
private inner class DeviceReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            val device = intent
                    .getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
            Log.e("Service","device: ${device.address}")
        }
    }
}

或者使用新的 API:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // startScan(List<ScanFilter> filters, ScanSettings settings, final ScanCallback callback)
    // 過(guò)濾器 filters: new ScanFilter.Builder().setDeviceName(deviceName).build();
    // 掃描設(shè)置 settings: new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
    mBluetoothAdapter.bluetoothLeScanner.startScan(object : ScanCallback() {
        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            // 啟用了批量掃描模式后的回調(diào)
            super.onBatchScanResults(results)
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            // startScan() 失敗的回調(diào)
        }

        override fun onScanResult(callbackType: Int, result: ScanResult?) {
            super.onScanResult(callbackType, result)
            // 默認(rèn)的掃描方式的回調(diào)
        }
    } )
}
  • 連接藍(lán)牙設(shè)備
    此時(shí)已經(jīng)獲取到了藍(lán)牙設(shè)備的實(shí)例:mDevice,開始連接
fun connectWithBluetoothDevice(){
    if (null == mDevice){
        toast("can not find device")
        return
    }
    if(mScanning){
        //如果正在掃描設(shè)備,則停止掃描
        scanLeDeviceWithBLE(false)
    }
    mDevice?.connectGatt(this,false,mBluetoothGattCallback)
}

關(guān)于 connectGatt() 的幾個(gè)參數(shù):

public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                     BluetoothGattCallback callback)

第二個(gè)參數(shù),autoConnect 為 true 時(shí),如果設(shè)備斷開了連接將會(huì)不斷的嘗試連接。
第三個(gè) BluetoothGattCallback 是一個(gè)接受回調(diào)的對(duì)象,也是這一部分的重點(diǎn)。
先看一下完整的 BluetoothGattCallback:

val mBluetoothGattCallback = object :BluetoothGattCallback(){

    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        super.onConnectionStateChange(gatt, status, newState)
        if (newState == BluetoothProfile.STATE_CONNECTED){
            //開始搜索服務(wù)
            gatt?.discoverServices()
        }
        // 接受到設(shè)備斷開的狀態(tài)后,還要手動(dòng)調(diào)用 close(),否則可能出現(xiàn)連接未斷開導(dǎo)致的重連失敗等問(wèn)題;
        if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            gatt?.close()
        }
        if(newState == BluetoothProfile.STATE_CONNECTING){
            //設(shè)備在連接中
        }
    }

    //成功發(fā)現(xiàn)服務(wù)的回調(diào)
    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
        super.onServicesDiscovered(gatt, status)

        if (gatt == null) {
            return
        }

        //設(shè)置回調(diào),打開 Android 端接收通知的開關(guān),用 Descriptor 開啟通知的數(shù)據(jù)開關(guān)
        //這里的三個(gè) UUID 都是由硬件決定的
        val bluetoothGattService = gatt.getService(UUID_0)
        val characteristic = bluetoothGattService.getCharacteristic(UUID_1)
        val descriptor = characteristic.getDescriptor(UUID_2)
        if (descriptor == null) {
            gatt.disconnect()
            return
        }

        //打開 Android 端開關(guān)
        if (!gatt.setCharacteristicNotification(characteristic, true)) {
            //打開失敗
        }

        //假如寫入數(shù)據(jù)成功,則會(huì)回調(diào)下面的 onDescriptorWrite() 方法
        //所以在 onDescriptorWrite() 方法中向硬件寫入數(shù)據(jù)
        descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
        if (!gatt.writeDescriptor(descriptor)) {
            //寫入失敗
        }
      
    }

    //調(diào)用 writeDescriptor 的回調(diào)
    override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
        super.onDescriptorWrite(gatt, descriptor, status)

        val bluetoothGattService = gatt?.getService(UUID_SERVICE_CHANNEL)
        val characteristic = bluetoothGattService?.getCharacteristic(UUID_CHARACTERISTIC_CHANNEL)
        if (characteristic == null){
            //獲取特征失敗,直接斷開連接
            gatt?.disconnect()
            return
        }
        //mSendValue 即要往硬件發(fā)送的數(shù)據(jù)
        //如果這里寫入數(shù)據(jù)成功會(huì)回調(diào)下面的 onCharacteristicWrite() 方法
        characteristic.value = mSendValue
        if (!gatt.writeCharacteristic(characteristic)){
            //寫入數(shù)據(jù)失敗,斷開連接
            gatt.disconnect()
        }
    }

    //調(diào)用 writeCharacteristic 的回調(diào)
    override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
        super.onCharacteristicWrite(gatt, characteristic, status)

        val stringBuilder = StringBuilder()
        characteristic?.value
                ?.filter  { it > 0 }
                ?.forEach { stringBuilder.append(String.format("%c", it)) }
        //這時(shí)候 stringBuilder 應(yīng)該和上面 mSendValue 是一樣的
   }

    //硬件返回?cái)?shù)據(jù)的回調(diào),由于設(shè)置了回調(diào),所以當(dāng)硬件返回?cái)?shù)據(jù)時(shí)會(huì)調(diào)用這個(gè)方法
    override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
        super.onCharacteristicChanged(gatt, characteristic)

        val stringBuilder = StringBuilder()
        characteristic?.value?.forEach {
            val b = it
            hexStringBuilder.append(Integer.toHexString(b.toInt()))
            stringBuilder.append(String.format("%c",b))
        }
        runOnUiThread { toast("$stringBuilder") }
        //接受到數(shù)據(jù)之后就可以斷開連接了
        gatt?.disconnect()
    }
}

首先是 onConnectionStateChange(gatt,status,newState)
這個(gè)方法在成功連接、斷開連接等狀態(tài)改變的時(shí)候回調(diào),所以一開始會(huì)先進(jìn)入這個(gè)方法。
參數(shù)中, newState 代表當(dāng)前設(shè)備的連接的狀態(tài):

/** The profile is in disconnected state */
public static final int STATE_DISCONNECTED  = 0;
/** The profile is in connecting state */
public static final int STATE_CONNECTING    = 1;
/** The profile is in connected state */
public static final int STATE_CONNECTED     = 2;
/** The profile is in disconnecting state */
public static final int STATE_DISCONNECTING = 3;

所以當(dāng) newState 為 2 的時(shí)候就是剛連上設(shè)備的時(shí)候,這時(shí)候可以調(diào)用
gatt.discoverServices() 開始異步的查找藍(lán)牙服務(wù):

if (newState == BluetoothProfile.STATE_CONNECTED){
      //發(fā)現(xiàn)服務(wù)
      gatt?.discoverServices()
} 

執(zhí)行了discoverServices()后,若找到可用的服務(wù),系統(tǒng)又會(huì)回調(diào) mBluetoothGattCallback 里的onServicesDiscovered() 方法,所以添加:

override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {

    val bluetoothGattService = gatt?.getService(UUID_0)
    val characteristic = bluetoothGattService?.getCharacteristic(UUID_1)
    if (characteristic == null){
        //獲取特征的實(shí)例失敗,斷開連接
        gatt?.disconnect()
        return
    }
    //向硬件寫入數(shù)據(jù)
    characteristic.value = mSendValue
    if (!gatt.writeCharacteristic(characteristic)){
        //當(dāng)上面的方法返回 false 時(shí),寫入數(shù)據(jù)失敗
        gatt.disconnect()
    }
}

如果成功寫入數(shù)據(jù),系統(tǒng)回調(diào)mBluetoothGattCallbackonCharacteristicWrite()方法:

override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
    super.onCharacteristicWrite(gatt, characteristic, status)
    
    //這里遍歷 characteristic 中的 value,拼接在一起后成為一個(gè) stringBuilder
    //stringBuilder 應(yīng)該和發(fā)送給硬件的數(shù)據(jù)是一樣的
    val stringBuilder = StringBuilder()
    characteristic?.value
            ?.filter  { it > 0 }
            ?.forEach { stringBuilder.append(String.format("%c", it)) }

     //斷開連接,這一句最好延遲幾秒后執(zhí)行
     gatt?.disconnect()
}

上面的代碼可以成功往硬件發(fā)送數(shù)據(jù),但是不能接受硬件返回的數(shù)據(jù)。
如果想要接受硬件返回的數(shù)據(jù),需要在 onServicesDiscovered(),也就是連上服務(wù)后,先不發(fā)送數(shù)據(jù)而是設(shè)置硬件返回?cái)?shù)據(jù)的開關(guān):

//設(shè)置回調(diào):打開 Android 端接收通知的開關(guān);并且向 Descriptor 寫入數(shù)據(jù)來(lái)開啟通知
val bluetoothGattService = gatt.getService(UUID_SERVICE_CHANNEL)
val characteristic = bluetoothGattService.getCharacteristic(UUID_CHARACTERISTIC_CHANNEL)
val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR_UUID)
val descriptors = characteristic.descriptors
if (descriptors == null) {
    //獲取特征描述符失敗,斷開連接
    gatt.disconnect()
    return
}
//打開 Android 端開關(guān)
if (!gatt.setCharacteristicNotification(characteristic, true)) {
    //失敗的處理
}
//向硬件寫入一些數(shù)據(jù),打開硬件返回?cái)?shù)據(jù)的開關(guān)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (!gatt.writeDescriptor(descriptor)) {
    //寫入數(shù)據(jù)失敗
}
  • 實(shí)際上向硬件寫入數(shù)據(jù)這一段代碼有時(shí)候是可以省略的,只需要打開 Android 段的開關(guān)即可接收到返回的數(shù)據(jù),可能是和硬件有關(guān)。這樣一來(lái),就不能繼續(xù)在 onServicesDiscovered() 執(zhí)行寫入數(shù)據(jù)的代碼,改為在 onDescriptorWrite() 中執(zhí)行。

還有就是用 Kotlin 寫的 MainActivity 部分:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        verticalLayout {
            gravity = CENTER
            linearLayout {
                orientation = LinearLayout.VERTICAL
                button("搜索設(shè)備"){
                    setOnClickListener {
                        mBinder?.startScanLeDevice()
                    }
                }.lparams(width = matchParent,height = wrapContent){
                    padding = dip(5)
                    margin = dip(10)
                }

                button("發(fā)送開鎖指令"){
                    padding = dip(10)
                    setOnClickListener{
                        mBinder?.connect()
                    }
                }.lparams(width = matchParent,height = wrapContent){
                    padding = dip(5)
                    margin = dip(10)
                }
            }

        }
        val intent = Intent(this, BluetoothService::class.java)
        bindService(intent,mConnect,Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        super.onDestroy()
        mDisposable?.dispose()
    }

    var mBinder : BluetoothService.MBinder ?= null
    var mDisposable : Disposable ?= null
    val mConnect = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            mBinder = service as BluetoothService.MBinder
        }
        override fun onServiceDisconnected(name: ComponentName) {

        }
    }
}

BLE 相關(guān)的代碼是寫在了 Service 中,通過(guò)綁定時(shí)返回的 mBinder 來(lái)調(diào)用 Service 中的方法。

connectGatt() 不觸發(fā)回調(diào) (BluetoothGattCallback) 的異常


  • 在實(shí)際的操作過(guò)程中遇到過(guò)一些藍(lán)牙設(shè)備,在調(diào)用了 connectGatt 會(huì)無(wú)響應(yīng),既不回調(diào) callback,也不拋出異常;
    后續(xù)的 debug 中偶然發(fā)現(xiàn) 6.0 及以上的 connectGatt() 新增了可選參數(shù) transport,支持設(shè)置連接設(shè)備的傳輸模式的;
@param transport preferred transport for GATT connections to remote dual-mode devices
     *             {@link BluetoothDevice#TRANSPORT_AUTO} or
     *             {@link BluetoothDevice#TRANSPORT_BREDR} or {@link BluetoothDevice#TRANSPORT_LE}

這一參數(shù)在 5.0 及 5.1 中是無(wú)法直接設(shè)置的,通過(guò)反射調(diào)用后解決了以上出現(xiàn)的無(wú)回調(diào)、無(wú)響應(yīng)問(wèn)題:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    device.connectGatt(mActivity, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE)
} else {
    val d = mBluetoothAdapter.getRemoteDevice(device.address)
    val creMethod = d.javaClass.getDeclaredMethod("connectGatt",
            Context::class.java, Boolean::class.javaPrimitiveType,
            BluetoothGattCallback::class.java, Int::class.javaPrimitiveType)
    creMethod.isAccessible = true
    val transport = d.javaClass.getDeclaredField("TRANSPORT_LE").getInt(null)

    mBluetoothAdapter.getRemoteDevice(d.address)
    val res = creMethod.invoke(d, mActivity, true, bluetoothGattCallback, transport) as BluetoothGatt
}

連接未徹底斷開


首先遇到的問(wèn)題是 gatt.disconnect 無(wú)法徹底斷開設(shè)備的連接,藍(lán)牙設(shè)備的狀態(tài)為已連接,但 gatt.getConnectionState() 的狀態(tài)缺為已斷開;

參考了一下各路方法,通過(guò)反射 BluetoothDevice 的內(nèi)部類判斷是否連接

public static final int CONNECTION_STATE_DISCONNECTED = 0;
public static final int CONNECTION_STATE_CONNECTED = 1;
public static final int CONNECTION_STATE_UN_SUPPORT = -1;
    
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @SuppressLint("PrivateApi")
    public static int getInternalConnectionState(String mac) {
        //該功能是在21 (5.1.0)以上才支持, 5.0 以及以下 都 不支持
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return CONNECTION_STATE_UN_SUPPORT;
        }
        if(Build.MANUFACTURER.equalsIgnoreCase("OPPO")){//OPPO勿使用這種辦法判斷, OPPO無(wú)解
            return CONNECTION_STATE_UN_SUPPORT;
        }
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        BluetoothDevice remoteDevice = adapter.getRemoteDevice(mac);
        Object mIBluetooth = null;
        try {
            Field sService = BluetoothDevice.class.getDeclaredField("sService");
            sService.setAccessible(true);
            mIBluetooth = sService.get(null);
        } catch (Exception e) {
            return CONNECTION_STATE_UN_SUPPORT;
        }
        if (mIBluetooth == null) return CONNECTION_STATE_UN_SUPPORT;

        boolean isConnected;
        try {
            Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected");
            isConnectedMethod.setAccessible(true);
            isConnected = (Boolean) isConnectedMethod.invoke(remoteDevice);
            isConnectedMethod.setAccessible(false);
        } catch (Exception e) {
        //如果找不到,說(shuō)明不兼容isConnected, 嘗試去使用getConnectionState 判斷
            try {
                Method getConnectionState = mIBluetooth.getClass().getDeclaredMethod("getConnectionState", BluetoothDevice.class);
                getConnectionState.setAccessible(true);
                int state = (Integer) getConnectionState.invoke(mIBluetooth, remoteDevice);
                getConnectionState.setAccessible(false);
                isConnected = state == CONNECTION_STATE_CONNECTED;
            } catch (Exception e1) {
                return CONNECTION_STATE_UN_SUPPORT;
            }
        }
        return isConnected ? CONNECTION_STATE_CONNECTED : CONNECTION_STATE_DISCONNECTED;

    }

BluetoothGattCallback 各回調(diào)中不可有耗時(shí)操作,否則會(huì)影響下一個(gè)回調(diào)的執(zhí)行。


讀寫問(wèn)題


  • 首先 BLE 的所有操作都是通信的結(jié)果,所以基本都是異步操作,在頻繁讀 / 寫的過(guò)程中不免要等待上一次結(jié)束再繼續(xù)讀 / 寫;

  • Android BLE 默認(rèn)單次傳輸數(shù)據(jù)包最大為 20 字節(jié),在實(shí)際場(chǎng)景中明顯不夠,一般有兩種思路:

1.設(shè)置 MTU 修改單次傳輸包大小上限: gatt.requestMtu(),但不同設(shè)備有失敗的可能;

2.分包傳輸,注意連接寫入時(shí)的間隔問(wèn)題,實(shí)際上設(shè)備是每隔一定時(shí)間去讀取一次特征值來(lái)獲取寫入的數(shù)據(jù),BLE 默認(rèn)這個(gè)時(shí)間間隔為 7.5ms (與設(shè)備相關(guān)),如果寫入的時(shí)間間隔小于這個(gè)讀取間隔則會(huì)導(dǎo)致丟包。
因此可以在寫入成功回調(diào)后 (onCharacteristicWrite()) 再繼續(xù)下一個(gè)寫入,或者粗暴的加一個(gè)時(shí)間間隔,考慮不同設(shè)備的差異 200ms 一般足夠穩(wěn)妥。

3.對(duì)于 (2) 中提到的讀 / 寫的時(shí)間間隔,其實(shí)可以通過(guò) requestConnectionPriority() 來(lái)修改,參數(shù)為:
BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER
BluetoothGatt#CONNECTION_PRIORITY_BALANCED
BluetoothGatt#CONNECTION_PRIORITY_HIGH
分別對(duì)于低功耗、中等、高功耗三種模式。

經(jīng)典藍(lán)牙


參考官方文檔(基礎(chǔ)的應(yīng)用基本上看這一篇文檔就可以了。

其中有些細(xì)節(jié),經(jīng)典藍(lán)牙連接設(shè)備是需要配對(duì)的,而很多藍(lán)牙設(shè)備采用了默認(rèn)的 pin 碼:0000 或 1234 等。
這里就存在優(yōu)化空間,我們可以通過(guò)代碼去設(shè)置并關(guān)閉系統(tǒng)彈出的配對(duì)窗口 (這里的實(shí)踐基于 Android 5.1);

fun connect(device: BluetoothDevice) {
    var isBond = false
    try {
        //檢查是否處于未配對(duì)狀態(tài)
        if (device.bondState == BluetoothDevice.BOND_NONE) {
            // 監(jiān)聽配對(duì)彈窗的廣播
            activity.registerReceiver(object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    // 關(guān)閉配對(duì)彈窗
                    abortBroadcast()
                    // 設(shè)置 Pin 碼, 默認(rèn)為 0000
                    val removeBondMethod = device.javaClass.getDeclaredMethod("setPin", ByteArray::class.java)
                    removeBondMethod.invoke(device, "0000".toByteArray())
                    activity.unregisterReceiver(this)
                }
            }, IntentFilter().apply { addAction("android.bluetooth.device.action.PAIRING_REQUEST") })

            // 開始配對(duì)
            val creMethod = device.javaClass.getMethod("createBond")
            isBond = creMethod.invoke(device) as Boolean
        } else {
            isBond = true
        }
    } catch (e: Exception) {
        e.printStackTrace()
        // onConnectError("連接設(shè)備失敗, 請(qǐng)手動(dòng)配對(duì)藍(lán)牙;", 272)
    }
    if (!isBond) {
        // onConnectError("連接設(shè)備失敗, 請(qǐng)手動(dòng)配對(duì)藍(lán)牙;", 275)
        return
    }

    try {
        tmp.connect()
        // tmpIn = tmp.getInputStream()
        // tmpOut = tmp.getOutputStream()
        // mSocket = tmp
        // 連接成功
    } catch (e: IOException) {
        // onConnectError("連接設(shè)備失敗: 287, 請(qǐng)重試;", 287)
        e.printStackTrace()
        tmp.close()
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評(píng)論 1 283
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,608評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容