藍(lán)牙BLE(BlueTooth BLE)入門及爬坑指南

前言

最近比較忙,兩三周沒有更新簡書了,公司正好在做藍(lán)牙BLE的項目,本來覺得挺簡單的東西從網(wǎng)上找了個框架,就咔咔地開始搞,搞完以后才發(fā)現(xiàn)里面還有不少坑呢,故而寫一篇藍(lán)牙BLE入門及爬坑指南,旨在幫助剛?cè)胨{(lán)牙BLE的小伙伴們少走彎路。

注:本文所有的具體代碼實現(xiàn)都在文章最后的github上

經(jīng)典藍(lán)牙和藍(lán)牙BLE的區(qū)別

說起藍(lán)牙,大家一定聽過藍(lán)牙1.0 2.0 3.0 4.0,不過現(xiàn)在已經(jīng)不再用版本號區(qū)分藍(lán)牙了,藍(lán)牙1.0~3.0都是經(jīng)典藍(lán)牙,在塞班系統(tǒng)就已經(jīng)開始使用了,確實很經(jīng)典。有些人一直認(rèn)為藍(lán)牙4.0就是藍(lán)牙BLE,其實是錯誤的。因為4.0是雙模的,既包括經(jīng)典藍(lán)牙又包括低能耗藍(lán)牙。經(jīng)典藍(lán)牙和藍(lán)牙BLE雖然都是藍(lán)牙,但其實還是存在很大區(qū)別的。藍(lán)牙BLE相比于經(jīng)典藍(lán)牙的優(yōu)點是搜索、連接的速度更快,關(guān)鍵就是BLE(Bluetooth Low Energy)低能耗,缺點呢就是傳輸?shù)乃俣嚷?,傳輸?shù)臄?shù)據(jù)量也很小,每次只有20個字節(jié)。但是藍(lán)牙BLE因為其低能耗的優(yōu)點,在智能穿戴設(shè)備和車載系統(tǒng)上的應(yīng)用越來越廣泛,因此,藍(lán)牙BLE開發(fā)已經(jīng)是我們Android開發(fā)不得不去掌握的一門技術(shù)了。

藍(lán)牙BLE的簡介

藍(lán)牙BLE是在Android4.3系統(tǒng)及以上引入的,但是僅作為中央設(shè)備,直到5.0以后才可以既作為中央設(shè)備又可以作為周邊設(shè)備。也就是5.0系統(tǒng)以后,可以手機(jī)控制手機(jī)了,不過絕大多數(shù)的場景手機(jī)還是作為中央設(shè)備去控制其他的周邊設(shè)備。Android BLE 使用的藍(lán)牙協(xié)議是 GATT 協(xié)議。關(guān)于這個GATT協(xié)議,我就不詳細(xì)給大家介紹了,放上個鏈接,感興趣的可以看一下http://blog.chinaunix.net/uid-21411227-id-5750680.html

Service和Characteristic

Service是服務(wù),Characteristic是特征值。藍(lán)牙里面有多個Service,一個Service里面又包括多個Characteristic,具體的關(guān)系可以看圖
service和characteristic的關(guān)系

圖中畫的比較少,實際上一個藍(lán)牙協(xié)議里面包含的Service和Characteristic是比較多的 ,這時候你可能會問,這么多的同名屬性用什么來區(qū)分呢?答案就是UUID,每個Service或者Characteristic都有一個 128 bit 的UUID來標(biāo)識。Service可以理解為一個功能集合,而Characteristic比較重要,藍(lán)牙設(shè)備正是通過Characteristic來進(jìn)行設(shè)備間的交互的(如讀、寫、訂閱等操作)。

小結(jié)

經(jīng)典藍(lán)牙和藍(lán)牙BLE雖然都是藍(lán)牙,但是在連接和數(shù)據(jù)傳遞上還是存在很大的區(qū)別,而藍(lán)牙BLE依靠著其低能耗的特點,逐漸在智能穿戴設(shè)備上占有一席之地。藍(lán)牙BLE基于GATT協(xié)議傳輸數(shù)據(jù),提供了Serivice和Characteristic進(jìn)行設(shè)備之間的通訊。以上,就是藍(lán)牙BLE的基本概念,下面開始藍(lán)牙BLE的正式開發(fā)!

