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 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).
// 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.
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.
// 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.
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.
// 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.
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.
// 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().
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.
// 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 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.
// 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
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.
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.
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.
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.
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
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.
Lea también