BLE學習

Jason_發表於2018-03-06

android 18(4.3)引入BLE(低功耗藍芽)全稱Bluetooth Low Energy的核心功能API,應用程式通過這些API可以完成 掃描裝置  連結裝置 查詢服務  讀寫characteristics(特徵值)等操作。

Android BLE 使用的藍芽協議是 GATT 協議,有關該協議的詳細內容可以參見藍芽官方文件。以下我引用一張官網的圖來大概說明 Android 開發中我們需要了解的一些 Bluetooth Low Energy 的專業術語

BLE學習

(藍芽協議棧圖)

Bluetooth4.0結構圖BLE學習

Service

一個低功耗裝置可以有很多個service,一個service可以理解為裝置的一個功能,而一個低功耗裝置可以理解為一個功能集合,裝置中每一個service都有一個128位的UUID,每個service的UUID都不相同,它作為一個service的唯一標識,藍芽核心規範制定了兩種不同的UUID,一種是基本的UUID,一種16為的UUID用來代替的UUID,所有藍芽技術聯盟定義公用一個基本的UUID,它的基本格式為:0x0000xxxx-0000-1000-8000-00805F9B34FB

為了進一步簡化基本UUID,每一個藍芽技術聯盟定義的屬性有一個唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率測量特性使用0X2A37作為它的16位UUID,因此它完整的128位UUID為:
0x00002A37-0000-1000-8000-00805F9B34FB

Characteristic

在service下面有許多的characteristic(特徵值),每個characteristic都是一個獨立的資料,可以理解為屬性(我們把一個裝置看成一個project,每個service相當於這個project中的class, 而characteristic就是每個class中的屬性),characteristic和service相同,每個characteristic都有一個標識的UUID,在Android開反中,藍芽建立連線後我們向裝置的傳送資料,其實就是改變characteristic的value欄位的值,外圍設定傳送資料給手機就是在監聽這些characteristic的value有沒有發生變化,如果發生了變化手機BLE的API就會產生回撥。

API簡介(https://developer.android.com/reference/android/bluetooth/BluetoothA2dp.html)

BluetoothAdapter

bluetoothAdapter代表本中心裝置(如:手機),它擁有基本的藍芽操作 掃描 使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)例項化一個 BluetoothDevice 用於連線藍芽裝置的操作等等。

BluetoothDevice

代表一個遠端藍芽裝置。這個類可以讓你連線所代表的藍芽裝置或者獲取一些有關它的資訊,例如它的名字,地址和繫結狀態等等。

BluetoothGatt

bluetoothGatt是整個藍芽連線通訊中最為重要的類,它主要用於傳送和接受資料。

BluetoothGattService

bluetoothService通過(bluetoothGatt#getServices())獲得,如果當前服務不可見返回null,buletoothService發現過程為非同步(這是一個耗時過程,掃描也相同),我們可以通過個BluetoothService來獲取characteristic(單個獲取getCharacteristic(UUID)獲取全部getCharacteristics())。

BluetoothGattCharacteristic

通過改變這個BluetoothGattCharacteristic的value來實現裝置與平臺之間的相互通訊。

BluetoothSocket

代表藍芽socket的介面(類似TCP的Socket)。這是允許一個應用程式跟另一個藍芽裝置通過輸入流和輸出流進行資料交換的連線點。

BluetoothServerSocket

代表一個開啟的監聽傳入請求的服務介面(類似於TCP的ServerSocket)。為了連線兩個Android裝置,一個裝置必須用這個類開啟一個服務介面。當遠端藍芽裝置請求跟本裝置建立連線請求時,BluetoothServerSocket會在連線被接收時返回一個被連線的BluetoothSocket物件。

BluetoothHeadset

提供對使用藍芽耳機的行動電話的支援。它同時包含了Bluetooth Headset和Hands-Free(v1.5)的配置

BluetoothA2dp

定義如何把高品質的音訊通過藍芽連線從一個裝置流向另一個裝置。“A2DP”是Advanced Audio Distribution Profile的縮寫。 

BluetoothProfile.ServiceListener

BluetoothProfile IPC客戶端連線或斷開服務的通知介面(它是執行特俗配置的內部服務)。

Android Bluetooth開發流程

一許可權申請

<uses-permission android:name="android.permission.BLUETOOTH"/> 使用藍芽所需要的許可權
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用掃描和設定藍芽的許可權(申明這一個許可權必須申明上面一個許可權)
複製程式碼

在Android5.0之前,是預設申請GPS硬體功能的。而在Android 5.0 之後,需要在manifest 中申明GPS硬體模組功能的使用。

<uses-feature android:name="android.hardware.location.gps" />
複製程式碼

在Android6.0後還需要位置許可權,如果沒有位置許可權掃描功能是不能使用的(其它藍芽操作例如連線藍芽裝置和寫入資料不受影響)

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
複製程式碼

二初始化藍芽介面卡

BluetoothManage是系統的服務所以一般使用:context.getSystemService(Context.BLUETOOTH_SERVICE)l來獲取一個BluetoothManage。

BluetoothAdapter adapter = blurtoothManage.getAdapter();//獲取藍芽介面卡

如果返回的adapter == null者說明該裝置部支援藍芽

adapter.isEnable()返回true則代表藍芽是開啟的,false這表示藍芽沒有開啟需要我們去開啟藍芽。

開啟藍芽(隱式意圖,彈窗告知使用者並請求開啟藍芽):
 Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);

