Android BLE 快速上手指南

NoHarry發表於2018-11-02

原文地址


本文旨在提供一個方便沒接觸過Android上低功耗藍芽(Bluetooth Low Energy)的同學快速上手使用的簡易教程,因此對其中的一些細節不做過分深入的探討,此外,為了讓沒有Ble裝置的同學也能模擬與裝置的互動過程,本文還提供了中央裝置(central)和外圍裝置(peripheral)的示例程式碼,只需2部手機大家就可以愉快的“左右互搏”了。

準備工作

角色

上面我們提到了中央裝置(central)和外圍裝置(peripheral),在這裡我們可以這樣簡單的理解:

  • 中央裝置(central):收到外圍裝置發出的廣播訊號後能主動發起連線的主裝置,例如我們給摩拜單車開鎖時我們的手機就是作為中央裝置連線單車並進行開鎖等一系列操作的,通常情況下同一時間一臺中央裝置只能與最多7臺外圍裝置建立連線。
  • 外圍裝置(peripheral):能被中央裝置連線的從裝置,同一時間外圍裝置只能被一箇中央裝置連線。

:Android從4.3(API Level 18) 開始支援低功耗藍芽,但是剛開始只支援作為中央裝置(central)模式,從 Android 5.0(API Level 21) 開始才支援作為外圍裝置(peripheral)的模式,因此我們最好使用Android 5.0以上版本的手機進行下面的操作。

需要的許可權

  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  //使用ble掃描時還需要我們到’設定 > 安全性和位置資訊 > 位置資訊‘處開啟位置資訊,
  //否則將會搜尋不到周圍的裝置
複製程式碼

可能有人會問為什麼使用低功耗藍芽還需要位置許可權?簡單來說就是藍芽也有定位的功能。

示例程式碼

開始

接下來我們就準備開始實際操作了,首先我們準備2臺手機,手機A作為中央裝置,手機B作為外圍裝置,在開啟B手機的ble廣播後,我們使用A手機進行開啟藍芽-->掃描-->連線-->獲取服務,特徵-->開啟通知-->寫特徵-->讀特徵-->斷開連線,通過這些步驟我們就能學會Android Ble 的基本方法的使用。

從掃描開始,接下來的這些操作中你可能會遇到各種奇奇怪怪的問題,為了減少大家踩坑的概率,我會在後面的操作中分享一些可能會遇到的問題和解決方法,有的問題在官方文件中可能有提到,有的在一些論壇帖子中有提及,還有的一些就是自己的經驗之談。

開啟藍芽

開啟藍芽有以下兩種方式:

    //方法一
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
    if (mBluetoothAdapter != null){
      mBluetoothAdapter.enable();
    }
複製程式碼
    //方法二
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
    if (!mBluetoothAdapter.isEnabled() && !mBluetoothAdapter.isEnabled()) {
      Intent enableBtIntent = new Intent(
          BluetoothAdapter.ACTION_REQUEST_ENABLE);
      startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
複製程式碼
  • 使用方法一將會直接開啟藍芽,使用方法二會跳轉到系統Activity由使用者手動開啟藍芽

掃描

掃描是一個非常耗電的操作,因此當我們找到我們需要的裝置後應該馬上停止掃描。官方提供了2個掃描的方法:

  //舊API
  //啟動掃描
  private void scan(){
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothManager.getAdapter().startLeScan(mLeScanCallback);

    //如果想要指定搜尋裝置,可以使用下面這個構造方法,傳入外圍裝置廣播出的服務的UUID陣列
    UUID[] uuids=new UUID[]{UUID_ADV_SERVER};
    bluetoothManager.getAdapter().startLeScan(uuids,mLeScanCallback);
  }

  //停止掃描
  private void stopScan(){
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothManager.getAdapter().stopLeScan(mLeScanCallback);
  }

  //掃描結果回撥
  LeScanCallback mLeScanCallback = new LeScanCallback() {
      @Override
      public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
        //device:掃描到的藍芽裝置物件
        //rssi:掃描到的裝置的訊號強度,這是一個負值,值越大代表訊號強度越大
        //scanRecord:掃描到的裝置廣播的資料,包含裝置名,服務UUID等
      }
    };
