Android 低功耗藍(lán)牙(Ble) 開(kāi)發(fā)總結(jié)

Android 從 4.3(API Level 18) 開(kāi)始支持低功耗藍(lán)牙,但是只支持作為中心設(shè)備(Central)模式,這就意味著 Android 設(shè)備只能主動(dòng)掃描和鏈接其他外圍設(shè)備(Peripheral)。從 Android 5.0(API Level 21) 開(kāi)始兩種模式都支持。

低功耗藍(lán)牙開(kāi)發(fā)算是較偏技術(shù),實(shí)際開(kāi)發(fā)中坑是比較多的,網(wǎng)上有很多文章介紹使用和經(jīng)驗(yàn)總結(jié),但是有些問(wèn)題答案不好找,甚至有些誤導(dǎo)人,比如 :獲取已經(jīng)連接的藍(lán)牙,有的是通過(guò)反射,一大堆判斷,然而并不是對(duì)所有手機(jī)有用,關(guān)于Ble傳輸速率問(wèn)題的解決,都是默認(rèn)Android每次只能發(fā)送20個(gè)字節(jié),然而也并不是,,,下面進(jìn)入正文。

Ble的使用 官網(wǎng)

1. 權(quán)限

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--Android 6.0及后續(xù)版本掃描藍(lán)牙,需要定位權(quán)限(進(jìn)入GPS設(shè)置,可以看到藍(lán)牙定位)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />//9.0以上
<uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="false" />
<!--不支持低功耗的設(shè)備也可以安裝,否則可以設(shè)置 required ="true"-->

2. 初始 使用 (在掃描前要記得先打開(kāi)藍(lán)牙成功,否則會(huì)很長(zhǎng)時(shí)間不響應(yīng))

    fun initBle() {
        // 檢查藍(lán)牙開(kāi)關(guān)
        val adapter = BluetoothAdapter.getDefaultAdapter()
        if (adapter == null) {
            ToastUtils.showShortSafe("本機(jī)沒(méi)有找到藍(lán)牙硬件或驅(qū)動(dòng)!")
            return
        } else {
            /*if (!adapter.isEnabled()) {
                //直接開(kāi)啟藍(lán)牙
                adapter.enable();
                //跳轉(zhuǎn)到設(shè)置界面
                //startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 112);
            }*/
            if (!adapter.isEnabled) {
                val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
                mContext.startActivityForResult(enableIntent, REQUEST_ENABLE_BT)
                // 在onActivityResult里提示是否開(kāi)成功根據(jù)Activity.RESULT_OK

            }
        }

        // 檢查是否支持BLE藍(lán)牙
        if (!mContext.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            ToastUtils.showShortSafe("本機(jī)不支持低功耗藍(lán)牙!")
            return
        }

        // Android 6.0動(dòng)態(tài)請(qǐng)求位置權(quán)限 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_COARSE_LOCATION)
            for (str in permissions) {
                if (mContext.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
                    mContext.requestPermissions(permissions, REQUEST_ENABLE_LC)
                    break
                }
            }
        }
    }

3. 掃描

