[筆記] Android APP BLE範例程式 -- BluetoothLeGatt

前言 

       在 Android 裡面的藍芽和低功耗藍芽 (Bluetooth Low Energy) 控制方式不一樣,這裡有一篇文章[連結]說明傳統藍芽與 BLE 有哪些部份不同。此外在 API 方面,很多低功耗藍芽的函式必須在 API level 18 以上才能使用,也就是 Android 4.3平台以上。上一篇介紹的 BluetoothChat 程式是採用傳統的藍芽控制方式,為了讓開發者認識 BLE 如何控制,Android 官方網站提供一個 BLE Gatt 的範例程式,圖一所示是這個範例程式的專案檔,主要的程式檔為 DeviceScanActivityDeviceControlActivity 和 BluetoothLeService。

圖一:BLE範例程式專案檔


        圖二所示是這個範例的系統方塊圖,從 AndroidManifest.xml 設定檔可得知操作畫面先呼叫 DeviceScanActivity 類別,它衍生自 ListActivity 類別。這 Activity 畫面的 onCreate( ) 函式裡面依舊執行傳統藍芽的檢查:

  1. 檢查本身有無藍芽裝置 (參考前一篇 BluetoothChat 的做法)
  2. 檢查有無 BLE service
  3. 打開藍芽功能
  4. 搜尋 BLE 設備
       當 DeviceScanActivity 創建出來後,畫面右上方會出現選單,這對應到類別的 option 操作。這裡有兩種選擇:start scan 或 stop scan,如下圖的左邊所示。當搜尋後的結果會列在畫面上,操作者可以從中選一 BLE 設備進行連線,如下圖的右邊所示。透過 Intent 方式開啟 DeviceControlActivity 類別,生成這類別過程會開啟 BluetoothLeService 這個藍芽服務。這裡使用了一個技巧,就是 bind service 的方法,我們把這個 service 類別想像成提供在背景持續工作的服務,而且能提供訊息給 UI 畫面。

圖二:BLE範例的系統方塊圖


搜尋 BLE 設備

        搜尋 BLE 設備的方式與傳統藍芽有些不同,第一步都必須取得藍芽配適器 Bluetooth Adapter,不過接下來 BLE 呼叫的函式則不一樣。API分別提供 startLeScan 開始搜尋和 stopLeScan 停止搜尋的函式,再將 callback function 傳入給系統,這樣搜尋的結果就能在 callback 裡面由我們來處理。

        // 開始搜尋 BLE 設備
        mBluetoothAdapter.startLeScan( mLeScanCallback );

        // 停止搜尋 BLE 設備
        mBluetoothAdapter.stopLeScan( mLeScanCallback );

這個 callback function 裡面只有一個 method (名為 onLeScan),我們要 override 這個同名的方法,把搜尋到的設備放到 ListActivity 裡面,如下。系統會告訴我們搜尋到的設備資料、訊號強度、設備廣播的內容...等,其中設備資料包含藍芽位址做為之後連線的參考。

        public void onLeScan( final BluetoothDevice device, int rssi, byte[] scanRecord )
       {
            runOnUiThread( new Runnable( )   // 這個執行緒是為了 UI 畫面顯示
                {
                     @Override
                     public void run( )
                    {
                         mLeDeviceListAdapter.addDevice( device );  // 最主要是取得 BLE 設備的資料
                         mLeDeviceListAdapter.notifyDataSetChanged( );
                     }
                 }
            )
        }

        此外,開始搜尋之後,要如何停止搜尋呢?如果不要求系統停止,它會一直處在搜尋的狀態。這裡用到一個延遲的技巧就是使用 Handler 類別的 postDelayed,利用這個函式設定延遲時間,再停止藍芽搜尋。

        mHandler.postDelayed( new Runnable( )
        {
            @Override
            public void run( )
           {
                mBluetoothAdapter.stopLeScan( mLeScanCallback );
            }
        }, SCAN_PERIOD );  // 設定延遲時間


連線 BLE 設備

        從 DeviceScanActivity 畫面搜尋到的藍芽位址,當使用者點選後,透過 Intent 方式帶出另一個畫面 DeviceControlActivity,同時把藍芽位址資料也傳過去,如圖二所示。創建畫面的同時,也建立 BluetoothLeService 類別 (衍生自 service 類別),這裡利用一個技巧,讓服務在背景執行,然後將畫面與服務 bind 在一起。如下的程式碼:

        Intent gattService = new Intent( this, BluetoothLeService.class );
        bindService( gattService, mServiceConnection, BIND_AUTO_CREATE );