藍(lán)牙BLE正確開發(fā)姿勢(本文重點)

第一步:聲明藍(lán)牙BLE權(quán)限

<!--聲明藍(lán)牙權(quán)限-->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Android6.0系統(tǒng)以上開啟藍(lán)牙還需要定位權(quán)限,定位權(quán)限屬于危險權(quán)限,需要動態(tài)申請,筆者實現(xiàn)的方法是使用了RxPerssion動態(tài)庫。

 /**
     * 檢查權(quán)限
     */
    private void checkPermissions() {
        RxPermissions rxPermissions = new RxPermissions(MainActivity.this);
        rxPermissions.request(android.Manifest.permission.ACCESS_FINE_LOCATION)
                .subscribe(new io.reactivex.functions.Consumer<Boolean>() {
                    @Override
                    public void accept(Boolean aBoolean) throws Exception {
                        if (aBoolean) {
                            // 用戶已經(jīng)同意該權(quán)限
                            scanDevice();
                        } else {
                            // 用戶拒絕了該權(quán)限,并且選中『不再詢問』
                            ToastUtils.showLong("用戶開啟權(quán)限后才能使用");
                        }
                    }
                });
    }

第二步:連接藍(lán)牙前需要初始化的工作

mBluetoothManager= (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
        mBluetoothAdapter=mBluetoothManager.getAdapter();
        if (mBluetoothAdapter==null||!mBluetoothAdapter.isEnabled()){
            Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(intent,0);
        }

拿到BluetoothManager,在通過BluetoothManager.getAdapter()拿到BluetoothAdapter,然后判斷一下藍(lán)牙是否打開,沒打開的話Intent隱式調(diào)用打開系統(tǒng)開啟藍(lán)牙界面。

第三步:掃描設(shè)備

 /**
     * 開始掃描 10秒后自動停止
     * */
    private void scanDevice(){
        tvSerBindStatus.setText("正在搜索");
        isScaning=true;
        pbSearchBle.setVisibility(View.VISIBLE);
        mBluetoothAdapter.startLeScan(scanCallback);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //結(jié)束掃描
                mBluetoothAdapter.stopLeScan(scanCallback);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        isScaning=false;
                        pbSearchBle.setVisibility(View.GONE);
                    }
                });
            }
        },10000);
    }

藍(lán)牙掃描如果不停止,會持續(xù)掃描,很消耗資源,一般都是開啟10秒左右停止

BluetoothAdapter.LeScanCallback scanCallback=new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            Log.e(TAG, "run: scanning...");
            if (!mDatas.contains(device)){
                mDatas.add(device);
                mRssis.add(rssi);
                mAdapter.notifyDataSetChanged();
            }

        }
    };

這里的scanCallback是上一段代碼里mBluetoothAdapter.startLeScan(scanCallback)里面的對象,其中onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)里面的參數(shù)都很直觀,device是設(shè)備對象,rssi掃描到的設(shè)備強(qiáng)度,scanRecord是掃面記錄,沒什么卵用。 掃描過的設(shè)備仍然會被再次掃描到,因此要加入設(shè)備列表之前可以判斷一下,如果已經(jīng)加入過了就不必再次添加了。
看一下搜索的效果圖吧


搜索效果圖

第三步:連接設(shè)備

BluetoothDevice bluetoothDevice= mDatas.get(position);
                    //連接設(shè)備
                    tvSerBindStatus.setText("連接中");
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,
                                true, gattCallback, TRANSPORT_LE);
                    } else {
                        mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,
                                true, gattCallback);
                    }