複製程式碼

↑ 這是個在Android 5.0時被標註deprecated的API,該方法目前仍能使用。由於onLeScan中回撥出的裝置的廣播資料需要自己手動解析,這是個比較麻煩的過程。

advData

在新的API中已經封裝了方法來解析廣播資料,如果為了適配性使用這個舊的掃描方法,同時又希望解析得到廣播中的資料,我們可以使用原始碼中新API使用的解析方法(需要稍許修改,直接使用會報錯),或者使用我自己修改過的方法,如果你想了解更多關於廣播資料的解析可以看Core Specifications 5.0中Volume 3, Part C, Section 11這一節。

  //新API,需要Android 5.0(API Level 21)及以上版本才能使用
  //啟動掃描
  private void scanNew() {
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    //基本的掃描方法
    bluetoothManager
        .getAdapter()
        .getBluetoothLeScanner()
        .startScan(mScanCallback);


    //設定一些掃描引數
    ScanSettings settings=new ScanSettings
        .Builder()
        //例如這裡設定的低延遲模式,也就是更快的掃描到周圍裝置,相應耗電也更厲害
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
        .build();

    //你需要設定的過濾條件,不只可以像舊API中的按服務UUID過濾
    //還可以按裝置名稱,MAC地址等條件過濾
    List<ScanFilter> scanFilters=new ArrayList<>();

    //如果你需要過濾掃描到的裝置可以用下面的這種構造方法
    bluetoothManager
        .getAdapter()
        .getBluetoothLeScanner()
        .startScan(scanFilters,settings,mScanCallback);
  }

  //掃描結果回撥
  ScanCallback mScanCallback = new ScanCallback() {
     @Override
     public void onScanResult(int callbackType, ScanResult result) {
        //callbackType:掃描模式
        //result:掃描到的裝置資料,包含藍芽裝置物件,解析完成的廣播資料等
     }
   };

  //停止掃描
  private void stopNewScan(){
    BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    bluetoothManager.getAdapter().getBluetoothLeScanner().stopScan(mScanCallback);
  }
複製程式碼

相比舊API,新API的功能更全面,但是需要Android 5.0以上才能使用,究竟需要使用哪種方法,大家可以根據自己的實際情況選擇。

注意坑來了:

  • 1.如果搜尋不到裝置,請檢查對於Android 6.0及以上版本ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION許可權是否已經動態授予,同時檢查位置資訊(也就是GPS)是否已經開啟,一般來說搜不到裝置就是這兩個原因。

  • 2.不管是新舊API的掃描結果回撥都是不停的回撥掃描到的裝置,就算是相同的裝置也會重複回撥,直到你停止掃描,因此最好不要在回撥方法中做過多的耗時操作,否則可能會出現這個問題,如果需要處理回撥的資料可以把資料放到另外一個執行緒處理,讓回撥儘快返回。

連線

