安卓 BLE 開發詳解


相關概念

  • BR
    Basic Rate,早期的傳統藍牙技術 V1.1, V1.2 版本,傳輸速率為748~810kb/s。

  • EDR
    Enhanced Data Rate,傳統藍牙技術 V2.0, V2.1 版本,優化傳輸速率,減少耗電,速率為1.8M/s~2.1M/s。

  • AMP
    GenericAlternate MAC/PHY,高速藍牙技術,V3.0版本。
    采用交替射頻技術,藍牙模塊僅創建設備間的配對,數據傳輸通過WIFI射頻來完成以達到高速率。
    假如設備某一方沒有內建WIFI模塊,速率將降至 EDR 速率。

  • BLE
    Bluetooth Low Energy,低耗藍牙技術,V4.0版本的新規范,通過三個方式實現超低功耗:
    1.大幅度削減掃描信道
    2.極短的鏈路連接時間
    3.采用長度很短的數據包
    低耗藍牙的芯片有單模和雙模,前者只支持LE技術,后者兼容BR/EDR技術。


1:GATT 協議

  • GATT概述
    GATT(Generic Attributes,通用屬性協議),定義了一種面向 BLE設備 的分層數據結構。
    GATT建立在ATT( Attribute Protocol,通用訪問協議)之上,ATT使用GATT數據定義兩個BLE設備間收發標準消息的方式。
    由于 GATT 是面向 LE 技術的協議,所以在只支持 BR/EDR 技術的設備上無法使用。

  • GATT分層數據結構的層次

    GATT定義了用于BLE設備傳輸數據的標準數據結構,結構主要包括了如上圖所示的:
    1.服務(Service)
    2.特征(Characteristic)
    3.描述符(Descriptor)。

  • 配置文件(Profile)
    配置文件,GATT頂層,該由滿足 配置實例 需要的一個或多個服務組成。

  • 服務(Service)
    服務 由 特征 和 其他服務的引用 組成,擁有固定的 UUID 作為標記值。
    設備的功能主要體現在服務上,每種服務都對應著某一種功能。

    可以到官網上查看服務列表 GATT Services
    通過服務列表中的 Assigned Numbers 可以獲取服務的UUID。

    Assigned Numbers轉換成可用的服務UUID 的方法于文檔 Service Discovery
    簡單來說,就是:

      "服務的Assigned Numbers"-0000-1000-8000-00805F9B34FB
    
  • 特征(Characteristic)
    特征是BLE通信的主體,是一個服務端和客戶端共享的讀寫空間。
    主機在從機上獲取所需的信息,實際就是通過獲取對應的特征的內容進行的。

    特征由屬性值和描述符組成:

    1. 屬性值
      屬性值包括聲明(Declaration),值(Value),一個屬性值最少包括一個聲明和一個值,即是屬性值是特征必選的條目。
    2. 描述符
      特征可以包括零到若干個描述符,可選條目。

    特征信息列表可以查看官方文檔 GATT Characteristics

  • 描述符(Descriptors)
    用于表達 特征 的其他附加信息,如特征值的有效范圍,可讀性描述等信息。

    其中包含了特殊的 CCCD(Client Characteristic Configuration Descriptor, Assigned Number : 0x2902):
    CCCD 可以設置 服務端 在對應特征值發生變化時,是否對 客戶端 進行信息 推送(直接發送信息) 或 提示(發送一個提示并等待回復)。
    當特征包含通知能力時,CCCD為必選項。

    描述符列表可以查看官方文檔 GATT Descriptors


