與 BLE 裝置互動的第一步是連線到該裝置。具體來說,就是連線至裝置上的 GATT 伺服器。如要連線至 BLE 裝置上的 GATT 伺服器,請使用 connectGatt() 方法。這個方法需要三個參數:Context 物件、autoConnect (布林值,表示是否要在 BLE 裝置可用時立即自動連線),以及 BluetoothGattCallback 的參照:
var bluetoothGatt: BluetoothGatt? = null // ... bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
這會連線至 BLE 裝置託管的 GATT 伺服器,並傳回 BluetoothGatt 執行個體,可用於執行 GATT 用戶端作業。呼叫端 (Android 應用程式) 是 GATT 用戶端。BluetoothGattCallback 用於將結果傳送至用戶端,例如連線狀態,以及任何進一步的 GATT 用戶端作業。
設定繫結服務
在以下範例中,BLE 應用程式提供活動 (DeviceControlActivity),用於連線至藍牙裝置、顯示裝置資料,以及顯示裝置支援的 GATT 服務和特性。根據使用者輸入內容,這項活動會與名為 BluetoothLeService 的 Service 通訊,後者會使用 BLE API 與 BLE 裝置互動。「繫結服務」可讓活動連線至 BluetoothLeService,並呼叫函式連線至裝置,進而處理這項通訊。BluetoothLeService 需要 Binder 實作,為活動提供服務存取權。
class BluetoothLeService : Service() { private val binder = LocalBinder() override fun onBind(intent: Intent): IBinder? { return binder } inner class LocalBinder : Binder() { fun getService(): BluetoothLeService { return this@BluetoothLeService } } }
活動可以使用 bindService() 啟動服務,並傳入 Intent 啟動服務、ServiceConnection 實作監聽連線和中斷連線事件,以及指定其他連線選項的旗標。
class DeviceControlActivity : AppCompatActivity() { private var bluetoothService: BluetoothLeService? = null // Code to manage Service lifecycle. private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected( componentName: ComponentName, service: IBinder ) { bluetoothService = (service as LocalBinder).getService() bluetoothService?.let { bluetooth -> // call functions on service to check connection and connect to devices } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.gatt_services_characteristics) val gattServiceIntent = Intent(this, BluetoothLeService::class.java) bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE) } }
設定 BluetoothAdapter
活動繫結至服務後,服務需要存取 BluetoothAdapter。應檢查裝置上是否有轉接器。如要進一步瞭解 BluetoothAdapter,請參閱「設定藍牙」。以下範例將這段設定程式碼包裝在 initialize() 函式中,該函式會傳回表示成功的 Boolean 值。
private const val TAG = "BluetoothLeService" class BluetoothLeService : Service() { private var bluetoothAdapter: BluetoothAdapter? = null fun initialize(): Boolean { bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter == null) { Log.e(TAG, "Unable to obtain a BluetoothAdapter.") return false } return true } // ... }
Activity 會在其 ServiceConnection 實作中呼叫此函式。如何處理 initialize() 函式傳回的 false 值,取決於您的應用程式。您可以向使用者顯示錯誤訊息,指出目前裝置不支援藍牙作業,或停用任何需要藍牙才能運作的功能。在以下範例中,活動會呼叫 finish(),將使用者送回上一個畫面。
class DeviceControlActivity : AppCompatActivity() { // Code to manage Service lifecycle. private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected( componentName: ComponentName, service: IBinder ) { bluetoothService = (service as LocalBinder).getService() bluetoothService?.let { bluetooth -> if (!bluetooth.initialize()) { Log.e(TAG, "Unable to initialize Bluetooth") finish() } // perform device connection } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } } // ... }
連結裝置
BluetoothLeService 執行個體初始化後,即可連線至 BLE 裝置。Activity 必須將裝置位址傳送至服務,才能啟動連線。服務會先在 BluetoothAdapter 上呼叫 getRemoteDevice(),以存取裝置。如果轉接器找不到該位址的裝置,getRemoteDevice() 會擲回 IllegalArgumentException。
fun connect(address: String): Boolean { bluetoothAdapter?.let { adapter -> try { val device = adapter.getRemoteDevice(address) } catch (exception: IllegalArgumentException) { Log.w(TAG, "Device not found with provided address.") return false } // connect to the GATT server on the device return true } ?: run { Log.w(TAG, "BluetoothAdapter not initialized") return false } }
DeviceControlActivity 會在服務初始化後呼叫這個 connect() 函式。活動需要傳入 BLE 裝置的位址。在以下範例中,裝置位址會以意圖額外資訊的形式傳遞至活動。
// Code to manage Service lifecycle. private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected( componentName: ComponentName, service: IBinder ) { bluetoothService = (service as LocalBinder).getService() bluetoothService?.let { bluetooth -> if (!bluetooth.initialize()) { Log.e(TAG, "Unable to initialize Bluetooth") finish() } // perform device connection deviceAddress?.let { bluetooth.connect(it) } } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } }
宣告 GATT 回呼
Activity 告知 Service 要連線至哪個裝置,且 Service 連線至裝置後,Service 必須連線至 BLE 裝置上的 GATT 伺服器。這個連線需要 BluetoothGattCallback,才能接收連線狀態、服務探索、特徵讀取和特徵通知。
本文著重於連線狀態通知。如要瞭解如何探索服務、讀取特徵,以及要求特徵通知,請參閱「轉移 BLE 資料」。
當裝置的 GATT 伺服器連線變更時,系統會觸發 onConnectionStateChange() 函式。在以下範例中,回呼是在 Service 類別中定義,因此可在服務連線後與 BluetoothDevice 搭配使用。
private val bluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { // successfully connected to the GATT Server } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server } } }
連線至 GATT 服務
宣告 BluetoothGattCallback 後,服務可以使用 connect() 函式中的 BluetoothDevice 物件,連線至裝置上的 GATT 服務。
服務會呼叫 connectGatt() 函式。這需要 Context 物件、autoConnect 布林值旗標和 BluetoothGattCallback。在本範例中,應用程式會直接連線至 BLE 裝置,因此 autoConnect 會傳遞 false。
服務也會新增 BluetoothGatt 屬性,以便在不再需要時關閉連線。
class BluetoothLeService : Service() { // ... private var bluetoothGatt: BluetoothGatt? = null // ... fun connect(address: String): Boolean { bluetoothAdapter?.let { adapter -> try { val device = adapter.getRemoteDevice(address) // connect to the GATT server on the device bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback) return true } catch (exception: IllegalArgumentException) { Log.w(TAG, "Device not found with provided address. Unable to connect.") return false } } ?: run { Log.w(TAG, "BluetoothAdapter not initialized") return false } } }
廣播最新消息
當服務連線至 GATT 伺服器或與其連線中斷時,必須通知活動新狀態。有幾種方法可以達成這個目標。 以下範例使用廣播,將資訊從服務傳送至活動。
服務會宣告函式,用於廣播新狀態。這個函式會接收動作字串,並將其傳遞至 Intent 物件,然後再廣播至系統。
private fun broadcastUpdate(action: String) { val intent = Intent(action) sendBroadcast(intent) }
廣播函式就緒後,服務會在 BluetoothGattCallback 中使用該函式,傳送與 GATT 伺服器的連線狀態資訊。服務會宣告代表 Intent 動作和服務目前連線狀態的常數。
class BluetoothLeService : Service() { private var connectionState = STATE_DISCONNECTED private val bluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { // successfully connected to the GATT Server connectionState = STATE_CONNECTED broadcastUpdate(ACTION_GATT_CONNECTED) } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server connectionState = STATE_DISCONNECTED broadcastUpdate(ACTION_GATT_DISCONNECTED) } } } // ... companion object { const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED" const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" private const val STATE_DISCONNECTED = 0 private const val STATE_CONNECTED = 2 } }
監聽活動更新
服務廣播連線更新後,活動需要實作 BroadcastReceiver。設定活動時註冊這個接收器,並在活動離開畫面時取消註冊。透過監聽服務事件,活動可以根據與 BLE 裝置的目前連線狀態更新使用者介面。
class DeviceControlActivity : AppCompatActivity() { private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { BluetoothLeService.ACTION_GATT_CONNECTED -> { connected = true updateConnectionState(R.string.connected) } BluetoothLeService.ACTION_GATT_DISCONNECTED -> { connected = false updateConnectionState(R.string.disconnected) } } } } override fun onResume() { super.onResume() registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter()) bluetoothService?.let { service -> deviceAddress?.let { address -> val result = service.connect(address) Log.d(TAG, "Connect request result=$result") } } } override fun onPause() { super.onPause() unregisterReceiver(gattUpdateReceiver) } private fun makeGattUpdateIntentFilter(): IntentFilter { return IntentFilter().apply { addAction(BluetoothLeService.ACTION_GATT_CONNECTED) addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED) } } }
在「轉移 BLE 資料」中,BroadcastReceiver 也用於傳達服務探索以及裝置的特徵資料。
關閉 GATT 連線
處理藍牙連線時,其中一個重要步驟是在連線完成後關閉連線。方法是對 BluetoothGatt 物件呼叫 close() 函式。在以下範例中,服務會保留 BluetoothGatt 的參照。當活動與服務解除繫結時,連線會關閉,以免耗盡裝置電池電力。
class BluetoothLeService : Service() { // ... override fun onUnbind(intent: Intent?): Boolean { close() return super.onUnbind(intent) } private fun close() { bluetoothGatt?.let { gatt -> gatt.close() bluetoothGatt = null } } }