同一時間我們只能對一個外圍裝置發起連線,如果需要對多個裝置連線可以等上一個連線成功後再進行下一個連線,否則如果前面的某個連線操作失敗了沒有回撥,後面的操作會被一直阻塞。

  //發起連線
  private void connect(BluetoothDevice device){
    mBluetoothGatt = device.connectGatt(context, false, mBluetoothGattCallback);
  }

  //Gatt操作回撥,此回撥很重要,後面所有的操作結果都會在此方法中回撥
  BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
     @Override
     public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
       //gatt:GATT客戶端
       //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常
       //newState:當前連線處於的狀態,例如連線成功,斷開連線等

       //當連線狀態改變時觸發此回撥
     }

     @Override
     public void onServicesDiscovered(BluetoothGatt gatt, int status) {
       //gatt:GATT客戶端
       //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常

       //成功獲取服務時觸發此回撥,“獲取服務,特徵”一節會介紹
     }

     @Override
     public void onCharacteristicRead(BluetoothGatt gatt,
         final BluetoothGattCharacteristic characteristic, final int status) {
           //gatt:GATT客戶端
           //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常
           //characteristic:被讀的特徵

           //當對特徵的讀操作完成時觸發此回撥,“讀特徵”一節會介紹
     }

     @Override
     public void onCharacteristicWrite(BluetoothGatt gatt,
         final BluetoothGattCharacteristic characteristic, final int status) {
           //gatt:GATT客戶端
           //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常
           //characteristic:被寫的特徵

           //當對特徵的寫操作完成時觸發此回撥,“寫特徵”一節會介紹
     }

     @Override
     public void onCharacteristicChanged(BluetoothGatt gatt,
         final BluetoothGattCharacteristic characteristic) {
           //gatt:GATT客戶端
           //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常
           //characteristic:特徵值改變的特徵

           //當特徵值改變時觸發此回撥,“開啟通知”一節會介紹
     }

     @Override
     public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
         int status) {
           //gatt:GATT客戶端
           //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常
           //descriptor:被讀的descriptor

           //當對descriptor的讀操作完成時觸發
     }

     @Override
     public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
         int status) {
           //gatt:GATT客戶端
           //status:此次操作的狀態碼,返回0時代表操作成功,返回其他值就是各種異常
           //descriptor:被寫的descriptor

           //當對descriptor的寫操作完成時觸發,“開啟通知”一節會介紹
     }
   };

複製程式碼

當我們呼叫connectGatt方法後會觸發onConnectionStateChange這個回撥,回撥中的status我們用來判斷這次操作的成功與否,newState用來判斷當前的連線狀態。

注意坑來了:

  • 我們在呼叫連線斷開連線這兩方法的時候最好放到主執行緒呼叫,否則可能會在一些手機上遇到奇怪的問題

獲取服務,特徵

當我們連線成功後,GATT客戶端(手機A)可以通過發現方法檢索GATT服務端(手機B)的服務和特徵,以便後面操作使用。

Android BLE 快速上手指南

  //連線成功後掉用發現服務
  gatt.discoverServices();

      //當服務檢索完成後會回撥該方法,檢索完成後我們就可以拿到需要的服務和特徵
      @Override
      public void onServicesDiscovered(BluetoothGatt gatt, int status) {

        //獲取特定UUID的服務
        BluetoothGattService service = gatt.getService(UUID_SERVER);

        //獲取所有服務
        List<BluetoothGattService> services = gatt.getServices();

        if (service!=null){

          //獲取該服務下特定UUID的特徵
          mCharacteristic = service.getCharacteristic(UUID_CHARWRITE);

          //獲取該服務下所有特徵
          List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();

        }
      }
複製程式碼

開啟通知

開啟通知官方的標準做法分兩步:

//官方文件做法
private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
//第一步,開啟手機A(本地)對這個特徵的通知
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
//第二步,通過對手機B(遠端)中需要開啟通知的那個特徵的CCCD寫入開啟通知命令,來開啟通知
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
複製程式碼

由於Android7.0以前版本存在一個bug:對descriptor的寫操作會複用父特徵的寫入型別,這個bug在7.0之後進行了修復,為了提高相容性,我們可以對官方做法稍許修改:

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
//第一步,開啟手機A(本地)對這個特徵的通知
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
//第二步,通過對手機B(遠端)中需要開啟通知的那個特徵的CCCD寫入開啟通知命令,來開啟通知
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
//獲取特徵的寫入型別,用於後面還原
int parentWriteType = characteristic.getWriteType();
//設定特徵的寫入型別為預設型別
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
//還原特徵的寫入型別
characteristic.setWriteType(parentWriteType);
複製程式碼

接下來我們來看看回撥

      @Override
      public void onCharacteristicChanged(BluetoothGatt gatt,
          final BluetoothGattCharacteristic characteristic) {
          //當手機B的通知發過來的時候會觸發這個回撥
      }

      @Override
      public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
          int status) {
        //第二步會觸發此回撥
      }