這里用的是 Android5.0 新增的掃描API,

  • 盡量不要在掃描結(jié)果方法做耗時(shí)操作,它會(huì)快速連續(xù)調(diào)用(二期同一個(gè)設(shè)備連續(xù)返回)
  • 添加用服務(wù)uuid做過(guò)濾,會(huì)加快掃描速度節(jié)省時(shí)間,比自己通過(guò)代碼刷選好很多
  • 掃描是耗電較大的操作,注意定時(shí)停止,官方 Demo 寫的10m
 // 掃描BLE藍(lán)牙(不會(huì)掃描經(jīng)典藍(lán)牙)
    private void scanBle() {
        getConnedDevs();
        isScanning = true;
//        BluetoothAdapter bluetoothAdapter = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE).getDefaultAdapter();
        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
        // Android5.0新增的掃描API,掃描返回的結(jié)果更友好,比如BLE廣播數(shù)據(jù)以前是byte[] scanRecord,而新API幫我們解析成ScanRecord類
        bluetoothLeScanner.startScan(buildScanFilters(), buildScanSettings(), mScanCallback);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                bluetoothLeScanner.stopScan(mScanCallback); //停止掃描
                isScanning = false;
            }
        }, 3000);//(耗電)3秒后停止,根據(jù)需要設(shè)置
    }

    /**
     * 過(guò)濾
     *
     * @return
     */
    private List<ScanFilter> buildScanFilters() {
        List<ScanFilter> scanFilters = new ArrayList<>();

        ScanFilter.Builder builder = new ScanFilter.Builder();
        // 通過(guò)服務(wù) uuid 過(guò)濾自己要連接的設(shè)備
        builder.setServiceUuid(new ParcelUuid(BleUtils.Companion.getUUID_SERVICE()));
        scanFilters.add(builder.build());

        return scanFilters;
    }

    private ScanSettings buildScanSettings() {
        ScanSettings.Builder builder = new ScanSettings.Builder();
        builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);//低耗電模式
//SCAN_MODE_LOW_LATENCY 低延遲 我用的(耗電,無(wú)所謂,又不是一直掃描的,3秒就停止了)
        return builder.build();
    }


 private final ScanCallback mScanCallback = new ScanCallback() {// 掃描Callback
        @Override
        public void onScanResult(int callbackType, ScanResult result) {//盡量不做耗時(shí)操作,此方法會(huì)快速重復(fù)多次調(diào)用,返回相同的設(shè)備
            String address = result.getDevice().getAddress();
            if (!mDevices.contains(dev)) {
                mDevices.add(dev);
                notifyDataSetChanged();
                Log.i(TAG, "onScanResult: " + result); // result.getScanRecord() 獲取BLE廣播數(shù)據(jù)
            }
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            super.onBatchScanResults(results);
            Log.i(TAG, "onScanResult: " + "    " + results);
        }
    };

 // 重新掃描
    public void reScan() {
        mDevices.clear();
        notifyDataSetChanged();
        scanBle();
    }

這里說(shuō)一下,如果做藍(lán)牙設(shè)備管理頁(yè)面,可能區(qū)分是否是已連接的設(shè)備,網(wǎng)上又通過(guò)反射或其他挺麻煩的操作,也不見(jiàn)得獲取到,官方Api 就有提供

 BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
 List<BluetoothDevice> connectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT);

