Conectarse con un servidor GATT

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