A primeira etapa para interagir com um dispositivo BLE é se conectar a ele. Mais
especificamente, conectar-se ao 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 a conexão com o dispositivo BLE deve ser feita automaticamente 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 autor da chamada (o app Android) é o cliente GATT. O
BluetoothGattCallback é usado para fornecer resultados ao cliente, como
status de 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 mostrar os serviços e as características do 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 usando a API BLE. Um
serviço vinculado, que permite que a atividade se conecte ao
BluetoothLeService e chame funções para se conectar aos dispositivos, processa essa
comunicação. 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 outras opções de
conexão.
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 a atividade se vincula ao serviço, o serviço precisa acessar o
BluetoothAdapter. Ele precisa verificar se o adaptador está
disponível no dispositivo. Para mais informações sobre o BluetoothAdapter, consulte
Configurar o Bluetooth. O exemplo a seguir une 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.
A maneira de lidar com 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 oferece suporte à operação Bluetooth ou desativar todos os recursos
que exigem o Bluetooth para funcionar. No exemplo a seguir, a atividade chama
finish() 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 for inicializada, ela poderá se conectar ao
dispositivo BLE. A atividade precisa enviar o endereço do dispositivo ao serviço para que ele
possa iniciar a conexão. O serviço primeiro chama
getRemoteDevice() no BluetoothAdapter para acessar o dispositivo. Se
o adaptador não 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() depois que o serviço é
inicializado. A atividade precisa transmitir o endereço do dispositivo BLE. No
exemplo abaixo, 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 do 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 requer um BluetoothGattCallback para receber
notificações sobre o estado da conexão, descoberta de serviços, leituras
de características e notificações de características.
Este documento se concentra nas notificações de estado da conexão. Para saber como descobrir serviços, ler características e solicitar notificações de características, consulte Transferir dados BLE.
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 depois 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.
O serviço chama a função connectGatt(). Isso requer um objeto Context, uma flag booleana autoConnect e o BluetoothGattCallback. Neste exemplo, o app está se conectando diretamente ao
dispositivo BLE, então false é transmitido para autoConnect.
O serviço também adiciona uma propriedade BluetoothGatt para que possa
fechar 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 serviço se conecta ou se desconecta do servidor GATT, ele precisa notificar a atividade sobre o 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 usa
uma string de ação que é transmitida para 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 em vigor, o serviço vai usá-la no
BluetoothGattCallback para enviar informações sobre o estado da conexão com o
servidor GATT. O serviço declara constantes que representam as ações Intent
e o estado de conexão atual do serviço.
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 estiver saindo da
tela. Ao detectar os eventos do serviço, a atividade pode
atualizar a interface do usuário com base no estado de conexão atual 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.
Fechar conexão GATT
Uma etapa importante ao lidar com conexões Bluetooth é fechar a
conexão quando 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 se desvincula 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 } } }