BluetoothGatt: qué es, métodos y protocolo GATT BLE en Android

Autor: IT Sectr Publicado: 2026-07-16 Tiempo de lectura: 10 min

BluetoothGatt es una clase de Android que proporciona una API para el funcionamiento del cliente GATT (Generic Attribute Profile) sobre una conexión BLE. BluetoothGatt encapsula la conexión a un servidor GATT remoto (dispositivo periférico BLE) y gestiona todas las operaciones del perfil: descubrimiento de servicios, lectura y escritura de características, suscripción a notificaciones e indicaciones. Una instancia de BluetoothGatt se obtiene mediante BluetoothDevice.connectGatt() con un BluetoothGattCallback. Según Android Developers, 2026, BluetoothGatt es la clase central para la comunicación BLE bidireccional, compatible con operaciones GATT desde BLE 4.0 hasta BLE 5.4.

Puntos Clave

  • BluetoothGatt es una clase de Android para el cliente GATT que gestiona la conexión BLE con un dispositivo periférico
  • connectGatt() es un método de BluetoothDevice para crear BluetoothGatt; acepta contexto, autoConnect, callback y transport
  • discoverServices() es un método para obtener la jerarquía GATT: servicios (BluetoothGattService), características (BluetoothGattCharacteristic)
  • readCharacteristic/writeCharacteristic son métodos de lectura y escritura con resultados asíncronos mediante BluetoothGattCallback
  • setCharacteristicNotification es un método para suscribirse a notificaciones BLE con escritura obligatoria del descriptor CCCD

Qué es BluetoothGatt: esencia y creación de conexión

BluetoothGatt es un objeto proxy que representa una conexión GATT entre un dispositivo Android (central) y un periférico BLE (servidor). Cada instancia de BluetoothGatt corresponde a una conexión BLE activa. Todas las operaciones del perfil GATT se realizan a través de él: descubrimiento, lectura, escritura, notificaciones. BluetoothGatt no se crea directamente — lo devuelve el método BluetoothDevice.connectGatt().

Crear un BluetoothGatt requiere cuatro parámetros. Context — el contexto de la aplicación (Activity o Application). autoConnect — si es false, Android inicia inmediatamente una conexión directa; si es true, Android se conecta automáticamente cuando se detecta el dispositivo (útil para conexión en segundo plano). BluetoothGattCallback — callback obligatorio para todos los eventos GATT. transport — BluetoothDevice.TRANSPORT_LE (BLE) o TRANSPORT_BREDR (Classic). En dispositivos BLE, usa siempre TRANSPORT_LE.

Ciclo de vida de BluetoothGatt consta de cinco estados. DISCONNECTED — estado inicial. CONNECTING — después de llamar a connectGatt, antes de la confirmación. CONNECTED — después de onConnectionStateChange con STATE_CONNECTED. Tras la conexión, se llama a discoverServices() para obtener la jerarquía GATT. Al finalizar el trabajo — disconnect() y close() para liberar recursos del sistema. Sin close(), la aplicación puede agotar el límite de conexiones BLE de Android (generalmente 4–8).

kotlin
// Creando conexión BluetoothGatt
import android.bluetooth.*

class GattConnector(private val context: Context) {

    private var bluetoothGatt: BluetoothGatt? = null

    fun connect(device: BluetoothDevice): BluetoothGatt? {
        // Cerrar conexión anterior si existe
        close()

        bluetoothGatt = device.connectGatt(
            context,
            false,  // autoConnect = false ( )
            object : BluetoothGattCallback() {
                override fun onConnectionStateChange(
                    gatt: BluetoothGatt, status: Int, newState: Int
                ) {
                    when (newState) {
                        BluetoothProfile.STATE_CONNECTED -> {
                            // 2. Conexión establecida → Descubrimiento
                            gatt.discoverServices()
                        }
                        BluetoothProfile.STATE_DISCONNECTED -> {
                            // 3. Conexión perdida
                            close()
                        }
                    }
                }

                override fun onServicesDiscovered(
                    gatt: BluetoothGatt, status: Int
                ) {
                    if (status == BluetoothGatt.GATT_SUCCESS) {
                        // 4. Jerarquía GATT recibida
                        onGattReady(gatt)
                    }
                }
            },
            BluetoothDevice.TRANSPORT_LE
        )
        return bluetoothGatt
    }

    private fun onGattReady(gatt: BluetoothGatt) {
        // GATT listo para operaciones de lectura/escritura
    }

    fun close() {
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
    }
}