4. 連接

  1. 在設(shè)備列表 點(diǎn)擊 dev.connectGatt(mContext, false, mBluetoothGattCallback) -> 這里的false 是不自動(dòng)連,如果是true 使用時(shí)連接很慢。
  2. onConnectionStateChange()(1 成功 繼續(xù)啟動(dòng)發(fā)現(xiàn)服務(wù) 2 . 經(jīng)常斷鏈有時(shí)候連接一次不成功,就重復(fù) dev.connectGatt()->
  3. onServicesDiscovered() 服務(wù)發(fā)現(xiàn)成功好會(huì)把設(shè)備所有 Service 和 Service 里的 characteristics 及 characteristic.descriptors 全返回來(lái)(可以用來(lái)展示,調(diào)試和判斷自己要用的服務(wù) 特征 到底有沒(méi)有成功發(fā)現(xiàn))然后setNotify() 需要接收數(shù)據(jù)時(shí)得打開(kāi)通知 ->
  4. onDescriptorWrite() 寫入通知成功后mBluetoothGatt!!.requestMtu() 請(qǐng)求該設(shè)備的Mtu ,默認(rèn)23個(gè)字節(jié) 我們用的設(shè)備我申請(qǐng)256個(gè)字節(jié)最后只能拿到244個(gè)字節(jié)->
  5. onMtuChanged 拿到mtu-3 為什么減3 后面做數(shù)據(jù)分包->
  6. 最后通知連接成功 mBleConnCallBack!!.onConntionedCallBack(status, isConnected)
    這個(gè)callback是我自己寫的回調(diào) 才能做后面的發(fā)送,接收數(shù)據(jù)


    image.png

 /**
   * 連接設(shè)備###
   */
    fun connDev(dev: BluetoothDevice, bleConnCallBack: BleConnCallBack) {
        closeConn()
        mBleConnCallBack = bleConnCallBack
        mBluetoothGatt = dev.connectGatt(mContext, false, mBluetoothGattCallback) // 連接藍(lán)牙設(shè)備
    }

   /**
     * BLE中心設(shè)備連接外圍設(shè)備的數(shù)量有限(大概2~7個(gè)),在建立新連接之前必須釋放舊連接資源,否則容易出現(xiàn)連接錯(cuò)誤133
     */
    fun closeConn() {
        if (mBluetoothGatt != null) {
            mBluetoothGatt!!.disconnect()
            mBluetoothGatt!!.close()
        }
    }


 private fun initBleClient() {
        mBluetoothGattCallback = object : BluetoothGattCallback() {
            /**
             * 連接 狀態(tài)
             */
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                val dev = gatt.device
               AppLog.i(TAG, String.format("連接 狀態(tài)++onConnectionStateChange:%s,%s,%s,%s", dev.name, dev.address, status, newState))
                if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
                    isConnected = true
                    connTimes = 0

                    gatt.discoverServices() //啟動(dòng)服務(wù)發(fā)現(xiàn)
                } else {
                    isConnected = false
                    closeConn()
                    if (connTimes++ < 5) {//錯(cuò)誤連接5次
                        dev.connectGatt(mContext, false, this)
                    } else {
                        connTimes = 0
                        Handler(Looper.getMainLooper()).post { mBleConnCallBack!!.onConntionedCallBack(status, isConnected) }
                    }
                }
                BleDevConnedState.saveBasicID(dev.address, isConnected)
                AppLog.i(TAG,"isConnected : "+ isConnected+"  status:"+status)
                logTv(String.format(
                        if (status == 0)
                            if (newState == 2)
                                "與[%s]連接成功"
                            else "與[%s]連接斷開(kāi)"
                        else "與[%s]連接出錯(cuò),錯(cuò)誤碼:$status",
                        dev))
            }

            /**
             * 發(fā)現(xiàn)服務(wù)
             */
            override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
                AppLog.i(TAG, String.format("發(fā)現(xiàn)服務(wù)++onServicesDiscovered:%s,%s,%s", gatt.device.name, gatt.device.address, status))
                if (status == BluetoothGatt.GATT_SUCCESS) { //BLE服務(wù)發(fā)現(xiàn)成功
                    Handler(Looper.getMainLooper()).post{setNotify()}
                    // 遍歷獲取BLE服務(wù)Services/Characteristics/Descriptors的全部UUID
                    for (service in gatt.services) {
                        val allUUIDs = StringBuilder("UUIDs={\nS=" + service.uuid.toString())
                        for (characteristic in service.characteristics) {
                            allUUIDs.append(",\nC=").append(characteristic.uuid)
                            for (descriptor in characteristic.descriptors)
                                allUUIDs.append(",\nD=").append(descriptor.uuid)
                        }
                        allUUIDs.append("}")
                        logTv("發(fā)現(xiàn)服務(wù)$allUUIDs")

                    }
                }
            }

            /**
             * 讀
             */
            override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("onCharacteristicRead:%s,%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr, status))
            }


            /**
             * 寫入成功++
             * @param gatt
             * @param characteristic
             * @param status
             */
            override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("寫入成功++onCharacteristicWrite:%s,%s,%s,%s,%s,\n" +
                        " %s", gatt.device.name, gatt.device.address, uuid, valueStr, status, Arrays.toString(characteristic.value)))                
                onSendBytes()
            }

            /**
             * 發(fā)過(guò)來(lái)
             * 通知過(guò)來(lái)的消息++
             * @param gatt
             * @param characteristic
             */
            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("發(fā)過(guò)來(lái)++onCharacteristicChanged:%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr))
                onReadBytes(characteristic.value)
            }

            override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
                val uuid = descriptor.uuid
                val valueStr = Arrays.toString(descriptor.value)

                AppLog.i(TAG, String.format("onDescriptorRead:%s,%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr, status))
            }

            /**
             * 寫入通知成功
             */
            override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
                val uuid = descriptor.uuid
                val valueStr = Arrays.toString(descriptor.value)
                Handler(Looper.getMainLooper()).post {
                    mBluetoothGatt!!.requestMtu(256)//我們的是244
                }
            }

            override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
                super.onMtuChanged(gatt, mtu, status)
                mMtu=mtu-3
                AppLog.i(TAG,"連接成功"+"requestMtu"+"onMtuChanged:"+mtu+"  "+status)
                Handler(Looper.getMainLooper()).post {
                    mBleConnCallBack!!.onConntionedCallBack(status, isConnected)
                }
            }
        }
    }



