El primer paso para interactuar con un dispositivo BLE es conectarse a él. Más específicamente, conectarse al servidor GATT en el dispositivo. Para conectarte a un servidor GATT en un dispositivo BLE, usa el método connectGatt(). Este método
recibe tres parámetros: un objeto Context, autoConnect (un booleano
que indica si se debe establecer una conexión automática con el dispositivo BLE en cuanto
esté disponible) y una referencia a un
BluetoothGattCallback:
var bluetoothGatt: BluetoothGatt? = null // ... bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
Esto se conecta al servidor GATT alojado en el dispositivo BLE y devuelve una instancia de BluetoothGatt, que luego puedes usar para realizar operaciones de cliente GATT. El emisor (la app para Android) es el cliente GATT. El BluetoothGattCallback se usa para entregar resultados al cliente, como el estado de la conexión, así como cualquier otra operación del cliente GATT.
Configura un servicio vinculado
En el siguiente ejemplo, la app de BLE proporciona una actividad (DeviceControlActivity) para conectarse a dispositivos Bluetooth, mostrar datos del dispositivo y mostrar los servicios y las características de GATT que admite el dispositivo. Según
la entrada del usuario, esta actividad se comunica con un Service llamado
BluetoothLeService, que interactúa con el dispositivo BLE usando la API de BLE. Un servicio vinculado, que permite que la actividad se conecte a BluetoothLeService y llame a funciones para conectarse a los dispositivos, controla esta comunicación. El BluetoothLeService necesita una implementación de Binder que proporcione acceso al servicio para la actividad.
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 } } }
La actividad puede iniciar el servicio con bindService(), pasando un Intent para iniciar el servicio, una implementación de ServiceConnection para escuchar los eventos de conexión y desconexión, y una marca para especificar opciones de conexión adicionales.
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) } }
Configura el BluetoothAdapter
Después de que la actividad se vincula al servicio, este debe acceder al
BluetoothAdapter. Debe verificar que el adaptador esté disponible en el dispositivo. Para obtener más información sobre el BluetoothAdapter, consulta
Cómo configurar el Bluetooth. En el siguiente ejemplo, se une este código de configuración
en una función initialize() que devuelve un valor Boolean que indica
que se realizó correctamente.
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 } // ... }
La actividad llama a esta función dentro de su implementación de ServiceConnection.
El manejo de un valor de retorno falso de la función initialize() depende de tu
aplicación. Puedes mostrar un mensaje de error al usuario que indique que el dispositivo actual no admite la operación de Bluetooth o inhabilitar cualquier función que requiera Bluetooth para funcionar. En el siguiente ejemplo, la actividad llama a finish() para enviar al usuario de vuelta a la pantalla 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 } } // ... }
Conectarse a un dispositivo
Después de que se inicializa la instancia de BluetoothLeService, puede conectarse al dispositivo BLE. La actividad debe enviar la dirección del dispositivo al servicio para que pueda iniciar la conexión. Primero, el servicio llama a
getRemoteDevice() en BluetoothAdapter para acceder al dispositivo. Si el adaptador no puede encontrar un dispositivo con esa dirección, getRemoteDevice() arroja una 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 } }
El DeviceControlActivity llama a esta función connect() después de que se inicializa el servicio. La actividad debe pasar la dirección del dispositivo BLE. En el siguiente ejemplo, la dirección del dispositivo se pasa a la actividad como un intent adicional.
// 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 } }
Declara la devolución de llamada de GATT
Después de que la actividad le indica al servicio a qué dispositivo conectarse y el servicio se conecta al dispositivo, el servicio debe conectarse al servidor GATT en el dispositivo BLE. Esta conexión requiere un BluetoothGattCallback para recibir
notificaciones sobre el estado de la conexión, el descubrimiento de servicios, las lecturas de características
y las notificaciones de características.
Este documento se enfoca en las notificaciones de estado de la conexión. Para aprender a descubrir servicios, leer características y solicitar notificaciones de características, consulta Cómo transferir datos de BLE.
La función onConnectionStateChange() se activa cuando cambia la conexión al servidor GATT del dispositivo. En el siguiente ejemplo, la devolución de llamada se define en la clase Service para que se pueda usar con BluetoothDevice después de que el servicio se conecte a ella.
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 } } }
Cómo conectarse al servicio de GATT
Después de que se declara el BluetoothGattCallback, el servicio puede usar el objeto BluetoothDevice de la función connect() para conectarse al servicio de GATT en el dispositivo.
El servicio llama a la función connectGatt(). Esto requiere un objeto Context, una marca booleana autoConnect y el BluetoothGattCallback. En este ejemplo, la app se conecta directamente al dispositivo BLE, por lo que se pasa false para autoConnect.
El servicio también agrega una propiedad BluetoothGatt para que pueda
cerrar la conexión cuando ya no sea necesaria.
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 } } }
Actualizaciones de transmisiones
Cuando el servicio se conecta al servidor GATT o se desconecta de él, debe notificar a la actividad sobre el estado nuevo. Existen varias formas de lograrlo. En el siguiente ejemplo, se usan transmisiones para enviar la información del servicio a la actividad.
El servicio declara una función para transmitir el estado nuevo. Esta función toma una cadena de acción que se pasa a un objeto Intent antes de transmitirse al sistema.
private fun broadcastUpdate(action: String) { val intent = Intent(action) sendBroadcast(intent) }
Una vez que se implementa la función de transmisión, el servicio la usa dentro de BluetoothGattCallback para enviar información sobre el estado de la conexión con el servidor GATT. El servicio declara constantes que representan las acciones de Intent
y el estado de conexión actual del servicio.
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 } }
Cómo escuchar actualizaciones de la actividad
Después de que el servicio transmite las actualizaciones de conexión, la actividad debe implementar un BroadcastReceiver. Registra este receptor cuando configures la actividad y cancela el registro cuando la actividad salga de la pantalla. Cuando se escuchan los eventos del servicio, la actividad puede actualizar la interfaz de usuario según el estado de conexión actual con el 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) } } }
En Cómo transferir datos de BLE, también se usa BroadcastReceiver para comunicar el descubrimiento de servicios y los datos de características del dispositivo.
Cierra la conexión de GATT
Un paso importante cuando se trabaja con conexiones Bluetooth es cerrar la conexión cuando hayas terminado de usarla. Para ello, llama a la función close() en el objeto BluetoothGatt. En el siguiente ejemplo, el servicio
contiene la referencia al BluetoothGatt. Cuando la actividad se desvincula del servicio, se cierra la conexión para evitar que se agote la batería del 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 } } }