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

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

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


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

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

藍(lán)牙 4.0 支持單模和雙模兩種部署方式棺禾,其中單模即是我們說的 BLE,而雙模指的是 Classic Bluetooth + BLE 悍手。
實際上帘睦,BLE 和經(jīng)典藍(lán)牙的使用等各方面都像是沒有關(guān)聯(lián)的兩個東西袍患,甚至因為 BLE 的通訊機(jī)制不同坦康,所以是不能向下兼容的;經(jīng)典藍(lán)牙則可以兼容到藍(lán)牙 3.0 / 2.1诡延。

BLE


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

  • 概念與常用 API

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

BluetoothDeivce:調(diào)用 startLeScan()獲取該實例恳不,用于連接設(shè)備。
BluetoothManager:藍(lán)牙管理器开呐,調(diào)用 getSystemService() 獲取烟勋,用于獲取藍(lán)牙適配器和管理所有和藍(lán)牙相關(guān)的東西规求。
BluetoothAdapter:藍(lán)牙適配器,通過 BluetoothManager 獲取卵惦,用于打開藍(lán)牙颓哮、開始掃描設(shè)備等操作。
BluetoothGatt:通用屬性協(xié)議鸵荠, 定義了BLE通訊的基本規(guī)則冕茅,就是通過把數(shù)據(jù)包裝成服務(wù)和特征的約定過程。
BluetoothGattCallback:一個回調(diào)類蛹找,非常重要而且會頻繁使用姨伤,用于回調(diào) GATT 通信的各種狀態(tài)和結(jié)果。
BluetoothGattService:服務(wù)庸疾,通過 BluetoothGatt 實例調(diào)用 getService(UUID) 獲取乍楚。
BluetoothGattCharacteristic:特征团赁,通過 BluetoothGattService 實例調(diào)用 getCharacteristic(UUID) 獲取栗柒,是 GATT 通信中的最小數(shù)據(jù)單元。
BluetoothGattDescriptor:特征描述符趣些,對特征的額外描述金顿,包括但不僅限于特征的單位臊泌,屬性等。

  • 聲明權(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)牙沒有打開則向用戶申請
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled)
        bluetoothAdapter.enable()
}
  • 掃描設(shè)備與停止掃描
var mDevice : BluetoothDevice ?= null
//掃描結(jié)果的回調(diào)揍拆,開始掃描后會多次調(diào)用該方法
val mLeScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
    //通過對比設(shè)備的 mac 地址獲取需要的實例
    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è)備時最好調(diào)用 stopLeScan()
        mBluetoothAdapter?.stopLeScan(mLeScanCallback)
        mScanning = false
    }
}

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

private fun startDiscover() {
    //這種方法需要注冊接收廣播播揪,獲取掃描結(jié)果。
    val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
    bluetoothAdapter?.startDiscovery()
}
//注冊廣播筒狠,監(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)
    // 過濾器 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è)備
    此時已經(jīng)獲取到了藍(lán)牙設(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() 的幾個參數(shù):

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

第二個參數(shù)辩恼,autoConnect 為 true 時雇庙,如果設(shè)備斷開了連接將會不斷的嘗試連接。
第三個 BluetoothGattCallback 是一個接受回調(diào)的對象运挫,也是這一部分的重點(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)后,還要手動調(diào)用 close()谁帕,否則可能出現(xiàn)連接未斷開導(dǎo)致的重連失敗等問題;
        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)
        //這里的三個 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ù)成功,則會回調(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ù)成功會回調(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)) }
        //這時候 stringBuilder 應(yīng)該和上面 mSendValue 是一樣的
   }

    //硬件返回數(shù)據(jù)的回調(diào),由于設(shè)置了回調(diào),所以當(dāng)硬件返回數(shù)據(jù)時會調(diào)用這個方法
    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)峡继,
這個方法在成功連接、斷開連接等狀態(tài)改變的時候回調(diào)匈挖,所以一開始會先進(jìn)入這個方法碾牌。
參數(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è)備的時候,這時候可以調(diào)用
gatt.discoverServices() 開始異步的查找藍(lán)牙服務(wù):

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