連接這里大家可能已經(jīng)發(fā)現(xiàn)了,判斷了一下手機(jī)系統(tǒng),6.0及以上連接設(shè)備的方法是bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE)。這里就是我遇見的第一個大坑了,我的手機(jī)是8.0的系統(tǒng)使用
bluetoothDevice.connectGatt(MainActivity.this, true, gattCallback);總是連接失敗,提示status返回133,用了各種方法都不行,后臺一查才發(fā)現(xiàn)6.0及以上系統(tǒng)的手機(jī)要使用bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE),其中TRANSPORT_LE參數(shù)是設(shè)置傳輸層模式。傳輸層模式有三種TRANSPORT_AUTO 、TRANSPORT_BREDR 和TRANSPORT_LE。如果不傳默認(rèn)TRANSPORT_AUTO,6.0系統(tǒng)及以上需要使用TRANSPORT_LE這種傳輸模式,具體為啥,我也不知道,我猜是因為Android6.0及以上系統(tǒng)重新定義了藍(lán)牙BLE的傳輸模式必須使用TRANSPORT_LE這種方式吧。bluetoothDevice.connectGatt()方法返回的對象BluetoothGatt,這個BluetoothGatt對象非常重要,甚至可以說是最重要的。一般都是單獨聲明成全局變量來使用的,因為我們設(shè)備的讀、寫和訂閱等操作都需要用到這個對象。

 private BluetoothGattCallback gattCallback=new BluetoothGattCallback() {
        /**
         * 斷開或連接 狀態(tài)發(fā)生變化時調(diào)用
         * */
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            Log.e(TAG,"onConnectionStateChange()");
            if (status==BluetoothGatt.GATT_SUCCESS){
                //連接成功
                if (newState== BluetoothGatt.STATE_CONNECTED){
                    Log.e(TAG,"連接成功");
                    //發(fā)現(xiàn)服務(wù)
                    gatt.discoverServices();
                }
            }else{
                //連接失敗
                Log.e(TAG,"失敗=="+status);
                mBluetoothGatt.close();
                isConnecting=false;
            }
        }
        /**
         * 發(fā)現(xiàn)設(shè)備(真正建立連接)
         * */
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            //直到這里才是真正建立了可通信的連接
            isConnecting=false;
            Log.e(TAG,"onServicesDiscovered()---建立連接");
            //獲取初始化服務(wù)和特征值
            initServiceAndChara();
            //訂閱通知
            mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
                    .getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);


            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    bleListView.setVisibility(View.GONE);
                    operaView.setVisibility(View.VISIBLE);
                    tvSerBindStatus.setText("已連接");
                }
            });
        }
        /**
         * 讀操作的回調(diào)
         * */
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
            Log.e(TAG,"onCharacteristicRead()");
        }
        /**
         * 寫操作的回調(diào)
         * */
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);

            Log.e(TAG,"onCharacteristicWrite()  status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));
        }
        /**
         * 接收到硬件返回的數(shù)據(jù)
         * */
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            Log.e(TAG,"onCharacteristicChanged()"+characteristic.getValue());
            final byte[] data=characteristic.getValue();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    addText(tvResponse,bytes2hex(data));
                }
            });

        }
    };

這一段是連接的回調(diào) 這里我只重寫了幾個比較重要的方法,每個方法都有具體的注釋,需要強(qiáng)調(diào)的是有些同學(xué)重復(fù)連接會報133連接失敗,這個調(diào)用一下mBluetoothGatt.close()就可以解決,還有要注意的就是回調(diào)里面的方法不要做耗時的操作,也不要在回調(diào)方法里面更新UI,這樣有可能會阻塞線程。

第四步:發(fā)現(xiàn)服務(wù)

  /**
         * 發(fā)現(xiàn)設(shè)備(真正建立連接)
         * */
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            //直到這里才是真正建立了可通信的連接
            isConnecting=false;
            Log.e(TAG,"onServicesDiscovered()---建立連接");
            //獲取初始化服務(wù)和特征值
            initServiceAndChara();
            //訂閱通知
            mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
                    .getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);


            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    bleListView.setVisibility(View.GONE);
                    operaView.setVisibility(View.VISIBLE);
                    tvSerBindStatus.setText("已連接");
                }
            });
        }