複製程式碼

注意:

  • 對於有的裝置可能我們只需要執行第一步就能收到通知,但是為了保險起見我們最好兩步都做,以防出現通知開啟無效的情況。
  • 再次強調讀、寫、通知等這些GATT的操作都只能序列的使用,並且在執行下一個任務前必須保證上一個任務已經完成並且成功回撥,否則可能出現後面的任務都阻塞無法進行的情況。
  • 對於開啟通知這個操作觸發onDescriptorWrite時代表任務完成,可以進行下一個GATT操作。

寫特徵

//預設的寫入型別,需要外圍裝置響應
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
//無需裝置響應的寫入型別
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);

mCharacteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(mCharacteristic);


      //寫入特徵回撥
      @Override
      public void onCharacteristicWrite(BluetoothGatt gatt,
          final BluetoothGattCharacteristic characteristic, final int status) {

      }

複製程式碼

寫特徵的用法和前面開啟通知中的寫descriptor類似。

注意:

  • 上面提到了2種寫入型別,他們的區別是:
    • WRITE_TYPE_DEFAULT:寫入資料後需要外圍裝置給出響應才會回撥onCharacteristicWrite
    • WRITE_TYPE_NO_RESPONSE:寫入資料後無需外圍裝置給出響應就會回撥onCharacteristicWrite

如果使用WRITE_TYPE_DEFAULT這種型別寫入,而外圍裝置沒有迴應,那後面的操作都會被阻塞。因此,使用哪種方式需要大家根據自己的外圍裝置決定,大家可以嘗試把示例工程中的這一行註釋掉然後在來寫入資料,結合日誌看看會能更好的理解。

  • 一次寫入最多能寫入20位元組的資料,如果需要寫入更多的資料可以分包多次寫入,或者如果裝置支援更改MTU的話一次最多可以傳輸512位元組。

讀特徵

//讀特徵
mBluetoothGatt.readCharacteristic(mCharacteristic);

//讀特徵的回撥
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
          final BluetoothGattCharacteristic characteristic, final int status) {

}
複製程式碼

讀特徵這個操作沒多少坑,只是需要前面提到的成功回撥以後才算執行完成

斷開連線

private void disConnect(){
    if (mBluetoothGatt!=null){
      //斷開連線
      mBluetoothGatt.disconnect();
      // mBluetoothGatt.close();
    }
  }

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (newState==BluetoothProfile.STATE_DISCONNECTED){
        //關閉GATT客戶端
        gatt.close();
      }
}
複製程式碼

注意:

  • 斷開連線連線一樣最好都在主執行緒執行
  • BluetoothGatt.disConnect()方法和BluetoothGatt.close()方法要成對配合使用,有一點需要注意:如果呼叫disConnect()方法後立即呼叫close()方法(就像上面註釋掉的程式碼那樣)藍芽能正常斷開,只是在onConnectionStateChange中我們就收不到newState為BluetoothProfile.STATE_DISCONNECTED的狀態回撥,因此,可以在收到斷開連線的回撥後在關閉GATT客戶端。
  • 如果斷開連線後沒呼叫close方法,在多次重複連線-斷開之後可能你就再也連不上裝置了。

總結

其實這篇文章除了給大家列舉了一些使用的API和可能遇到的問題外,最主要是要強調一個藍芽操作的節奏,也就是一個任務完成下一個任務才能開始的原則,為了便於大家入門,上面這些使用簡化了很多需要考慮的邏輯,例如:讀、寫、通知一直沒回撥怎麼辦?(可以給這些操作都加上超時時間)等等,不過如果大家按照本文提供的方法使用就已經能避開很多可能會遇到的奇怪問題了。

如果大家需要了解更多更詳細的使用方法,這裡給大家推薦2個開源的ble庫:

  • Android-BLE-Library:NordicSemiconductor官方的Android ble庫。
  • BLELib:我自己封裝的ble庫,大家喜歡的話可以順手star一下。

相關文章