雖然已經將服務與畫面連結起來了,不過如果藍芽服務有任何訊息要通知 UI 畫面,該怎麼辦?此時,畫面需要攔截藍芽服務的訊息,並顯示在 UI 畫面上。因此,我們必須向系統註冊一個 callback 函式用來處理服務的訊息,底下程式碼說明了先設定好欲攔截的訊息,然後將 callback 函式 (mGattUpdateReceiver) 向系統註冊:

        IntentFilter intentFilter = new IntentFilter( );
        intentFilter.addAction( BluetoothLeService.ACTION_GATT_CONNECTED );
        intentFilter.addAction( BluetoothLeService.ACTION_GATT_DISCONNECTED );
        registerReceiver( mGattUpdateReceiver, intentFilter );


        比起傳統的藍芽連線,連線 BLE 設備的方式簡單多了。連線只要呼叫 connectGatt 函式,同時提供 callback function (mGattCallback) 給系統

        BluetoothDevice  device = mBtAdapter.getRemoteDevice( address );
        BluetoothGattCallback  mBluetoothGatt = device.connectGatt( this, false, mGattCallback );

另外,我們需要研究一下這個 BluetoothGattCallback 物件,它提供幾個方法是系統用來通知我們上層呼叫者的。比如說,連線狀態改變、回報 GATT 的服務列表、回報讀或寫屬性、屬性改變....等等方法,我們可以 override 這些方法,設計成自己的處理方法。底下列出狀態改變得程式碼

       // 連線狀態改變
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }
   private void broadcastUpdate(final String action) {
       final Intent intent = new Intent(action);
       sendBroadcast(intent);
   }

當連線狀態發生改變時,系統呼叫我們的 callback 函式中的 onConnectionStateChange 方法。而這個方法會將改變的情況通知 UI 畫面,透過 API 函式 sendBroadcast( ) 將動作與資料傳給畫面 DeviceControlActivity。還記得前面我們向系統註冊了一個 mGattUpdateReceiver 函式,用來攔截藍芽服務的訊息,因此當服務端呼叫 sendBroadcast 時,我們的 callback 就會攔截到並且處理該訊息,比如說更新 UI 畫面的藍芽狀態。


BLE 的服務與特性

        當連線 BLE 之後,除了系統偵測連線狀態改變之外,還有發現設備的服務的功能。系統取得 BLE 的服務資訊後,會呼叫 onServicesDiscovered( ),透過 callback 函式,我們便能攔截到訊息並處理之。下圖是從先前的 BLE 酒精偵測模組所取的服務與特性。

圖三:BLE 酒精偵測模組的服務列表


        這些服務列表在 APP 上都顯示未知的服務,所以我們可以修改一下範例程式,在這些服務與特性的名稱上做些改變,如下面圖四所示。將提供酒精偵測服務的代碼修改成適當的名稱,便於明瞭服務的作用,至少別顯示個妾身不明的東西。然後,偵測的數值放在特性欄位裡面,並且呼叫系統函式,通知系統這個服務與特性必須開啟回報,參考底下的程式碼。如此,APP 便可以收到來自 BLE 設備藉由該服務所傳遞的資訊了。

private BluetoothGatt  mBluetoothGatt;
BluetoothGattCharacteristic  characteristic;
boolean  enabled;
...
mBluetoothGatt.setCharacteristicNotification( characteristic, enabled );
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG) );
descriptor.setValue( BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE );
mBluetoothGatt.writeDescriptor( descriptor );
圖四:修改後的服務列表

留言

Unknown說…
想請問一下如果是用BluetoothAdapter.LeScanCallback 要怎麼取得UUID?
他回傳的是 (device rssi scanrecord)
而scanrecord是byte[]型態的
裡面像是所有的東西都塞在裡面
我該怎麼只把我需要的UUID之類的資訊抓出來呢?
Unknown說…
想請問可以更換toolbar風格嗎
試了好久都無法
Unknown說…
救命好文啊...感謝您的細心筆法
Unknown說…
路過
對,那裡面有 device rssi scanrecord
@Overrid
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord)
然後這四個東西算比較重要的
device.getName(), device.getAddress(), device.getBondState(),rssi
你說的 UUID 就是 device.getAddress()
Unknown說…
此留言已被作者移除。
匿名說…
https://havincy.github.io/blog/post/BluetoothLowEnergyOnAndroid/

這裡面有講解關於UUID的判讀方法,個人覺得寫得很好懂

此網誌的熱門文章

[筆記] Raspberry Pi 樹莓派的軟體開發

[應用] 在 ESP32 Audio 開發板的 VoIP 範例

[筆記] ESP32 在 VS Code 開發環境的編譯與除錯

[筆記] Android APP 藍芽範例說明 -- BluetoothChat

[筆記] Visual Studio 遠端偵錯的設定步驟

[應用] STM32 DFU (Device Firmware Upgrade)