Android BLE 快速上手指南

原文地址java


本文旨在提供一個方便沒接觸過Android上低功耗藍牙(Bluetooth Low Energy)的同窗快速上手使用的簡易教程,所以對其中的一些細節不作過度深刻的探討,此外,爲了讓沒有Ble設備的同窗也能模擬與設備的交互過程,本文還提供了中央設備(central)和外圍設備(peripheral)的示例代碼,只需2部手機你們就能夠愉快的「左右互搏」了。android

準備工做

角色

上面咱們提到了中央設備(central)和外圍設備(peripheral),在這裏咱們能夠這樣簡單的理解:git

  • 中央設備(central):收到外圍設備發出的廣播信號後能主動發起鏈接的主設備,例如咱們給摩拜單車開鎖時咱們的手機就是做爲中央設備鏈接單車並進行開鎖等一系列操做的,一般狀況下同一時間一臺中央設備只能與最多7臺外圍設備創建鏈接。
  • 外圍設備(peripheral):能被中央設備鏈接的從設備,同一時間外圍設備只能被一箇中央設備鏈接。

:Android從4.3(API Level 18) 開始支持低功耗藍牙,可是剛開始只支持做爲中央設備(central)模式,從 Android 5.0(API Level 21) 開始才支持做爲外圍設備(peripheral)的模式,所以咱們最好使用Android 5.0以上版本的手機進行下面的操做。github

須要的權限

<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 的基本方法的使用。安全

從掃描開始,接下來的這些操做中你可能會遇到各類奇奇怪怪的問題,爲了減小你們踩坑的機率,我會在後面的操做中分享一些可能會遇到的問題和解決方法,有的問題在官方文檔中可能有提到,有的在一些論壇帖子中有說起,還有的一些就是本身的經驗之談。app

打開藍牙

打開藍牙有如下兩種方式:ide

//方法一
    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個掃描的方法:ui

//舊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中回調出的設備的廣播數據須要本身手動解析,這是個比較麻煩的過程。google

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)的服務和特徵,以便後面操做使用。

//鏈接成功後掉用發現服務
  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一下。
相關文章
相關標籤/搜索