La clase GattConnector demuestra la creación correcta de BluetoothGatt. El parámetro autoConnect=false indica una conexión directa (para dispositivos escaneados). En onConnectionStateChange con STATE_CONNECTED, se llama inmediatamente a discoverServices(). onServicesDiscovered señala que GATT está listo. close() llama secuencialmente a disconnect() y close() — sin close() los recursos del sistema no se liberan, lo que provoca fugas de conexiones BLE.

Descubrimiento de servicios y características mediante BluetoothGatt

discoverServices() es el primer método GATT que se llama después de la conexión. Inicia una búsqueda asíncrona de todos los servicios en el periférico BLE. El resultado llega en onServicesDiscovered() con un código de estado: GATT_SUCCESS (0) — éxito, 133 — GATT_ERROR, 8 — GATT_CONNECTION_TIMEOUT. Tras un descubrimiento exitoso, BluetoothGatt completa la lista de servicios accesibles mediante getServices().

Cada BluetoothGattService contiene una lista de BluetoothGattCharacteristic. Una característica tiene un UUID, propiedades (PROPERTY_READ, PROPERTY_WRITE, PROPERTY_NOTIFY) y descriptores opcionales. Las propiedades determinan qué operaciones están permitidas: si una característica no tiene PROPERTY_READ, llamar a readCharacteristic devolverá un error. Usa getDescriptors() para obtener los descriptores de la característica.

kotlin
// Descubrimiento de servicios y búsqueda de características
class GattServiceExplorer {

    // Buscar servicio por UUID
    fun findService(gatt: BluetoothGatt, uuid: UUID): BluetoothGattService? {
        return gatt.services?.firstOrNull { it.uuid == uuid }
    }

    // Buscar característica en el servicio
    fun findCharacteristic(
        service: BluetoothGattService,
        uuid: UUID
    ): BluetoothGattCharacteristic? {
        return service.characteristics?.firstOrNull { it.uuid == uuid }
    }