2:Android BLE 相關 API

  • BluetoothAdapter
    藍牙適配器:

    本地設備藍牙適配器,提供基本藍牙功能的工具,例如開啟藍牙發現,查詢配對設備,實例化藍牙設備鏈接,監聽連接請求,掃描設備等。
    基本上說,藍牙適配器是進行藍牙操作的起點。

    獲取BluetoothAdapter實例,在 API 18 及以上的設備,使用:

    BluetoothManager.getAdapter
    

    在API18以下設備使用以下API獲取:

    BluetoothAdapter.getDefaultAdapte
    

    本類線程安全。涉及到的權限為:

    <uses-permission android:name="android.permission.BLUETOOTH"/> 
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    
  • BluetoothDevice
    遠程藍牙設備:

    提供了遠程藍牙設備的基本信息,如名稱,地址,類別,綁定狀態等。
    本質上只是對藍牙硬件地址的簡單包裝。該類的實例不可修改。

    一般來說,通過掃描設備的掃描結果回調中獲取。
    也可以直接通過以下方式獲取:

    /* 使用已知的物理地址作為參數進行連接 */
    BluetoothAdapter.getRemoteDevice(address);
    /* 獲取已適配的藍牙記錄列表 */
    BluetoothAdapter.getBondedDevices();
    
  • BluetoothGatt
    GATT客戶端,GATT協議的公共API,提供了GATT的基本功能,如實現藍牙設備的通信。
    通過掃描支持LE技術的藍牙設備,獲取到 BluetoothDevice,然后通過:

    /* GATT連接操作的回調 */
    BluetoothGattCallback mCallback;
    BluetoothDevice.connectGatt(content, autoConnect, mCallback);
    

    通過設置 BluetoothGattCallback 回調,可以從回調中得到 BluetoothGatt 實例。

  • BluetoothGattCallback
    GATT狀態回調,大部分GATT操作的結果都會通過該類實例回調,包括:

    /* 連接狀態回調,包括連接到服務器 / 從服務器斷開連接 */
    onConnectionStateChange();
    /* 遠程設備發現新服務 */
    onServicesDiscovered();
    /* 特征相關操作的回調 */
    onCharacteristicRead();
    onCharacteristicWrite();
    onCharacteristicChanged();
    

    同時,掃描設備 和 停止掃描 的操作,都需要用到該類的實例。

  • BluetoothGattService
    GATT服務,根據服務的 UUID,嘗試獲取服務實例。

    /* 如果對應的設備支持該服務,則返回一個服務的實例,否則返回空 */
    BluetoothGatt.getService(uuid); 
    
  • BluetoothGattCharacteristic
    GATT特征,實際通信中的數據信息主體。通過以下方法獲取:

    /* 獲取對應UUID的特征 */
    BluetoothGattService.getCharacteristic(uuid);
    /* 獲取服務的特征列表 */
    BluetoothGattService.getCharacteristics();
    