5. 收發(fā)數(shù)據(jù)

與外圍設(shè)備交互經(jīng)常每次發(fā)的數(shù)據(jù)大于 mtu的,需要做分包處理,接收數(shù)據(jù)也要判斷數(shù)據(jù)的完整性最后才返回原數(shù)據(jù)做處理,所以一般交互最少包含包長(zhǎng)度,和包校驗(yàn)碼和原數(shù)據(jù)。當(dāng)然也可以加包頭,指令還有其他完整性校驗(yàn)。下面分享幾個(gè)公用方法:

   /**
     * 要可接收消息 需要要打開(kāi)通知###
     */
    fun setNotify() {
        val service = getGattService(UUID_SERVICE)
        if (service != null) {
            // 設(shè)置Characteristic通知
            val characteristic = service.getCharacteristic(UUID_CHAR_READ_NOTIFY)//通過(guò)UUID獲取可通知的Characteristic
            mBluetoothGatt!!.setCharacteristicNotification(characteristic, true)

            // 向Characteristic的Descriptor屬性寫入通知開(kāi)關(guān),使藍(lán)牙設(shè)備主動(dòng)向手機(jī)發(fā)送數(shù)據(jù)
            val descriptor = characteristic.getDescriptor(UUID_DESC_NOTITY)
            // descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);//和通知類似,但服務(wù)端不主動(dòng)發(fā)數(shù)據(jù),只指示客戶端讀取數(shù)據(jù)
            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE//1,0
            mBluetoothGatt!!.writeDescriptor(descriptor)
        }
    }

    //數(shù)據(jù)分包處理
    private fun splitPacketForMtuByte(data: ByteArray?): Queue<ByteArray> {
        val dataInfoQueue = LinkedList<ByteArray>()
        if (data != null) {
            var index = 0
            do {
                val currentData: ByteArray
                if (data.size - index <= mMtu) {
                    currentData = ByteArray(data.size - index)
                    System.arraycopy(data, index, currentData, 0, data.size - index)
                    index = data.size
                } else {
                    currentData = ByteArray(mMtu)
                    System.arraycopy(data, index, currentData, 0, mMtu)//從零開(kāi)始復(fù)制++ mMtu
                    index += mMtu
                }
                dataInfoQueue.offer(currentData)
            } while (index < data.size)
        }
        return dataInfoQueue
    }
    private fun intToByteArray(a: Int): ByteArray =
            byteArrayOf((a shr 24 and 0xFF).toByte(),
                    (a shr 16 and 0xFF).toByte(),
                    (a shr 8 and 0xFF).toByte(),
                    (a and 0xFF).toByte())


    private fun bytes2Int(byteArray: ByteArray): Int =
            (byteArray[0].toInt() and 0xFF shl 24) or
                    (byteArray[1].toInt() and 0xFF shl 16) or
                    (byteArray[2].toInt() and 0xFF shl 8) or
                    (byteArray[3].toInt() and 0xFF)

 /**
     * 寫數(shù)據(jù)###
     * @param instruction 指令
     * @param jsonData json數(shù)據(jù)
     * @param bleServerCallBack 接收完Basic 數(shù)據(jù)后響應(yīng)
     */
    fun write(instruction: Byte, jsonData: String, bleServerCallBack: BleServerCallBack) {
        val service = getGattService(UUID_SERVICE)
        if (service != null) {
         // ....
            var bleData = byteMergerAll(PACKAGE_HEAD, intToByteArray, instructionArray,crc32ByteArray, jsonData.toByteArray())
            bleData = byteMergerAll(
                  //...頭,length,指令等
                    jsonData.toByteArray(),
                    getShaHead(bleData)) //可以用hash校驗(yàn)數(shù)據(jù)完整性

            mBleServerCallBack = bleServerCallBack
            resultSize = 0
            mCharacteristic = service.getCharacteristic(UUID_CHAR_WRITE)//通過(guò)UUID獲取可寫的Characteristic
            writeCharacteristic(mCharacteristic, bleData)
        }

    }
   /**
     * 向characteristic寫數(shù)據(jù)
     *
     * @param value
     */
    private fun writeCharacteristic(characteristic: BluetoothGattCharacteristic?, value: ByteArray) {
        this.mCharacteristic = characteristic
        if (dataInfoQueue != null) {
            dataInfoQueue!!.clear()
            writeSize = value.size//這是我自己寫的讀寫進(jìn)度用的
            writeProgress = 0
            dataInfoQueue = splitPacketForMtuByte(value)
            onSendBytes()
        }
    }
 /**
     * 連續(xù)發(fā)送
     * 每次從隊(duì)列里取第一個(gè)同時(shí)移除
     */
    private fun onSendBytes() {
        if (dataInfoQueue != null && !dataInfoQueue!!.isEmpty()) {
            //檢測(cè)到發(fā)送數(shù)據(jù),直接發(fā)送
            if (dataInfoQueue!!.peek() != null) {
                mCharacteristic!!.value = dataInfoQueue!!.poll()//移除并返回隊(duì)列頭部的元素
                writeProgress += mCharacteristic!!.value.size
                mBleServerCallBack!!.onWriteProgress(writeProgress * 100 / writeSize)
                Handler(Looper.getMainLooper()).postDelayed({
                    mBluetoothGatt!!.writeCharacteristic(mCharacteristic)
                }, 125)//120ms
            }
        }
    }

            /**
             * 寫入成功++   
             * @param gatt
             * @param characteristic
             * @param status
             */
            override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("寫入成功++onCharacteristicWrite:%s,%s,%s,%s,%s,\n" +
                        " %s", gatt.device.name, gatt.device.address, uuid, valueStr, status, Arrays.toString(characteristic.value)))
                onSendBytes()//繼續(xù)發(fā)送
            }
        /**
             * 發(fā)過(guò)來(lái)
             * 通知過(guò)來(lái)的消息++
             * @param gatt
             * @param characteristic
             */
            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
                val uuid = characteristic.uuid
                val valueStr = String(characteristic.value)
                AppLog.i(TAG, String.format("發(fā)過(guò)來(lái)++onCharacteristicChanged:%s,%s,%s,%s", gatt.device.name, gatt.device.address, uuid, valueStr))
                onReadBytes(characteristic.value) //處理發(fā)回來(lái)的數(shù)據(jù),最后校驗(yàn)拼接好
            }

  • 上面的設(shè)置通知和寫數(shù)據(jù)都要 用到 mBluetoothGatt的 服務(wù),特征
  • 主動(dòng)讀取和通知 會(huì)用到 特征里的描述詳見(jiàn) setNotify() 和 write()
    而操作服務(wù),特征,描述 都通過(guò)特定的UUIU來(lái)操作,這些uuid都是藍(lán)牙設(shè)備從生產(chǎn)出來(lái)就不變了, 兩手機(jī)調(diào)試時(shí)可以自定義uuid.
        val UUID_SERVICE =UUID.fromString("00000000-0000-0000-0000-0000000000f0")//自定義UUID
        val UUID_CHAR_READ_NOTIFY = UUID.fromString("00000000-0000-0000-0000-0000000000f2")
        val UUID_CHAR_WRITE = UUID.fromString("00000000-0000-0000-0000-000000000001")
        val UUID_DESC_NOTITY =  UUID.fromString("00000000-0000-0000-0000-000000000002")