    // Obtener todas las operaciones de característica compatibles
    fun getCharacteristicProperties(chars: BluetoothGattCharacteristic): List<String> {
        val props = mutableListOf<String>()
        with(chars.properties) {
            if (and(BluetoothGattCharacteristic.PROPERTY_READ) != 0) props.add("READ")
            if (and(BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) props.add("ESCRITURA")
            if (and(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.add("ESCRITURA_SIN_RESP")
            if (and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.add("NOTIFY")
            if (and(BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.add("INDICATE")
        }
        return props
    }

    // Registrar jerarquía GATT completa
    fun dumpGattTree(gatt: BluetoothGatt) {
        gatt.services?.forEach { service ->
            print("Service: ${service.uuid}")
            service.characteristics?.forEach { char ->
                print("  Characteristic: ${char.uuid}, properties: ${char.properties}")
                char.descriptors?.forEach { desc ->
                    print("    Descriptor: ${desc.uuid}")
                }
            }
        }
    }
}

La clase GattServiceExplorer proporciona utilidades para navegar por la jerarquía GATT. findService y findCharacteristic buscan servicios y características por UUID. getCharacteristicProperties verifica las máscaras de bits de propiedades mediante and(). dumpGattTree imprime la jerarquía completa en el registro — útil al depurar dispositivos BLE. Todas las operaciones sobre BluetoothGatt deben realizarse después de onServicesDiscovered exitoso, de lo contrario getServices() devolverá una lista vacía.

Lectura de características BLE: readCharacteristic y readDescriptor

readCharacteristic() es un método asíncrono de BluetoothGatt para leer el valor de una característica de un dispositivo BLE remoto. El resultado llega en onCharacteristicRead() de BluetoothGattCallback. Si un dispositivo tiene 2+ características coincidentes (poco probable pero posible), readCharacteristic() podría leer una no deseada — es más seguro llamar a readCharacteristic() en una instancia de BluetoothGattCharacteristic en lugar de por UUID.

readDescriptor() es un método para leer el valor de un descriptor de característica. Un descriptor típico es CCCD (Client Characteristic Configuration Descriptor, UUID 0x2902), que determina si las notificaciones están activadas. El resultado está en onDescriptorRead(). La lectura de descriptores rara vez es necesaria en la práctica — CCCD se gestiona con setCharacteristicNotification(), pero para descriptores personalizados (User Description 0x2901, Presentation Format 0x2904) readDescriptor() es la única forma de obtener metadatos.

MTU y lectura de datos grandes — si el valor de la característica supera el MTU (23 bytes para BLE 4.0), Android fragmenta y reensambla automáticamente los datos mediante una secuencia de solicitudes de lectura a través de la pila BLE. Para BLE 5.0+ con MTU extendido (hasta 251 bytes), no se requiere fragmentación — una sola lectura devuelve datos completos. Antes de leer, puedes llamar a requestMtu() para negociar el MTU máximo.

kotlin
// Leer característica y descriptor mediante BluetoothGatt
class GattReader {

    fun readHeartRate(gatt: BluetoothGatt) {
        // UUID del servicio de frecuencia cardíaca = 0x180D
        val service = gatt.getService(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
            ?: return
        // UUID de medición de frecuencia cardíaca = 0x2A37
        val characteristic = service.getCharacteristic(
            UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB")
        ) ?: return

        if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
            gatt.readCharacteristic(characteristic)
        }
    }

    fun readDescriptor(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        // CCCD (0x2902)
        val cccd = characteristic.getDescriptor(
            UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
        )
        cccd?.let { gatt.readDescriptor(it) }
    }

    // Procesar datos en callback onCharacteristicRead:
    fun parseHeartRate(value: ByteArray): Int {
        // Frecuencia cardíaca BLE: bytes = flags, segundo = bpm
        return if (value.isNotEmpty()) value[1].toInt() and 0xFF else 0
    }

    private fun hasProperty(char: BluetoothGattCharacteristic, prop: Int): Boolean {
        return char.properties and prop != 0
    }
}

La clase GattReader demuestra la lectura de una característica de frecuencia cardíaca. El servicio 0x180D contiene la característica 0x2A37 (Heart Rate Measurement) — un perfil BLE estándar de Bluetooth SIG. Antes de leer, se verifica la propiedad PROPERTY_READ mediante hasProperty. parseHeartRate analiza el formato BLE de pulso: el primer byte — flags (formato de datos), el segundo — valor bpm. El descriptor CCCD (0x2902) se lee para verificar el estado de las notificaciones.

Escritura de características: writeCharacteristic con WriteType

writeCharacteristic() es un método de BluetoothGatt para escribir datos en un periférico BLE. En Android API 33+, writeCharacteristic() se ha reemplazado por writeCharacteristic(request), donde BluetoothGattCharacteristicWriteRequest es un objeto de solicitud que contiene la característica, la matriz de bytes y WriteType. El antiguo método writeCharacteristic(characteristic) con setValue() está obsoleto. WriteType determina el comportamiento de la solicitud: WRITE_TYPE_DEFAULT (depende de las propiedades de la característica), WRITE_TYPE_NO_RESPONSE (sin respuesta) y WRITE_TYPE_SIGNED (autorización).

Elegir el WriteType afecta la velocidad y la fiabilidad. WRITE_TYPE_DEFAULT generalmente corresponde a withResponse (si la característica tiene PROPERTY_WRITE) o withoutResponse (si tiene PROPERTY_WRITE_NO_RESPONSE). Para datos en streaming (actualizaciones OTA, registros), usa WRITE_TYPE_NO_RESPONSE — máximo rendimiento. Para comandos con garantía de entrega (activar, configurar) — WRITE_TYPE_DEFAULT con confirmación mediante onCharacteristicWrite.

kotlin
// Escritura de característica BLE en Android API 33+
class GattWriter {

    // Escribir con respuesta (withResponse)
    fun writeWithResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            // API 33+: writeCharacteristic
            val request = BluetoothGattCharacteristicWriteRequest(
                characteristic,
                data,
                BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_DEFAULT,
                @android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
            )
            gatt.writeCharacteristic(request)
        } else {
            // API < 33:   (deprecated)
            characteristic.setValue(data)
            gatt.writeCharacteristic(characteristic)
        }
    }

    // Escribir sin respuesta (máxima velocidad)
    fun writeWithoutResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val request = BluetoothGattCharacteristicWriteRequest(
                characteristic,
                data,
                BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_NO_RESPONSE,
                @android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
            )
            gatt.writeCharacteristic(request)
        } else {
            characteristic.setValue(data)
            characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
            gatt.writeCharacteristic(characteristic)
        }
    }

    // Callback onCharacteristicWrite (API 33+)
    private val writeCallback = object : BluetoothGattCallback() {
        override fun onCharacteristicWrite(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray,
            status: Int,
            callbackType: Int
        ) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                print("Write success: ${value.size} bytes")
            }
        }
    }
}

