Conectar-se ao servidor GATT

A primeira etapa para interagir com um dispositivo BLE é se conectar a ele. Mais especificamente, a conexão com o servidor GATT no dispositivo. Para se conectar a um servidor GATT em um dispositivo BLE, use o método connectGatt(). Esse método usa três parâmetros: um objeto Context, autoConnect (um booleano que indica se é para conectar automaticamente ao dispositivo BLE assim que ele ficar disponível) e uma referência a um BluetoothGattCallback:

var bluetoothGatt: BluetoothGatt? = null
// ...
bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)

Isso se conecta ao servidor GATT hospedado pelo dispositivo BLE e retorna uma instância BluetoothGatt, que pode ser usada para realizar operações do cliente GATT. O caller (o app Android) é o cliente GATT. O BluetoothGattCallback é usado para entregar resultados ao cliente, como status da conexão, bem como outras operações do cliente GATT.

Configurar um serviço vinculado

No exemplo a seguir, o app BLE fornece uma atividade (DeviceControlActivity) para se conectar a dispositivos Bluetooth, mostrar dados do dispositivo e exibir os serviços e características GATT compatíveis com o dispositivo. Com base na entrada do usuário, essa atividade se comunica com um Service chamado BluetoothLeService, que interage com o dispositivo BLE pela API BLE. A comunicação é realizada usando um serviço vinculado, que permite que a atividade se conecte ao BluetoothLeService e chame funções para se conectar aos dispositivos. O BluetoothLeService precisa de uma implementação de Binder que forneça acesso ao serviço para a atividade.

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
        }
    }
}

A atividade pode iniciar o serviço usando bindService(), transmitindo um Intent para iniciar o serviço, uma implementação de ServiceConnection para detectar os eventos de conexão e desconexão e uma flag para especificar opções de conexão adicionais.

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)
    }
}

Configurar o BluetoothAdapter

Depois que o serviço é vinculado, ele precisa acessar o BluetoothAdapter. Ele precisa verificar se o adaptador está disponível no dispositivo. Leia Configurar o Bluetooth para mais informações sobre o BluetoothAdapter. O exemplo a seguir encapsula esse código de configuração em uma função initialize() que retorna um valor Boolean indicando sucesso.

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
    }

    // ...
}

A atividade chama essa função na implementação de ServiceConnection. O processamento de um valor de retorno falso da função initialize() depende do seu aplicativo. Você pode mostrar uma mensagem de erro ao usuário indicando que o dispositivo atual não é compatível com a operação Bluetooth ou desativar os recursos que exigem o Bluetooth para funcionar. No exemplo a seguir, finish() é chamado na atividade para enviar o usuário de volta à tela anterior.

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
        }
    }

    // ...
}

Conectar a um dispositivo

Depois que a instância BluetoothLeService é inicializada, ela pode se conectar ao dispositivo BLE. A atividade precisa enviar o endereço do dispositivo para o serviço para que ele possa iniciar a conexão. O serviço primeiro vai chamar getRemoteDevice() no BluetoothAdapter para acessar o dispositivo. Se o adaptador não conseguir encontrar um dispositivo com esse endereço, getRemoteDevice() vai gerar um 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
    }
}

O DeviceControlActivity chama essa função connect() assim que o serviço é inicializado. A atividade precisa transmitir o endereço do dispositivo BLE. No exemplo a seguir, o endereço do dispositivo é transmitido para a atividade como um extra de intent.

// 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
    }
}

Declarar callback GATT

Depois que a atividade informa ao serviço a qual dispositivo se conectar e o serviço se conecta ao dispositivo, o serviço precisa se conectar ao servidor GATT no dispositivo BLE. Essa conexão exige um BluetoothGattCallback para receber notificações sobre o estado da conexão, a descoberta de serviços, as leituras de características e as notificações de características.

Este tópico se concentra nas notificações de estado da conexão. Consulte Transferir dados BLE para saber como realizar descoberta de serviços, leituras de características e solicitações de notificações de características.

A função onConnectionStateChange() é acionada quando a conexão com o servidor GATT do dispositivo muda. No exemplo a seguir, o callback é definido na classe Service para que possa ser usado com o BluetoothDevice assim que o serviço se conectar a ele.

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
        }
    }
}

Conectar-se ao serviço GATT

Depois que o BluetoothGattCallback é declarado, o serviço pode usar o objeto BluetoothDevice da função connect() para se conectar ao serviço GATT no dispositivo.

A função connectGatt() é usada. Isso requer um objeto Context, uma flag booleana autoConnect e o BluetoothGattCallback. Neste exemplo, o app está se conectando diretamente ao dispositivo BLE. Portanto, false é transmitido para autoConnect.

Uma propriedade BluetoothGatt também é adicionada. Isso permite que o serviço feche a conexão quando ela não for mais necessária.

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
        }
    }
}

Atualizações de transmissão

Quando o servidor se conecta ou se desconecta do servidor GATT, ele precisa notificar a atividade do novo estado. Há várias maneiras de fazer isso. O exemplo a seguir usa transmissões para enviar as informações do serviço para a atividade.

O serviço declara uma função para transmitir o novo estado. Essa função recebe uma string de ação que é transmitida a um objeto Intent antes de ser transmitida para o sistema.

private fun broadcastUpdate(action: String) {
    val intent = Intent(action)
    sendBroadcast(intent)
}

Depois que a função de transmissão estiver implementada, ela será usada no BluetoothGattCallback para enviar informações sobre o estado da conexão com o servidor GATT. As constantes e o estado atual da conexão do serviço são declarados no serviço que representa as ações 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
    }
}

Detectar atualizações na atividade

Depois que o serviço transmitir as atualizações de conexão, a atividade precisará implementar um BroadcastReceiver. Registre esse receptor ao configurar a atividade e cancele o registro quando a atividade sair da tela. Ao detectar os eventos do serviço, a atividade pode atualizar a interface do usuário com base no estado atual da conexão com o dispositivo 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)
        }
    }
}

Em Transferir dados de BLE, o BroadcastReceiver também é usado para comunicar a descoberta de serviços e os dados de características do dispositivo.

Encerrar conexão GATT

Uma etapa importante ao lidar com conexões Bluetooth é fechar a conexão quando você terminar de usá-la. Para fazer isso, chame a função close() no objeto BluetoothGatt. No exemplo a seguir, o serviço mantém a referência ao BluetoothGatt. Quando a atividade é desvinculada do serviço, a conexão é fechada para evitar o consumo da bateria do dispositivo.

class BluetoothLeService : Service() {

    // ...
    override fun onUnbind(intent: Intent?): Boolean {
        close()
        return super.onUnbind(intent)
    }

    private fun close() {
        bluetoothGatt?.let { gatt ->
            gatt.close()
            bluetoothGatt = null
        }
    }
}