3:Android BLE 開發示例

  • 聲明權限

    一個聲明和兩個基本權限:

    <uses-feature android:name"android.permission.BLUETOOTH_ADMIN"/>
    
    <uses-permission android:name="android.permission.BLUETOOTH"/> 
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 
    

    執行搜索BLE設備的時候,需要使用定位權限。
    而在5.0及以上的版本,需要手動聲明GPS硬件模塊功能的權限:

    <uses-feature android:name="android.hardware.location.gps"/>
    

    而在6.0及以上版本,掃描設備還需要 動態申請 以下權限:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
  • 檢查設備支持性
    如果設備不支持BLE,可以跳過BLE相關操作了。

    boolean checkSupport() {
        return getPackageManager()
                  .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
    }
    
  • 初始化BluetoothAdapter

    private BluetoothAdapter mAdapter;
    
    BluetoothManager bluetoothManager = 
        (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mAdapter = bluetoothManager.getAdapter();
    

    然后檢查藍牙的支持性,及是否已打開藍牙。

    if (mAdapter == null) {
        return;
    }
    
    ... private final static int REQUEST_ENABLE_BT = 1;
    
    if (!mAdapter.isEnabled()) {
        Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(intent, REQUEST_ENABLE_BT);
    }
    
  • 啟動設備掃描

    創建LeScanCallback實例:
    首先需要實現一個 LeScanCallback 實例,掃描結果會通過實例的 onLeScan 方法返回:

    LeScanCallback mCallBack = new LeScanCallback (){ 
         @Override
         public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {}
    }
    

    啟動掃描與停止掃描:

    ··· static int SCAN_TIME = 5_000;
    ··· Handler mHandler = new Handler();
    
    /* 開始掃描:
       由于掃描消耗電量,所以不能一直處于掃描狀態,
       設置掃描一段時間后關閉掃描 */
    mAdapter.startLeScan(mCallBack);
    mHandler.postDelay(()->{
    
       /* 關閉掃描:
        * 注意需要傳入啟動掃描時的 callback對象,否則無效 */
       mAdapter.stopLeScan(mCallBack);
    }, SCAN_TIME);
    

    API 21 及以上時,掃描操作應使用 BluetoothLeScanner

    final ScanCallback callback = new ScanCallback() {};
    final BluetoothLeScanner scanner = mAdapter.getBluetoothLeScanner();
    
    scanner.startScan(new ScanCallback(){});
    mHandler.postDelay(()->{
       scanner.stopScan(scanCallback);
    }, SCAN_TIME);
    
  • 獲取掃描結果
    以 LeScanCallback 的回調方法 onLeScan 分析:

     /**
      * @param device:    識別到的遠程設備
      *
      * @param rssi:      信號強度指示,計數為dB。可以通過:
                           d = 10^((abs(RSSI) - A) / (10 * n)) 
                           計算出距離。A和n根據環境改變,需經實驗測出,
                           給出兩個網上的經驗值:
                           <1>  A: 50 n: 2.5    <2>  A: 59 n: 2.0
      *
      * @param scanRecord:廣播數據和掃描應答數據數據
                           BLE設備在對外廣播中,廣播中會攜帶一些有用的信息。
                           其中包含了 廣播數據 和 掃描應答數據,
                           兩者有效荷載最大都為 31字節(藍牙4),
                           以十六進制格式存儲,可通過 bytesToHex 轉換成可用的字符串。
      */
     void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {}
    

    注意相同的 BluetoothDevice 會重復出現在回調中,所以如果要記錄藍牙列表,需要自行 過濾 重復出現的設備,或更新對應重復出現的設備的信息。
    bytesToHex 參考

  • 連接外圍設備
    通過 BluetoothDevice 的 connectGatt 方法獲取一個 BluetoothGatt 實例。
    connectGatt 有多個重載方法,這里介紹其中最復雜的重載方法:

     /**
      * 以客戶端的身份連接到該設備托管的GATT服務器
      *
      * @param autoConnect:自動連接,設備不可用時會不斷嘗試重連。
      *
      * @param callback:   BluetoothGattCallback實例,用于接收異步回調
      *
      * @param transport:  GATT連接到雙模設備的首選傳輸模式:
      *                     1:TRANSPORT_AUTO   自動選擇 (默認值)
      *                     2:TRANSPORT_BREDR  BR/EDR 傳統藍牙
      *                     3:TRANSPORT_LE     LE 低耗藍牙
      *
      * @param phy:        PHY物理層的模式選擇:
      *                     1:PHY_LE_1M_MASK:
      *                        默認值,LE設備強制要求支持的模式,
      *                        符號速率為1M/s,未編碼。
      *                     2:PHY_LE_2M_MASK:
      *                        符號速率為2M/s,未編碼,
      *                        用于 藍牙5 的 "2x speed" 2倍速率。
      *                     3:PHY_LE_CODED_MASK:
      *                        在數據包中增加糾錯編碼以實現更遠的傳輸范圍,
      *                        以實現 藍牙5 的 "4x range" 4倍范圍。
      *                        使用FEC編碼,根據方案又分為:
      *                        LE Coded S=2:2個編碼位代替原來一個數據位,
      *                                      速率降為 500K/s,傳輸范圍增大2倍;
      *                        LE Coded S=8:8個編碼位代替原來一個數據位,
      *                                      速率降為 125K/s,傳輸范圍增大4倍;
      *                     設置 autoConnect 自動連接時,該項無效
      *
      * @param handler:    傳入一個Handler,以指定回調發生的線程,
      *                     傳入null時,回調將會在一個未指定的后臺線程上進行。
      */
     BluetoothGatt connectGatt(Context context,
                               boolean autoConnect,
                               BluetoothGattCallback callback, 
                               int transport, int phy,
                               Handler handler) { ··· }
    

    一般情況下使用默認值既可,
    注意必須傳入非空的callback,否則會拋出 IllegalArgumentException

    BluetoothDevice.connectGatt(content, autoConnect, callback);
    

    當連接成功時,會回調 callback 的 onConnectionStateChange 方法

    /**
     * GATT客戶端的連接狀態回調
     *
     * @param gatt:    GATT客戶端。
     * @param status:  連接或斷開操作的執行結果, 成功返回 GATT_SUCCESS
     * @param newState:當前的連接狀態:STATE_CONNECTED / STATE_DISCONNECTED
     */
    void onConnectionStateChange(BluetoothGatt gatt, int status, int newState);
    

    status 表示連接操作的結果,只有status為 GATT_SUCCESS 時,newState才是有效值。

    注意一臺安卓設備最多同時連接6個左右的藍牙設備,超出時可能出現:
    status == 133 連接錯誤,
    所以需要注意調用 BluetoothGatt.close() 方法進行資源釋放。
    可參考:Android中BLE連接出現“BluetoothGatt status 133”的解決方法

    當 status == GATT_SUCCESS,且 newState == STATE_CONNECTED 時,表示已成功連接設備,可以進行下一步操作。

  • 發現服務
    在建立連接之后,就可以通過 BluetoothGatt實例 進行發現服務操作,查找設備支持的服務。

    /**
     * 異步操作,發現服務完成時,會回調onServicesDiscovered()方法。
     * 假如發現服務已在啟動狀態中,則返回true
     */
    boolean discoverService();
    

    等待 BluetoothGattCallback 的 onServicesDiscovered() 被回調:

    /**
     * @param gatt:   執行發現服務后的GATT客戶端。
     * @param status: 發現服務的執行結果, 成功返回 GATT_SUCCESS
     */
    void onServicesDiscovered(BluetoothGatt gatt, int status) ;
    

    當 status 返回GATT_SUCCESS,表示與外部設備成功建立 可通信連接
    意味著可以執行如:寫入數據,讀取藍牙設備的數據等 藍牙通信操作了。
    先把獲取到的 BluetoothGatt實例 記錄為 mGatt:

    ··· BluetoothGatt mGatt;
    
    void onServicesDiscovered(BluetoothGatt gatt, int status) {
        mGatt = gatt;
    }
    
  • 獲取服務
    發現服務成功之后,可以通過以下的方法嘗試獲取 BluetoothGattService 實例:

    /* 獲取遠程設備提供的服務列表,
     * 如果未執行發現服務,會返回一個空列表 */
    mGatt.getServices();
    
    /* 通過服務的UUID,獲取指定的服務,
     * 如果遠程設備不支持給定UUID的服務,返回null,
     * 如果遠程設備存在多個給定UUID的服務實例,則返回第一個實例 */
    mGatt.getService(UUID);
    

    獲取到 BluetoothGattService 之后,就可以通過獲取服務的特征進行讀寫。

  • 特征的讀寫數據
    前面介紹了,通信主體實際上是 特征,要進行讀寫操作,其實就是在操作特征里的屬性詞條,所以要先通過 服務 獲取 特征:

    /* 假設 service 是從上一步獲取到的一個 BluetoothGattService 實例*/
    ··· BluetoothGattService service;
    
    /* 獲取該服務的特征列表 */
    service.getCharacteristics();
    
    /* 通過特征的UUID,獲取指定的特征,
     * 如果沒有找到給定UUID的特征,返回null,
     * 如果服務中存在多個給定UUID的特征,則返回第一個實例 */
    service.getCharacteristic(UUID);
    

    獲取到了特征之后,就可以通過上面獲取到的 mGatt 讀寫信息:

    /* 上一步獲取的 BluetoothGattCharacteristic 實例 */
    ··· BluetoothGattCharacteristic characteristic;
    
    /* 從關聯的遠程設備讀取請求的特征,
     * 異步操作,請求發起成功則返回true,讀取完成會回調:
     * BluetoothGattCallback.onCharacteristicRead() */
    mGatt.readCharacteristic(characteristic);
    
    /* 將給定的特征及其值寫入關聯的遠程設備,
     * 異步操作,請求發起成功則返回true,寫入完成會回調:
     * BluetoothGattCallback.onCharacteristicWrite() */
    mGatt.writeCharacteristic(characteristic);
    

    讀寫操作都是異步操作,方法返回的是請求是否成功,請求結果都會回調 BluetoothGattCallback 的方法:

    /**
     * 讀操作的回調
     * @param characteristic:  讀取后的特征
     * @param status:          讀取結果,成功為 GATT_SUCCESS
     */
    void onCharacteristicRead(BluetoothGatt gatt, 
                              BluetoothGattCharacteristic characteristic,
                              int status) { ··· }
    
    /**
     * 寫操作的回調
     * @param characteristic:  寫入后的特征
     *                          注意:這里返回的特征,為設備當前的特征, 
     *                          應該在該回調中,應對比該特征的內容是否符合期望值,
     *                          如果與期望值不同,應該選擇重發或終止寫入。                    
     * 
     * @param status:          寫入結果,成功為 GATT_SUCCESS
     */
    void onCharacteristicWrite(BluetoothGatt gatt, 
                              BluetoothGattCharacteristic characteristic,
                              int status) { ··· }
    

    寫數據的時候要注意,需要對比返回的特征和寫入的特征,判斷是否寫入成功或者產生了異常,選擇繼續寫入或者重寫,或者放棄操作。

  • 描述符的讀寫數據
    讀寫方式與 特征 的 讀寫方式基本一致,不再過多描述 :

    /* 獲取描述符 */
    ··· BluetoothGattCharacteristic characteristic;
    characteristic.getDescriptors();
    characteristic.getDescriptor(UUID);
    
    /* 通過 mGatt 讀寫數據
     * 同樣,寫操作需要做寫入結果校驗 */
    ··· BluetoothGattDescriptor descriptor;
    mGatt.readDescriptor(descriptor);
    mGatt.writeDescriptor(descriptor);
    
    /* 結果回調 */
    void onDescriptorRead(BluetoothGatt gatt,
                           BluetoothGattDescriptor descriptor,
                           int status) { ··· }
    void onDescriptorWrite(BluetoothGatt gatt,
                           BluetoothGattDescriptor descriptor,
                           int status) { ··· }
    
  • 讀寫數據需要注意的問題

    寫入數據量:
    每次寫操作的時候,無論是 特征 或者 描述符,一般來說最大只能設置 20個字節 的數據。
    這是因為ATT協議中,最大傳輸單元MTU的默認大小為23字節,其中3字節用于ATT協議的控制數據,所以GATT可用的數據大小默認為剩余的20字節。

    ATT的MTU最大值為512,在API 21及以上的安卓平臺,可以通過以下方法嘗試改變MTU的大小:

    ··· int mMtu;
    
    /* 請求變更MTU的大小 */
    BluetoothGatt.requestMtu(mMtu);
    
    /* 請求結果通過 BluetoothGattCallback 回調 
     * 當statue返回為 GATT_SUCCESS 時,表示變更成功
     * 變更成功后,可以使用(mMtu - 3)的大小傳輸數據*/
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {}
    

    無法改變的時候,超過20字節的數據,進行分包發送(BLE服務端需要支持)。

    讀寫間隔:
    讀寫操作都是隊列操作,需要等待操作結果返回后,才能進行下次操作,若當次操作未完成,下次操作調用時,將直接返回操作啟用失敗。

    寫入操作時,需等待服務器的確認信息,即寫入回調,再進行下次寫入操作。
    當寫入類型設置為 不需要接收服務器確認信息(PROPERTY_WRITE_NO_RESPONSE)以加快傳輸速度時,兩次操作之間應保留 80ms ~ 100ms 或以上的延時。

  • 數據變更通知
    前面說到ATT支持通知,一些特征在值發生變化時,可以主動向申請了監聽數據變化的客戶端推送通知或指示(不帶數據)。
    開啟特征的監聽,需要進行兩步操作:

    設置特征信息推送

    /**
     * 啟用或禁用給定特征的通知或指示
     * @param characteristic:  需要進行操作的特征
     * @param enable :         開啟或關閉
     */
    BluetoothGatt.setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
                                                boolean enable);
    

    寫入CCCD
    雖然開啟了特征的信息推送,但假如特征本身禁用了通知和指示,則不會有更新推送。
    前面提到了一個特殊的標識符CCCD,用于控制特征的消息推送。需要對特征的CCCD描述符進行操作,將其值置為 1 / 2,才能開啟對應的 通知 / 指示 功能。

    /* 設置特征信息推送 */
    ··· BluetoothGattCharacteristic characteristic;
        mGatt.setCharacteristicNotification(characteristic,true);
    
    /* CCCD 的UUID */
    private UUID ID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");  
    
    /* 獲取CCCD */
    BluetoothGattDescriptor cccd = characteristic.getDescriptor(ID_CCCD);
    
    /* 設置推送通知,參考值為:
     * BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE:   通知
     * BluetoothGattDescriptor.ENABLE_INDICATION_VALUE:     指示
     * BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE:  關閉
     */  
    cccd.setValue(參考值);
    /* 寫入CCCD */
    mGatt.writeDescriptor(descriptor);
    

    以上操作完成后,即開啟對應特征的更新推送了。

    接收推送
    更新推送會回調BluetoothGattCallback的onCharacteristicChanged()方法:

    /**
     * 特征變更推送觸發的回調
     * @param gatt:            特征 關聯的 BluetoothGatt 實例
     * @param characteristic:  更新后的 特征
     */
    void onCharacteristicChanged(BluetoothGatt gatt,
                                 BluetoothGattCharacteristic characteristic)
    
  • 關閉客戶端
    用完的東西總是要收拾好。

    斷開連接:

    /* 斷開當前連接,如果正在連接中,則取消連接操作 */
    BluetoothGatt.disconnect();
    

    斷開連接操作后,結果回調 onConnectionStateChange() 方法,應該通過回調返回的結果 status 和 newState 判斷是否成功斷開。

    關閉Gatt客戶端:
    成功斷開連接之后(甚至是斷開失敗),應該調用 BluetoothGatt 的close() 方法關閉客戶端釋放資源。
    安卓同時連接遠程設備的資源極其有限,在所以任何情況不再需要連接遠程設備時,都要使用BluetoothGatt 的 close() 方法釋放資源。