三掃描周圍裝置

在bluetoothAdapter中我們可以看到有兩個掃描的方法:

//掃描指定UUID的裝置
boolean    startLeScan(UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback)
複製程式碼

//掃描周圍所有可用的藍芽裝置 boolean startLeScan(BluetoothAdapter.LeScanCallback callback)

例:

//掃描到裝置後的回掉
final BluetoothAdapter.LeScanCallback callback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
        bluetoothDeviceArrayList.add(device);
        Log.d(TAG, "run: scanning...");
    }
};

mBluetoothAdapter.startLeScan(callback);複製程式碼

在LeScanBack的回撥中有三個引數,第一個BluetoothDevice代表平臺所掃描到的裝置,第二個rssi代表裝置訊號的強弱,數值越大訊號越強。通過訊號值我們可以大概計算出藍芽裝置離手機的距離。計算公式為:d = 10^((abs(RSSI) - A) / (10 * n));第三個引數是藍芽廣播出來的廣告資料。當執行上面的程式碼之後,一旦發現藍芽裝置,LeScanCallback 就會被回撥,直到 stopLeScan 被呼叫。出現在回撥中的裝置會重複出現,所以如果我們需要通過 BluetoothDevice 獲取外圍裝置的地址手動過濾掉已經發現的外圍裝置。

停止掃描

void    stopLeScan(BluetoothAdapter.LeScanCallback callback)
//這裡的callback必須和statLeScan()的callback是同一個。複製程式碼

由於掃描周圍裝置是一個比較耗費記憶體的事情,所以在找到我們需要的裝置時應該關閉掃描功能。

四連結裝置

打我們掃描到裝置後,我們通過BluetoothDevice.connectGatt(this, false, callback)連結遠端裝置引數一上下文 第二個。如果設定為 true, 表示如果裝置斷開了,會不斷的嘗試自動連線。設定為 false 表示只進行一次連線嘗試。 三需要一個BluetoothGattCallback的連結回撥。

當連結成功後會回撥BluetoothGattCalbackl的onConnectionStateChange 方法,返回當前的連結狀態。

void    onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
//status代表執行連結操作是否成功BluetoothGatt.GATT_SUCCESS代表成功執行了連結操作
//newState是最新的連結狀態
//BluetoothGatt.STATE_CONNECTING連結中
//BluetoothGatt.STATE_CONNECTED連線成功
//BluetoothGatt.STATE_DISCONNECTING//正在斷開連線
//BluetoothGatt.STATE_DISCONNECTED斷開連線複製程式碼

五發現服務

只有成功連結後才可以做這一步操作,當成功連結後使用BluetoothGatt呼叫discoverServices()發現服務,當成功執行發現服務的操作後回毀掉BluetoothGattCallback中OnServicesDiscovered(BluetoothGatt gatt, int status)方法,當status等於BluetoothGatt.GATT_SUCCESS是代表裝置與平臺之間的通道正式打通,可以相互開始通訊了。
現在我們已經成功建立了平臺與裝置之間的通道,我們使用BluetoothGatt.getServices()來獲取裝置中的所有BluetoothGattService(服務),並使用bluetoothGattService.getCharacteristics()來獲取每個服務中特徵值,也可以通過 BluetoothGattCharactristic#readCharacteristic 方法可以通知系統去讀取特定的資料。如果系統讀取到了藍芽裝置傳送過來的資料就會調BluetoothGattCallback#onCharacteristicRead 方法。通過 BluetoothGattCharacteristic#getValue 可以讀取到藍芽裝置的資料。

例:

@Override
public void onCharacteristicRead(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {

    Log.d(TAG, "callback characteristic read status " + status
            + " in thread " + Thread.currentThread());
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "read value: " + characteristic.getValue());
    }

}


// 讀取資料
BluetoothGattService service = gattt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
gatt.readCharacteristic();
複製程式碼