我自己封裝的一個(gè)BleUtil ,因?yàn)樯婕案緲I(yè)務(wù)關(guān)聯(lián)性太強(qiáng)(主要是傳輸包的協(xié)議不同)就先不開(kāi)源出來(lái)了,如果這邊文章對(duì)大家有幫助反饋不錯(cuò),我會(huì)考慮上傳個(gè)demo到github供大家使用,
在這先給大家推薦一個(gè)不錯(cuò) Demo,里面除了沒(méi)有分包,協(xié)議,和傳輸速率?;镜墓δ芏加校艺{(diào)試數(shù)據(jù)到打印到界面上了。最主要是它可以用兩個(gè)個(gè)手機(jī)一個(gè)當(dāng)中心設(shè)備一個(gè)當(dāng)外圍設(shè)備調(diào)試。

如果在實(shí)際使用藍(lán)牙, 調(diào)用時(shí)得姿勢(shì)應(yīng)該是這樣的
 BleUtils.getInstance(mContext)
            .write(
                BleInstructions.signTx,
                jsonObject.toString(),
                object : BleServerCallBack {
                    override fun onResponse(instruction: Byte?, result: String) {
                        val fromJson = Gson().fromJson(result, xxxBean::class.java)
                     
                    }

                    override fun onWriteProgress(progress: Int) {
                    }

                    override fun onReadProgress(progress: Int) {
                    }
                })

6. 傳輸速率

首先傳輸速率優(yōu)化有兩個(gè)方向,1 外圍設(shè)備傳輸?shù)紸ndroid 。2 Android傳輸?shù)酵鈬O(shè)備。
我在開(kāi)發(fā)中首先先使用上面那位仁兄的demo調(diào)試,兩個(gè)Android 設(shè)備調(diào)試不延時(shí),上一個(gè)成功馬上下一個(gè),最多一秒發(fā)11個(gè)20字節(jié)的包。