La clase GattWriter admite ambos WriteTypes para diferentes niveles de API. writeWithResponse usa WRITE_TYPE_DEFAULT — el dispositivo BLE confirma la escritura mediante onCharacteristicWrite. writeWithoutResponse usa WRITE_TYPE_NO_RESPONSE — los datos se envían sin confirmación, máximo rendimiento. En API 33+, se usa el nuevo writeCharacteristic(request) con BluetoothGattCharacteristicWriteRequest. En API < 33 — el antiguo setValue() + writeCharacteristic().

Suscripción a notificaciones: setCharacteristicNotification y CCCD

setCharacteristicNotification() es un método de BluetoothGatt para suscribirse a notificaciones sobre cambios de característica en el periférico. Tras activar la suscripción, el dispositivo BLE envía nuevos valores mediante onCharacteristicChanged(). Sin embargo, setCharacteristicNotification() solo activa la notificación local de Android — para habilitar las notificaciones en el propio dispositivo BLE, también debes escribir el valor 0x0100 en el descriptor CCCD (0x2902).

CCCD (Client Characteristic Configuration Descriptor) es un descriptor que controla el envío de notificaciones desde el periférico BLE. Valor 0x0000 — notificaciones desactivadas, 0x0100 — notificaciones activadas, 0x0200 — indicaciones activadas. La escritura en CCCD se realiza mediante writeDescriptor() en BluetoothGatt después de llamar a setCharacteristicNotification(). Android no escribe CCCD automáticamente — esta responsabilidad recae en el desarrollador.

kotlin
// Suscripción correcta a notificaciones BLE
class GattNotificationManager {

    // 1. Activar notificaciones
    fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        // Paso 1: suscripción local de Android
        val success = gatt.setCharacteristicNotification(characteristic, true)
        if (!success) {
            print("Error al suscribirse")
            return
        }

        // Paso 2: escribir CCCD (0x2902) en dispositivo BLE
        val cccdDescriptor = characteristic.getDescriptor(
            UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
        ) ?: return

        // 0x0100 = notificación, 0x0200 = indicación
        val cccdValue = if (characteristic.properties
            and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
            BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE  // [0x01, 0x00]
        } else {
            BluetoothGattDescriptor.ENABLE_INDICATION_VALUE     // [0x02, 0x00]
        }

        cccdDescriptor.setValue(cccdValue)
        gatt.writeDescriptor(cccdDescriptor)
    }

    // 2. Descubrimiento de características
    fun disableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        gatt.setCharacteristicNotification(characteristic, false)
        val cccdDescriptor = characteristic.getDescriptor(
            UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
        ) ?: return
        cccdDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
        gatt.writeDescriptor(cccdDescriptor)
    }

    // 3. Callback de notificación
    private val notificationCallback = object : BluetoothGattCallback() {
        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            value: ByteArray,
            callbackType: Int
        ) {
            // Nuevo valor del periférico BLE
            print("Notification: ${value.size} bytes")
        }
    }
}

La clase GattNotificationManager implementa el protocolo correcto de dos pasos para la suscripción a notificaciones BLE. enableNotification primero llama a setCharacteristicNotification(true) en Android, luego escribe 0x0100 en el descriptor CCCD mediante writeDescriptor. disableNotification realiza las operaciones inversas. Sin la escritura de CCCD, el dispositivo BLE no envía notificaciones — este es el error más común entre los desarrolladores BLE en Android.

Ejemplo de cliente GATT en Kotlin con BluetoothGattCallback

Ejemplo completo de un cliente GATT en Kotlin que combina la creación de BluetoothGatt, descubrimiento, lectura y suscripción a notificaciones en un único gestor utilizando corutinas para el procesamiento asíncrono.

kotlin
// Cliente GATT completo con corutinas en Kotlin
class GattClient(context: Context) {

    private val context = context.applicationContext
    private var gatt: BluetoothGatt? = null

    // Conectar con corutina
    suspend fun connect(device: BluetoothDevice): Boolean =
        suspendCoroutine { continuation ->
            gatt = device.connectGatt(
                context, false,
                object : BluetoothGattCallback() {
                    override fun onConnectionStateChange(
                        gatt: BluetoothGatt, status: Int, newState: Int
                    ) {
                        if (newState == BluetoothProfile.STATE_CONNECTED) {
                            gatt.discoverServices()
                        } else {
                            continuation.resume(false)
                        }
                    }

                    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
                        continuation.resume(status == BluetoothGatt.GATT_SUCCESS)
                    }
                },
                BluetoothDevice.TRANSPORT_LE
            )
        }

    // Leer característica mediante corutina
    suspend fun readCharacteristicValue(char: BluetoothGattCharacteristic): ByteArray? =
        suspendCoroutine { continuation ->
            gatt?.let { gatt ->
                // Guardar etiqueta de característica para identificación en callback
                gatt.setCharacteristic(char, null)  // para API < 33
                gatt.readCharacteristic(char)
            }
        }

    // Cerrar conexión
    fun release() {
        gatt?.disconnect()
        gatt?.close()
        gatt = null
    }
}