寫入資料

和讀取資料一樣,在執行寫入資料前需要獲取到 BluetoothGattCharactristic。接著執行一下步驟。

呼叫 BluetoothGattCharactristic#setValue 傳入需要寫入的資料(藍芽最多單次1支援 20 個位元組資料的傳輸,如果需要傳輸的資料大於這一個位元組則需要分包傳輸)。

呼叫 BluetoothGattCharactristic#writeCharacteristic 方法通知系統非同步往裝置寫入資料

系統回撥 BluetoothGattCallback#onCharacteristicWrite 方法通知資料已經完成寫入。

此時,我們需要執行 BluetoothGattCharactristic#getValue 方法檢查一下寫入的資料是否我們需要傳送的資料,如果不是按照專案的需要判斷是否需要重發。

@Override
public void onCharacteristicWrite(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {
    Log.d(TAG, "callback characteristic write in thread " + Thread.currentThread());
    if(!characteristic.getValue().equal(sendValue)) {
        // 執行重發策略
        gatt.writeCharacteristic(characteristic);
    }
}

//往藍芽資料通道的寫入資料
BluetoothGattService service = gattt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);
複製程式碼

註冊監聽實現實時讀取藍芽裝置資料

app中通常要獲取裝置中characteristic的變化通知,我們需要如何為一個裝置新增一個實時監聽呢?

mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);

BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
複製程式碼

值得注意的是,除了通過 BluetoothGatt#setCharacteristicNotification 開啟 Android 端接收通知的開關,還需要往 Characteristic 的 Descriptor 屬性寫入開啟通知的資料開關使得當硬體的資料改變時,主動往手機傳送資料。

斷開連線

當我們連線藍芽裝置完成一系列的藍芽操作之後就可以斷開藍芽裝置的連線了。通過 BluetoothGatt#disconnect 可以斷開正在連線的藍芽裝置。當這一個方法被呼叫之後,系統會非同步回撥 BluetoothGattCallback#onConnectionStateChange 方法。通過這個方法的 newState 引數可以判斷是連線成功還是斷開成功的回撥。

由於 Android 藍芽連線裝置的資源有限,當我們執行斷開藍芽操作之後必須執行 BluetoothGatt#close 方法釋放資源。需要注意的是通過 BluetoothGatt#close 方法也可以執行斷開藍芽的操作,不過 BluetoothGattCallback#onConnectionStateChange 將不會收到任何回撥。此時如果執行 BluetoothGatt#connect 方法會得到一個藍芽 API 的空指標異常。所以,我們推薦的寫法是當藍芽成功連線之後,通過 BluetoothGatt#disconnect 斷開藍芽的連線,緊接著在 BluetoothGattCallback#onConnectionStateChange 執行 BluetoothGatt#close 方法釋放資源。

@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status,
                                    final int newState) {
        Log.d(TAG, "onConnectionStateChange: thread "
                + Thread.currentThread() + " status " + newState);

        if (status != BluetoothGatt.GATT_SUCCESS) {
            String err = "Cannot connect device with error status: " + status;
      // 當嘗試連線失敗的時候呼叫 disconnect 方法是不會引起這個方法回撥的,所以這裡
                //   直接回撥就可以了。
            gatt.close();
            Log.e(TAG, err);
            return;
        }

        if (newState == BluetoothProfile.STATE_CONNECTED) {
            gatt.discoverService();
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            gatt.close();
        }
    }
複製程式碼

注意事項

藍芽的寫入操作( 包括 Descriptor 的寫入操作), 讀取操作必須序列化進行. 寫入資料和讀取資料是不能同時進行的, 如果呼叫了寫入資料的方法, 馬上呼叫又呼叫寫入資料或者讀取資料的方法,第二次呼叫的方法會立即返回 false, 代表當前無法進行操作.

Android 連線外圍裝置的數量有限,當不需要連線藍芽裝置的時候,必須呼叫 BluetoothGatt#close 方法釋放資源

藍芽 API 連線藍芽裝置的超時時間大概在 20s 左右,具體時間看系統實現。有時候某些裝置進行藍芽連線的時間會很長,大概十多秒。如果自己手動設定了連線超時時間(例如通過 Handler#postDelay 設定了 5s 後沒有進入 BluetoothGattCallback#onConnectionStateChange 就執行 BluetoothGatt#close 操作強制釋放斷開連線釋放資源)在某些裝置上可能會導致接下來幾次的連線嘗試都會在 BluetoothGattCallback#onConnectionStateChange 返回 state == 133。

所有的藍芽操作使用 Handler 固定在一條執行緒操作,這樣能省去很多因為執行緒不同步導致的麻煩


相關文章