android藍芽BLE(二) —— 通訊

大棋發表於2019-05-01

android BLE系列:

android藍芽BLE(一) —— 掃描
android藍芽BLE(二) —— 通訊

一、藍芽基礎協議

    想了解藍芽通訊之前,需要先了解藍芽兩個最基本的協議:GAP 和 GATT。

1、GAP(Generic Access Profile)簡介

    GAP是通用訪問配置檔案的首字母縮寫,主要控制藍芽連線和廣播。GAP使藍芽裝置對外界可見,並決定裝置是否可以或者怎樣與其他裝置進行互動。

    GAP定義了多種角色,但主要的兩個是:中心裝置 和 外圍裝置。

        中心裝置:可以掃描並連線多個外圍裝置,從外設中獲取資訊。

        外圍裝置:小型,低功耗,資源有限的裝置。可以連線到功能更強大的中心裝置,併為其提供資料。

2、GAP廣播資料

    GAP 中外圍裝置通過兩種方式向外廣播資料:廣播資料 和 掃描回覆。 每種資料最長可以包含 31 byte。

    廣播資料是必需的,因為外設必需不停的向外廣播,讓中心裝置知道它的存在。

    掃描回覆是可選的,中心裝置可以向外設請求掃描回覆,這裡包含一些裝置額外的資訊。

android藍芽BLE(二) —— 通訊
    外圍裝置會設定一個廣播間隔。每個廣播間隔中,它會重新傳送自己的廣播資料。廣播間隔越長,越省電,同時也不太容易掃描到。

3. 廣播的網路拓撲結構

    外設通過廣播自己讓中心裝置發現自己,並建立 GATT 連線,從而進行更多的資料交換。但有些情況是不需要連線的,只要外設廣播自己的資料即可。目的是讓外圍裝置,把自己的資訊傳送給多箇中心裝置。因為基於 GATT 連線的方式的,只能是一個外設連線一箇中心裝置。

android藍芽BLE(二) —— 通訊

4. GATT(Generic Attribute Profile)簡介

    GATT配置檔案是一個通用規範,用於在BLE鏈路上傳送和接收被稱為“屬性”的資料塊。目前所有的BLE應用都基於GATT。

    BLE裝置通過叫做 ServiceCharacteristic 的東西進行通訊

    GATT使用了 ATT(Attribute Protocol)協議,ATT 協議把 Service, Characteristic對應的資料儲存在一個查詢表中,次查詢表使用 16 bit ID 作為每一項的索引。

    GATT 連線是獨佔的。也就是一個 BLE 外設同時只能被一箇中心裝置連線。一旦外設被連線,它就會馬上停止廣播,這樣它就對其他裝置不可見了。當外設與中心裝置斷開,外設又開始廣播,讓其他中心裝置感知該外設的存在。而中心裝置可同時與多個外設進行連線。

android藍芽BLE(二) —— 通訊

5.GATT 通訊

    中心裝置和外設需要雙向通訊的話,唯一的方式就是建立 GATT 連線。

    GATT 通訊的雙方是 C/S 關係。外設作為 GATT 服務端(Server),它維持了 ATT 的查詢表以及 service 和 characteristic 的定義。中心裝置是 GATT 客戶端(Client),它向 外設(Server) 發起請求來獲取資料。

6.GATT 結構

android藍芽BLE(二) —— 通訊

Profile:並不是實際存在於 BLE 外設上的,它只是一個被 Bluetooth SIG 或者外設設計者預先定義的 Service 的集合。例如心率Profile(Heart Rate Profile)就是結合了 Heart Rate Service 和 Device Information Service。

Service:包含一個或者多個 Characteristic。每個 Service 有一個 UUID 唯一標識。

Characteristic: 是最小的邏輯資料單元。一個Characteristic包括一個單一value變數和0-n個用來描述characteristic變數的Descriptor。與 Service 類似,每個 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一標識。

    實際開發中,和 BLE 外設打交道,主要是通過 Characteristic。可以從 Characteristic 讀取資料,也可以往 Characteristic 寫資料,從而實現雙向的通訊。

     UUID 有 16 bit 、32bit 和 128 bit 的。16 bit 的 UUID 是官方通過認證的,需要花錢購買。 Bluetooth_Base_UUID定義為 00000000-0000-1000-8000-00805F9B34FB

    若16 bit UUID為xxxx,轉換為128 bit UUID為0000xxxx-0000-1000-8000-00805F9B34FB
    若32 bit UUID為xxxxxxxx,轉換為128 bit UUID為xxxxxxxx-0000-1000-8000-00805F9B34FB

