連線至 GATT 伺服器

與 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 服務和特徵。根據使用者輸入內容,這項活動會與 Service 稱為 BluetoothLeService 的服務通訊,該服務會透過 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
    }

    // ...
}

活動會在 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 裝置。活動必須將裝置地址傳送至服務,才能啟動連線。服務會先在 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 回呼

活動告知服務要連線至哪個裝置,且服務連線至裝置後,服務必須連線至 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 裝置,因此會傳遞 false 做為 autoConnect

系統也會新增 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
        }
    }
}