[筆記] Android APP BLE範例程式 -- BluetoothLeGatt
前言
在 Android 裡面的藍芽和低功耗藍芽 (Bluetooth Low Energy) 控制方式不一樣,這裡有一篇文章[連結]說明傳統藍芽與 BLE 有哪些部份不同。此外在 API 方面,很多低功耗藍芽的函式必須在 API level 18 以上才能使用,也就是 Android 4.3平台以上。上一篇介紹的 BluetoothChat 程式是採用傳統的藍芽控制方式,為了讓開發者認識 BLE 如何控制,Android 官方網站提供一個 BLE Gatt 的範例程式,圖一所示是這個範例程式的專案檔,主要的程式檔為 DeviceScanActivity、DeviceControlActivity 和 BluetoothLeService。
圖一:BLE範例程式專案檔
圖二所示是這個範例的系統方塊圖,從 AndroidManifest.xml 設定檔可得知操作畫面先呼叫 DeviceScanActivity 類別,它衍生自 ListActivity 類別。這 Activity 畫面的 onCreate( ) 函式裡面依舊執行傳統藍芽的檢查:
- 檢查本身有無藍芽裝置 (參考前一篇 BluetoothChat 的做法)
- 檢查有無 BLE service
- 打開藍芽功能
- 搜尋 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.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 ); // 設定延遲時間
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 );
當連線狀態發生改變時,系統呼叫我們的 callback 函式中的 onConnectionStateChange 方法。而這個方法會將改變的情況通知 UI 畫面,透過 API 函式 sendBroadcast( ) 將動作與資料傳給畫面 DeviceControlActivity。還記得前面我們向系統註冊了一個 mGattUpdateReceiver 函式,用來攔截藍芽服務的訊息,因此當服務端呼叫 sendBroadcast 時,我們的 callback 就會攔截到並且處理該訊息,比如說更新 UI 畫面的藍芽狀態。
這些服務列表在 APP 上都顯示未知的服務,所以我們可以修改一下範例程式,在這些服務與特性的名稱上做些改變,如下面圖四所示。將提供酒精偵測服務的代碼修改成適當的名稱,便於明瞭服務的作用,至少別顯示個妾身不明的東西。然後,偵測的數值放在特性欄位裡面,並且呼叫系統函式,通知系統這個服務與特性必須開啟回報,參考底下的程式碼。如此,APP 便可以收到來自 BLE 設備藉由該服務所傳遞的資訊了。
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 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 );
圖四:修改後的服務列表
留言
他回傳的是 (device rssi scanrecord)
而scanrecord是byte[]型態的
裡面像是所有的東西都塞在裡面
我該怎麼只把我需要的UUID之類的資訊抓出來呢?
試了好久都無法
對,那裡面有 device rssi scanrecord
@Overrid
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord)
然後這四個東西算比較重要的
device.getName(), device.getAddress(), device.getBondState(),rssi
你說的 UUID 就是 device.getAddress()
這裡面有講解關於UUID的判讀方法,個人覺得寫得很好懂