二、中心裝置與外設通訊

簡單介紹BLE開發當中各種主要類和其作用:

        BluetoothDeivce:藍芽裝置,代表一個具體的藍芽外設。
        BluetoothGatt:通用屬性協議,定義了BLE通訊的基本規則和操作
        BluetoothGattCallback:GATT通訊回撥類,用於回撥的各種狀態和結果。
        BluetoothGattService:服務,由零或多個特徵組構成。
        BluetoothGattCharacteristic:特徵,裡面包含了一組或多組資料,是GATT通訊中的最小資料單元。
        BluetoothGattDescriptor:特徵描述符,對特徵的額外描述,包括但不僅限於特徵的單位,屬性等。

1、連線

    對掃描到的藍芽可以用集合形式進行快取,也可只儲存其mac地址,儲存到字符集合中,用於後續的連線。

1.1、根據mac地址獲取到BluetoothDeivce用於連線

BluetoothManager bluetoothmanager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
 mBluetoothAdapter = bluetoothmanager.getAdapter();
 //獲取藍芽裝置物件進行連線
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddressStr)
複製程式碼

1.2、藍芽gatt回撥

    實現BluetoothGattCallBack類,監聽藍芽連線過程中各種回撥的監聽。

    藍芽Gatt回撥方法中都不可以進行耗時操作,需要將其方法內進行的操作丟進另一個執行緒,儘快返回。

//定義子執行緒handle,用於在BluetoothGattCallback中回撥方法中的操作拋到該執行緒工作。
private Handler mHandler;
//定義handler工作的子執行緒
private HandlerThread mHandlerThread;

初始化handler
mHandlerThread = new HandlerThread("daqi");
mHandlerThread.start();
//將handler繫結到子執行緒中
mHandler = new Handler(mHandlerThread.getLooper());


//定義藍芽Gatt回撥類
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

        //連線狀態回撥
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            // status 用於返回操作是否成功,會返回異常碼。
            // newState 返回連線狀態,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED
            
            //操作成功的情況下
            if (status == BluetoothGatt.GATT_SUCCESS){
                //判斷是否連線碼
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                
                }else if(newState == BluetoothProfile.STATE_DISCONNECTED){
                    //判斷是否斷開連線碼
                    
                }
            }else{
                //異常碼
                
            }
        }
        
        //服務發現回撥
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
        }

        //特徵寫入回撥
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
        }
        
        //外設特徵值改變回撥
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
        }
        
        //描述寫入回撥
        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
        }
    }
複製程式碼

1.3、連線裝置

    呼叫BluetoothDevice#connectGatt()進行ble連線,第二個引數預設選擇false,不自動連線。並定義BluetoothGatt變數,儲存BluetoothDevice#connectGatt()返回的物件。

android藍芽BLE(二) —— 通訊

//定義Gatt實現類
private BluetoothGatt mBluetoothGatt; 

//建立Gatt回撥
private BluetoothGattCallback mGattCallback = new daqiBluetoothGattCallback();
//連線裝置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
            false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
}
複製程式碼

1.4、連線異常處理
    藍芽連線時,不一定百分百連線成功。連線出錯時,會返回異常碼進行錯誤描述。
    對於大多數異常碼,可以通過重連來達到連線成功的目的。

錯誤程式碼:
    133 :連線超時或未找到裝置。
    8 : 裝置超出範圍
    22 :表示本地裝置終止了連線

//定義重連次數
private int reConnectionNum = 0;
//最多重連次數
private int maxConnectionNum = 3;

public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //連線狀態回撥
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        // status 用於返回操作是否成功,會返回異常碼。
        //操作成功的情況下
        if (status == BluetoothGatt.GATT_SUCCESS){
            
        }else{
            //重連次數不大於最大重連次數
            if(reConnectionNum < maxConnectionNum){
                //重連次數自增
                reConnectionNum++
                //連線裝置
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext,
                            false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
                } else {
                    mBluetoothGatt = mBluetoothDevice.connectGatt(mContext, false, mGattCallback);
                }
            }else{
                //斷開連線,返回連線失敗回撥
                
            }
        }
    }
    
    //其他回撥方法
}
複製程式碼

2、發現服務

連線成功後,觸發BluetoothGattCallback#onConnectionStateChange()方法。

2.1、發現服務

public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //連線狀態回撥
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        // status 用於返回操作是否成功,會返回異常碼。
        //操作成功的情況下
        if (status == BluetoothGatt.GATT_SUCCESS){
            //判斷是否連線碼
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    //可延遲發現服務,也可不延遲
                    mHandler.post(() ->
                        //發現服務
                        mBluetoothGatt.discoverServices();
                    );
                }else if(newState == BluetoothProfile.STATE_DISCONNECTED){
                    //判斷是否斷開連線碼
                    
                }
        }
    }
    
    //其他回撥方法
}
複製程式碼