El cliente GATT GattClient utiliza corutinas (suspendCoroutine) para transformar la API basada en callbacks de BluetoothGatt en llamadas secuenciales. connect() espera onServicesDiscovered, tras lo cual la jerarquía GATT está disponible. readCharacteristicValue() espera onCharacteristicRead. Este enfoque elimina el anidamiento de callbacks y hace que el código BLE sea lineal. release() garantiza la liberación de recursos — una llamada obligatoria en onDestroy de Activity o ViewModel.onCleared.

Preguntas Frecuentes

¿Qué es BluetoothGatt en Android?

BluetoothGatt es una clase de Android para el cliente GATT que gestiona una conexión BLE con un dispositivo periférico. Se crea mediante BluetoothDevice.connectGatt(), proporcionando métodos discoverServices(), readCharacteristic(), writeCharacteristic(), setCharacteristicNotification(). Los resultados de todas las operaciones llegan de forma asíncrona mediante BluetoothGattCallback. Sin BluetoothGatt, la comunicación BLE bidireccional en Android es imposible.

¿Por qué onServicesDiscovered devuelve el estado 133?

El estado 133 (GATT_ERROR) indica un error interno de la pila BLE de Android. Causas: el dispositivo se desconectó durante el descubrimiento, el MTU es inferior al mínimo (23 bytes) o la pila BLE está sobrecargada. Solución: reintenta discoverServices() con un retardo de 500 ms, verifica el RSSI del dispositivo y asegúrate de que el periférico admite el descubrimiento GATT en su estado actual.

¿Cómo escribir correctamente una característica con confirmación?

Para escribir con confirmación, llama a writeCharacteristic() con WRITE_TYPE_DEFAULT (API 33+: BluetoothGattCharacteristicWriteRequest). En caso de éxito, el dispositivo BLE envía una confirmación y Android llama a onCharacteristicWrite con GATT_SUCCESS. Si el dispositivo no responde en 30 segundos (tiempo de espera de la pila), el callback devuelve un estado de error. Para watchdog, usa Handler con postDelayed.

¿Cómo trabajar con BLE en Android 33+?

En Android 13+ (API 33), los métodos de BluetoothGatt han cambiado: writeCharacteristic() ahora acepta BluetoothGattCharacteristicWriteRequest, readCharacteristic() — BluetoothGattCharacteristicReadRequest. Los antiguos setValue()/writeCharacteristic() están obsoletos. BluetoothGattCallback también ha cambiado: onCharacteristicRead(), onCharacteristicWrite(), onCharacteristicChanged() reciben ByteArray value y callbackType. Usa Build.VERSION.SDK_INT para ramificar.

¿Cuántas conexiones BLE admite Android?

Android admite de 4 a 8 conexiones BLE GATT simultáneas (varía según el fabricante y la versión de Android). Pixel/Google: hasta 7, Samsung: hasta 5, Xiaomi: hasta 4. Cuando se supera el límite, connectGatt devuelve null u onConnectionStateChange con un error. Para trabajar con un gran número de dispositivos, usa conexión cíclica o Bluetooth Mesh.

Resumen

  • BluetoothGatt es un cliente GATT de Android para conexión BLE, creado mediante BluetoothDevice.connectGatt() con BluetoothGattCallback
  • discoverServices() es un paso obligatorio tras la conexión para obtener servicios, características y descriptores del dispositivo BLE
  • Lectura — readCharacteristic() con resultado asíncrono en onCharacteristicRead(); para datos grandes se requiere negociación de MTU
  • Escritura — writeCharacteristic() con WriteType: DEFAULT (withResponse) o NO_RESPONSE (sin confirmación)
  • Notificaciones — activación en dos pasos: setCharacteristicNotification() + escritura de CCCD (0x2902) con valor 0x0100
  • API 33+ — nuevos métodos writeCharacteristic(request) y readCharacteristic(request) con objetos de solicitud
  • Liberar recursos — llamada obligatoria a disconnect() y close() para evitar fugas de conexiones BLE

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también