參考文章:
藍牙技術基礎知識學習
藍牙核心技術概述
GATT協議及藍牙核心系統結構
Android BLE的總結
Android BLE 藍牙開發入門

更具體的藍牙技術說明請查看官方網站
Bluetooth Technology Website

歡迎留言,歡迎關注,會持續更新 安卓開發 中遇到的問題和技術上的一些自我總結。
如有錯誤,歡迎指出。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 因為自己的項目中有用到了藍牙相關的功能,所以之前也斷斷續續地針對藍牙通信尤其是BLE通信進行了一番探索,整理出了一...
    陳利健閱讀 116,451評論 172 297
  • BLE 與經典藍牙的區別 BLE 的 Kotlin 下實踐 BluetoothGattCallback 不回調異常...
    chauI閱讀 11,257評論 1 7
  • 初識低功耗藍牙 Android 4.3(API Level 18)開始引入Bluetooth Low Energy...
    JBD閱讀 113,003評論 46 342
  • 我聽到爆竹的雷動砰砰砰磅磅磅在大年初六在京郊大地的夜晚 響得高亢響得透亮響得正義凜然 聽得我心虛聽得鬼心虛聽得人類...
    藍柿閱讀 283評論 7 1
  • 又來到這棵老樹下,密密蓬蓬的枝丫向四周撐開披撒著,滿目的蔥綠中綴滿了粉色的花,每朵花都像是一團小火焰,細細的花絲嬌...
    古董碎片閱讀 501評論 1 5