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 del dispositivo. Para conectarte a un servidor GATT en un dispositivo BLE, usa el connectGatt() método. Este método recibe tres parámetros: un Context objeto, autoConnect (un valor booleano que indica si se debe conectar automáticamente al 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 por el dispositivo BLE y muestra una BluetoothGatt instancia, que luego puedes usar para realizar operaciones de cliente GATT. El emisor (la app para Android) es el cliente GATT. Se usa BluetoothGattCallback para entregar resultados al cliente, como el estado de la conexión, así como cualquier otra operación de cliente GATT.

Configura un servicio enlazado

En el siguiente ejemplo, la app BLE proporciona una actividad (DeviceControlActivity) para conectarse a dispositivos Bluetooth, mostrar datos del dispositivo y mostrar los servicios y las características GATT compatibles con el dispositivo. Según la entrada del usuario, esta actividad se comunica con un Service llamado BluetoothLeService, que interactúa con el dispositivo BLE a través de la API de BLE. La comunicación se realiza mediante un servicio enlazado que permite que la actividad se conecte a BluetoothLeService y llame a funciones para conectarse a los dispositivos. The BluetoothLeService needs a Binder implementation that provides access to the service for the activity.

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

Una vez que el servicio está enlazado, debe acceder a la BluetoothAdapter. Debe verificar que el adaptador esté disponible en el dispositivo. Lee Configura Bluetooth para obtener más información sobre el BluetoothAdapter. En el siguiente ejemplo, se incluye este código de configuración en una función initialize() que muestra un valor Boolean que indica el éxito.

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 devolución 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 que Bluetooth funcione. En el siguiente ejemplo, finish() se llama en la actividad para enviar al usuario 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

Una vez que se inicializa la instancia de BluetoothLeService, se puede conectar al dispositivo BLE. La actividad debe enviar la dirección del dispositivo al servicio para que pueda iniciar la conexión. Primero, el servicio llamará a getRemoteDevice() en el BluetoothAdapter para acceder al dispositivo. Si el adaptador no puede encontrar un dispositivo con esa dirección, getRemoteDevice() arroja un 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 llama a esta función connect() una vez 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 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
    }
}

Declara la devolución de llamada GATT

Una vez 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 del 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 tema se centra en las notificaciones de estado de la conexión. Consulta Cómo transferir datos de BLE para obtener información sobre cómo realizar el descubrimiento de servicios, las lecturas de características y las solicitudes de notificaciones de características .

La onConnectionStateChange() función 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 el BluetoothDevice una vez 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
        }
    }
}

Conéctate al servicio GATT

Una vez que se declara BluetoothGattCallback, el servicio puede usar el objeto BluetoothDevice de la función connect() para conectarse al servicio GATT del dispositivo.

La connectGatt() función se usa. 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.

También se agrega una propiedad BluetoothGatt. Esto permite que el servicio cierre 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 transmisión

Cuando el servidor se conecta o desconecta del servidor GATT, debe notificar a la actividad el estado nuevo. Hay 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 la función de transmisión está en su lugar, se usa dentro de BluetoothGattCallback para enviar información sobre el estado de la conexión con el servidor GATT. Las constantes y el estado de conexión actual del servicio se declaran en el servicio que representa las acciones 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
    }
}

Escucha las actualizaciones en la actividad

Una vez que el servicio transmite las actualizaciones de conexión, la actividad debe implementar un BroadcastReceiver. Registra este receptor cuando configures la actividad y anula el registro cuando la actividad salga de la pantalla. Al escuchar 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, así como los datos de características del dispositivo.

Cierra la conexión GATT

Un paso importante cuando se trata de conexiones Bluetooth es cerrar la conexión cuando termines de usarla. Para ello, llama a la función close() en el objeto BluetoothGatt. En el siguiente ejemplo, el servicio contiene la referencia a BluetoothGatt. Cuando la actividad se desvincula del servicio, se cierra la conexión para evitar agotar 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
        }
    }
}