當發現服務成功後,會觸發BluetoothGattCallback#onServicesDiscovered()回撥:

//定義需要進行通訊的ServiceUUID
private UUID mServiceUUID = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb");

//定義藍芽Gatt回撥類
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //服務發現回撥
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            mHandler.post(() ->
                //獲取指定uuid的service
                BluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);
                //獲取到特定的服務不為空
                if(gattService != null){
                    
                }else{
                    //獲取特定服務失敗
                    
                }
            );
        }
    }
}
複製程式碼

2.2、發現服務失敗

    發現服務時,會存在發現不了特定服務的情況。或者說,整個BluetoothGatt物件中的服務列表為空。
    BluetoothGatt類中存在一個隱藏的方法refresh(),用於重新整理Gatt的服務列表。當發現不了服務時,可以通過反射去呼叫該方法。

3、修改特徵值並監聽外設特徵值改變

3.1、讀寫特定特徵

//定義需要進行通訊的ServiceUUID
private UUID mServiceUUID = UUID.fromString("0000xxxx-0000-1000-8000-00805f9b34fb");
//定義需要進行通訊的CharacteristicUUID
private UUID mCharacteristicUUID = UUID.fromString("0000yyyy-0000-1000-8000-00805f9b34fb");


//定義藍芽Gatt回撥類
public class daqiBluetoothGattCallback extends BluetoothGattCallback{

    //服務發現回撥
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        if (status == BluetoothGatt.GATT_SUCCESS) {
            mHandler.post(() ->
                //獲取指定uuid的service
                BluetoothGattService gattService = mBluetoothGatt.getService(mServiceUUID);
                //獲取到特定的服務不為空
                if(gattService != null){
                    //獲取指定uuid的Characteristic
                    BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(mCharacteristicUUID);
                    //獲取特定特徵成功
                    if(gattCharacteristic != null){
                        //寫入你需要傳遞給外設的特徵值(即傳遞給外設的資訊)
                        gattCharacteristic.setValue(bytes);
                        //通過GATt實體類將,特徵值寫入到外設中。
                        mBluetoothGatt.writeCharacteristic(gattCharacteristic);
                        
                        //如果只是需要讀取外設的特徵值:
                        //通過Gatt物件讀取特定特徵(Characteristic)的特徵值
                        String readValue = mBluetoothGatt.readCharacteristic(gattCharacteristic);
                    }
                }else{
                    //獲取特定服務失敗
                    
                }
            );
        }
    }
}
複製程式碼

    當成功讀取特徵值時,會觸發BluetoothGattCallback#onCharacteristicRead()回撥。

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicRead(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        //獲取讀取到的特徵值
        characteristic.getValue()
    }
}
複製程式碼

    當成功寫入特徵值到外設時,會觸發BluetoothGattCallback#onCharacteristicWrite()回撥。

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicWrite(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        //獲取寫入到外設的特徵值
        characteristic.getValue()
    }
}
複製程式碼

    當寫入完特徵值後,外設會修改自己的特徵值,手機會觸發BluetoothGattCallback#onCharacteristicChanged()方法,進行雙向通訊

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
     if (status == BluetoothGatt.GATT_SUCCESS) {
        //獲取外設修改的特徵值
        String value = characteristic.getValue()
        //對特徵值進行解析
        
    }
}
複製程式碼

4、斷開連線

    斷開連線的操作分為兩步:

        1、mBluetoothGatt.disconnect();
        2、mBluetoothGatt.close();

    呼叫disconnect()後,會觸發手機會觸發BluetoothGattCallback#onConnectionStateChange()的回撥,回撥斷開連線資訊,newState = BluetoothProfile.STATE_DISCONNECTED。但接著馬上呼叫close(),會終止BluetoothGattCallback#onConnectionStateChange()的回撥,可以看情況將兩個進行拆分呼叫,但一般都是成對出現。
    例如:
        需要在外設修改特徵值觸發BluetoothGattCallback#onCharacteristicChanged()時,斷開連線。可以先在BluetoothGattCallback#onCharacteristicChanged()中呼叫disconnect(),並等呼叫BluetoothGattCallback#onConnectionStateChange()回撥,返回斷開連線資訊後,再呼叫close()對Gatt資源進行關閉。

    當和外設進行ble通訊時,如出現任何意外情況,馬上呼叫斷開連線操作。

相關文章