后來(lái)和我們的藍(lán)牙設(shè)備調(diào)試時(shí)發(fā)現(xiàn)發(fā)送特別快,但是數(shù)據(jù)不完整,他藍(lán)牙模塊接收成功了,但是透?jìng)鲾?shù)據(jù)到芯片處理時(shí)發(fā)現(xiàn)不完整,我們的硬件小伙伴說(shuō)因?yàn)?a target="_blank">波特率限制(差不多每10字節(jié)透?jìng)饕臅r(shí)1ms)和藍(lán)牙模塊的buff (打印時(shí)是最多100byte,100打印的)限制,就算藍(lán)牙模塊每包都告訴你接收成功,也是沒(méi)透?jìng)魍昃陀纸邮樟?。后?lái)通過(guò)調(diào)試每次發(fā)20K數(shù)據(jù),最后是 Android 發(fā)是 20字節(jié)/130ms穩(wěn)定。給Android 發(fā)是20字節(jié)/ 8ms。 (天殺的20字節(jié),網(wǎng)上都是說(shuō)20字節(jié)最多了)

后來(lái)看了國(guó)外一家物聯(lián)網(wǎng)公司總結(jié)的 Ble 吞吐量的文章(上面有連接),知道Android 每個(gè)延時(shí)是可以連續(xù)接收6個(gè)包的。就改為 120字節(jié)/ 16ms (為啥是16ms,不是每次間隔要6個(gè)包嗎,怎么像間隔兩次,這時(shí)因?yàn)椴ㄌ芈视绊?,多?個(gè)包100字節(jié),差不多 我們的單片機(jī)透?jìng)鞯剿{(lán)牙模塊要多耗時(shí)不到10ms )
而Android 發(fā)數(shù)據(jù)可以申請(qǐng) 我們?cè)O(shè)備的mtu 來(lái)得到最多每次能發(fā)多少字節(jié)。延時(shí)還是130ms,即:241字節(jié)/ 130ms 提高12倍,這個(gè)速度還可以。

7. 擴(kuò)展

BLE的傳輸速率分析