執(zhí)行了discoverServices()后舶吗,若找到可用的服務(wù)征冷,系統(tǒng)又會回調(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){
        //獲取特征的實例失敗誓琼,斷開連接
        gatt?.disconnect()
        return
    }
    //向硬件寫入數(shù)據(jù)
    characteristic.value = mSendValue
    if (!gatt.writeCharacteristic(characteristic)){
        //當(dāng)上面的方法返回 false 時检激,寫入數(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腹侣,拼接在一起后成為一個 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è)置硬件返回數(shù)據(jù)的開關(guān):

//設(shè)置回調(diào):打開 Android 端接收通知的開關(guān);并且向 Descriptor 寫入數(shù)據(jù)來開啟通知
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ù)复濒,打開硬件返回數(shù)據(jù)的開關(guān)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (!gatt.writeDescriptor(descriptor)) {
    //寫入數(shù)據(jù)失敗
}
  • 實際上向硬件寫入數(shù)據(jù)這一段代碼有時候是可以省略的,只需要打開 Android 段的開關(guān)即可接收到返回的數(shù)據(jù)乒省,可能是和硬件有關(guān)巧颈。這樣一來,就不能繼續(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 中馋劈,通過綁定時返回的 mBinder 來調(diào)用 Service 中的方法攻锰。

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


  • 在實際的操作過程中遇到過一些藍(lán)牙設(shè)備,在調(diào)用了 connectGatt 會無響應(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 中是無法直接設(shè)置的,通過反射調(diào)用后解決了以上出現(xiàn)的無回調(diào)楷拳、無響應(yīng)問題:

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
}

連接未徹底斷開


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

參考了一下各路方法陶耍,通過反射 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無解
            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) {
        //如果找不到,說明不兼容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)中不可有耗時操作,否則會影響下一個回調(diào)的執(zhí)行她混。


讀寫問題


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

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

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

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

3.對于 (2) 中提到的讀 / 寫的時間間隔压昼,其實可以通過 requestConnectionPriority() 來修改求冷,參數(shù)為:
BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER
BluetoothGatt#CONNECTION_PRIORITY_BALANCED
BluetoothGatt#CONNECTION_PRIORITY_HIGH
分別對于低功耗、中等窍霞、高功耗三種模式匠题。

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


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

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

fun connect(device: BluetoothDevice) {
    var isBond = false
    try {
        //檢查是否處于未配對狀態(tài)
        if (device.bondState == BluetoothDevice.BOND_NONE) {
            // 監(jiān)聽配對彈窗的廣播
            activity.registerReceiver(object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    // 關(guān)閉配對彈窗
                    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") })

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

    try {
        tmp.connect()
        // tmpIn = tmp.getInputStream()
        // tmpOut = tmp.getOutputStream()
        // mSocket = tmp
        // 連接成功
    } catch (e: IOException) {
        // onConnectError("連接設(shè)備失敗: 287, 請重試;", 287)
        e.printStackTrace()
        tmp.close()
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钱磅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子似枕,更是在濱河造成了極大的恐慌盖淡,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凿歼,死亡現(xiàn)場離奇詭異褪迟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)答憔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門味赃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人虐拓,你說我怎么就攤上這事心俗。” “怎么了侯嘀?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵另凌,是天一觀的道長谱轨。 經(jīng)常有香客問我,道長吠谢,這世上最難降的妖魔是什么土童? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮工坊,結(jié)果婚禮上献汗,老公的妹妹穿的比我還像新娘。我一直安慰自己王污,他們只是感情好罢吃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著昭齐,像睡著了一般尿招。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上阱驾,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天就谜,我揣著相機(jī)與錄音,去河邊找鬼里覆。 笑死丧荐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喧枷。 我是一名探鬼主播虹统,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼隧甚!你這毒婦竟也來了车荔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤呻逆,失蹤者是張志新(化名)和其女友劉穎夸赫,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咖城,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年呼奢,在試婚紗的時候發(fā)現(xiàn)自己被綠了宜雀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡握础,死狀恐怖辐董,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情禀综,我是刑警寧澤简烘,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布苔严,位于F島的核電站,受9級特大地震影響孤澎,放射性物質(zhì)發(fā)生泄漏届氢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一覆旭、第九天 我趴在偏房一處隱蔽的房頂上張望退子。 院中可真熱鬧,春花似錦型将、人聲如沸寂祥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丸凭。三九已至,卻和暖如春腕铸,著一層夾襖步出監(jiān)牢的瞬間贮乳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工恬惯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留向拆,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓酪耳,卻偏偏與公主長得像浓恳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子碗暗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

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