直到這里才是建立了真正可通信的連接,下一步就可以進(jìn)行讀寫訂閱等操作,之前文章中有提到要通過Service和Characteristic特征值來操作,但是如果獲取到對應(yīng)的服務(wù)和特征值呢?一般硬件開發(fā)工程師會定義好UUID,通知到我們,這個時候我們只需要調(diào)用下面的方法就能拿到Service和Characteristic

//write_UUID_service和write_UUID_chara是硬件工程師告訴我們的
 BluetoothGattService service=mBluetoothGatt.getService(write_UUID_service);
 BluetoothGattCharacteristic charaWrite=service.getCharacteristic(write_UUID_chara);

當(dāng)然也會比較坑爹的,就是硬件工程師居然不知道Service和Characteristic的UUID是啥(沒錯,我就遇見了),這個時候也不要慌,因為我們可以通過Android拿得到對應(yīng)UUID.

 private void initServiceAndChara(){
        List<BluetoothGattService> bluetoothGattServices= mBluetoothGatt.getServices();
        for (BluetoothGattService bluetoothGattService:bluetoothGattServices){
            List<BluetoothGattCharacteristic> characteristics=bluetoothGattService.getCharacteristics();
            for (BluetoothGattCharacteristic characteristic:characteristics){
                int charaProp = characteristic.getProperties();
                if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
                    read_UUID_chara=characteristic.getUuid();
                    read_UUID_service=bluetoothGattService.getUuid();
                    Log.e(TAG,"read_chara="+read_UUID_chara+"----read_service="+read_UUID_service);
                }
                if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
                    write_UUID_chara=characteristic.getUuid();
                    write_UUID_service=bluetoothGattService.getUuid();
                    Log.e(TAG,"write_chara="+write_UUID_chara+"----write_service="+write_UUID_service);
                }
                if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {
                    write_UUID_chara=characteristic.getUuid();
                    write_UUID_service=bluetoothGattService.getUuid();
                    Log.e(TAG,"write_chara="+write_UUID_chara+"----write_service="+write_UUID_service);

                }
                if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
                    notify_UUID_chara=characteristic.getUuid();
                    notify_UUID_service=bluetoothGattService.getUuid();
                    Log.e(TAG,"notify_chara="+notify_UUID_chara+"----notify_service="+notify_UUID_service);
                }
                if ((charaProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
                    indicate_UUID_chara=characteristic.getUuid();
                    indicate_UUID_service=bluetoothGattService.getUuid();
                    Log.e(TAG,"indicate_chara="+indicate_UUID_chara+"----indicate_service="+indicate_UUID_service);

                }
            }
        }
    }

BluetoothGattCharacteristic.PROPERTY_READ:對應(yīng)的就是讀取數(shù)據(jù)
BluetoothGattCharacteristic.PROPERTY_WRITE和BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE:都是寫入,區(qū)別就是據(jù)說PROPERTY_WRITE_NO_RESPONSE寫入效率更高,而NO_RESPONSE沒有響應(yīng),我也沒弄懂這個響應(yīng)指的是什么響應(yīng),我用PROPERTY_WRITE_NO_RESPONSE寫入,訂閱中依然得到了回應(yīng),這里有知道的朋友可以告訴一下筆者。
PROPERTY_NOTIFY和PROPERTY_INDICATE:這里都是訂閱的方法,區(qū)別就是PROPERTY_INDICATE一定能接收到訂閱回調(diào),一般用來接收一些比較重要的必須的回調(diào),但是不能太頻繁;而PROPERTY_NOTIFY不一定能百分之百接收到回調(diào),可以頻繁接收,這個一般也是使用得比較多的訂閱方式。

讀取數(shù)據(jù)

private void readData() {
        BluetoothGattCharacteristic characteristic=mBluetoothGatt.getService(read_UUID_service)
                .getCharacteristic(read_UUID_chara);
        mBluetoothGatt.readCharacteristic(characteristic);
    }