根據(jù)藍(lán)牙BLE協(xié)議, 物理層physical layer的傳輸速率是1Mbps,相當(dāng)于每秒125K字節(jié)。事實(shí)上,其只是基準(zhǔn)傳輸速率,協(xié)議規(guī)定BLE不能連續(xù)不斷地傳輸數(shù)據(jù)包,否則就不能稱為低功耗藍(lán)牙了。連續(xù)傳輸自然會(huì)帶來(lái)高功耗。所以,藍(lán)牙的最高傳輸速率并不由物理層的工作頻率決定的。

在實(shí)際的操作過(guò)程中,如果主機(jī)連線不斷地發(fā)送數(shù)據(jù)包,要么丟包嚴(yán)重要么連接出現(xiàn)異常而斷開(kāi)。

在BLE里面,傳輸速度受其連接參數(shù)所影響。連接參數(shù)定義如下:

1)連接間隔。藍(lán)牙基帶是跳頻工作的,主機(jī)和從機(jī)會(huì)商定多長(zhǎng)時(shí)間進(jìn)行跳頻連接,連接上才能進(jìn)行數(shù)據(jù)傳輸。這個(gè)連接和廣播狀態(tài)和連接狀態(tài)的連接不是一樣的意思。主機(jī)在從機(jī)廣播時(shí)進(jìn)行連接是應(yīng)用層的主動(dòng)軟件行為。而跳頻過(guò)程中的連接是藍(lán)牙基帶協(xié)議的規(guī)定,完全由硬件控制,對(duì)應(yīng)用層透明。明顯,如果這個(gè)連接間隔時(shí)間越短,那么傳輸?shù)乃俣染驮龃?。連接上傳完數(shù)據(jù)后,藍(lán)牙基帶即進(jìn)入休眠狀態(tài),保證低功耗。其是1.25毫秒一個(gè)單位。

2)連接延遲。其是為了低功耗考慮,允許從機(jī)在跳頻過(guò)程中不理會(huì)主機(jī)的跳頻指令,繼續(xù)睡眠一段時(shí)間。而主機(jī)不能因?yàn)閺臋C(jī)睡眠而認(rèn)為其斷開(kāi)連接了。其是1.25毫秒一個(gè)單位。明顯,這個(gè)數(shù)值越小,傳輸速度也高。

藍(lán)牙BLE協(xié)議規(guī)定連接參數(shù)最小是5,即7.25毫秒;而Android手機(jī)規(guī)定連接參數(shù)最小是8,即10毫秒。iOS規(guī)定是16,即20毫秒。

連接參數(shù)完全由主機(jī)決定,但從機(jī)可以發(fā)出更新參數(shù)申請(qǐng),主機(jī)可以接受也可以拒絕。android手機(jī)一部接受,而ios比較嚴(yán)格,拒絕的概率比較高。

參考:
在iOS和Android上最大化BLE吞吐量
最大化BLE吞吐量第2部分:使用更大的ATT MTU

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 因?yàn)樽约旱捻?xiàng)目中有用到了藍(lán)牙相關(guān)的功能,所以之前也斷斷續(xù)續(xù)地針對(duì)藍(lán)牙通信尤其是BLE通信進(jìn)行了一番探索,整理出了一...
    陳利健閱讀 119,712評(píng)論 172 300
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,355評(píng)論 25 708
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 14,128評(píng)論 2 59
  • 背景 藍(lán)牙歷史說(shuō)到藍(lán)牙,就不得不說(shuō)下藍(lán)牙技術(shù)聯(lián)盟(Bluetooth SIG),它負(fù)責(zé)藍(lán)牙規(guī)范制定和推廣的國(guó)際組織...
    徐正峰閱讀 13,047評(píng)論 6 33
  • 簡(jiǎn)愛(ài)跑步法 這套方法的核心價(jià)值觀就是:運(yùn)動(dòng)最重要的是不能受傷,所以跑步要慢 。簡(jiǎn)愛(ài)跑步法有一個(gè)5字要訣:挺、傾、柔...
    向陽(yáng)的石頭閱讀 178評(píng)論 0 0

友情鏈接更多精彩內(nèi)容