讀取數(shù)據(jù)用得比較少,我也就不重點介紹了,一般我們都是先訂閱,再寫入,在訂閱中回調(diào)數(shù)據(jù)進(jìn)行交互。

寫入數(shù)據(jù)

 private void writeData(){
        BluetoothGattService service=mBluetoothGatt.getService(write_UUID_service);
        BluetoothGattCharacteristic charaWrite=service.getCharacteristic(write_UUID_chara);
        byte[] data=HexUtil.hexStringToBytes(hex);
        if (data.length>20){//數(shù)據(jù)大于個字節(jié) 分批次寫入
            Log.e(TAG, "writeData: length="+data.length);
            int num=0;
            if (data.length%20!=0){
                num=data.length/20+1;
            }else{
                num=data.length/20;
            }
            for (int i=0;i<num;i++){
                byte[] tempArr;
                if (i==num-1){
                    tempArr=new byte[data.length-i*20];
                    System.arraycopy(data,i*20,tempArr,0,data.length-i*20);
                }else{
                    tempArr=new byte[20];
                    System.arraycopy(data,i*20,tempArr,0,20);
                }
                charaWrite.setValue(tempArr);
                mBluetoothGatt.writeCharacteristic(charaWrite);
            }
        }else{
            charaWrite.setValue(data);
            mBluetoothGatt.writeCharacteristic(charaWrite);
        }
    }

這里寫入數(shù)據(jù)需要說一下,首先拿到寫入的BluetoothGattService和BluetoothGattCharacteristic對象,把要寫入的內(nèi)容轉(zhuǎn)成16進(jìn)制的字節(jié)(藍(lán)牙BLE規(guī)定的數(shù)據(jù)格式),然后要判斷一下字節(jié)大小,如果大于20個字節(jié)就要分批次寫入了,因為GATT協(xié)議規(guī)定藍(lán)牙BLE每次傳輸?shù)挠行ё止?jié)不能超過20個,最后通過BluetoothGattCharacteristic.setValue(data); mBluetoothGatt.writeCharacteristic(BluetoothGattCharacteristic);就可以完成寫入了。寫入成功了會回調(diào)onCharacteristicWrite方法

/**
         * 寫操作的回調(diào)
         * */
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);

            Log.e(TAG,"onCharacteristicWrite()  status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));
        }

訂閱回調(diào)

//訂閱通知
            mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
                    .getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);

注意一定要寫在寫入之前,要不然就收不到寫入的數(shù)據(jù),我一般都是在發(fā)現(xiàn)服務(wù)之后就訂閱。關(guān)于訂閱收不到這里,需要注意一下,首先你寫入的和訂閱的Characteristic對象一定要屬于同一個Service對象,另外就是保證你寫入的數(shù)據(jù)沒問題,否則就可能收不到訂閱回調(diào)。

最后上一波效果圖:
寫入以后返回的數(shù)據(jù)

這里在EditText雖然沒有顯示,但其實我直接點擊默認(rèn)就輸入7B46363941373237323532443741397D 這一串?dāng)?shù)據(jù),實在懶得打了

總結(jié)

第一次打這么多字有點小累,總結(jié)這個地方就不多說了,這里就說點注意事項,在進(jìn)行藍(lán)牙操作的時候最好每次都延遲200ms再執(zhí)行,因為藍(lán)牙是線程安全的,當(dāng)你同時執(zhí)行多次操作的時候會出現(xiàn)busy的情況導(dǎo)致執(zhí)行失敗,所以這里建議一般都執(zhí)行一步操作延時一會,這樣可以保證操作的成功率,另外就是如果大家入了門以后想要快速的開發(fā)的話,建議網(wǎng)上找好輪子,找一個好用的,可以先自己看看實現(xiàn)的源碼,當(dāng)然最好就是自己封裝一個。
最后放上我的github地址:https://github.com/kaka10xiaobang/BlueToothBLE

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

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

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