BluetoothGatt è una classe Android che fornisce un'API per le operazioni del client GATT (Generic Attribute Profile) su una connessione BLE. BluetoothGatt incapsula la connessione a un server GATT remoto (dispositivo periferico BLE) e gestisce tutte le operazioni del profilo: scoperta dei servizi, lettura e scrittura delle caratteristiche, iscrizione a notifiche e indicazioni. Un'istanza di BluetoothGatt si ottiene tramite BluetoothDevice.connectGatt() con un BluetoothGattCallback. Secondo Android Developers, 2026, BluetoothGatt è la classe centrale per la comunicazione BLE bidirezionale, supportando operazioni GATT da BLE 4.0 a BLE 5.4.
Punti Chiave
BluetoothGatt è un oggetto proxy che rappresenta una connessione GATT tra un dispositivo Android (centrale) e una periferica BLE (server). Ogni istanza BluetoothGatt corrisponde a una connessione BLE attiva. Tutte le operazioni del profilo GATT vengono eseguite tramite esso: scoperta, lettura, scrittura, notifiche. BluetoothGatt non viene creato direttamente — viene restituito dal metodo BluetoothDevice.connectGatt().
Creare un BluetoothGatt richiede quattro parametri. Context — il contesto dell'applicazione (Activity o Application). autoConnect — se false, Android avvia immediatamente una connessione diretta; se true, Android si connette automaticamente quando il dispositivo viene rilevato (utile per la connessione in background). BluetoothGattCallback — callback obbligatorio per tutti gli eventi GATT. transport — BluetoothDevice.TRANSPORT_LE (BLE) o TRANSPORT_BREDR (Classic). Sui dispositivi BLE, usa sempre TRANSPORT_LE.
Ciclo di vita di BluetoothGatt è composto da cinque stati. DISCONNECTED — stato iniziale. CONNECTING — dopo la chiamata a connectGatt, prima della conferma. CONNECTED — dopo onConnectionStateChange con STATE_CONNECTED. Dopo la connessione, viene chiamato discoverServices() per ottenere la gerarchia GATT. Dopo aver terminato il lavoro — disconnect() e close() per liberare le risorse di sistema. Senza close(), l'applicazione potrebbe esaurire il limite di connessioni BLE di Android (di solito 4–8).
// Creazione connessione BluetoothGatt
import android.bluetooth.*
class GattConnector(private val context: Context) {
private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice): BluetoothGatt? {
// Chiudi connessione precedente se esiste
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. Connessione stabilita → Scoperta
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 3. Connessione persa
close()
}
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt, status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 4. Gerarchia GATT ricevuta
onGattReady(gatt)
}
}
},
BluetoothDevice.TRANSPORT_LE
)
return bluetoothGatt
}
private fun onGattReady(gatt: BluetoothGatt) {
// GATT pronto per operazioni di lettura/scrittura
}
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
}
La classe GattConnector dimostra la corretta creazione di BluetoothGatt. Il parametro autoConnect=false significa connessione diretta (per dispositivi scansionati). All'onConnectionStateChange con STATE_CONNECTED, discoverServices() viene chiamato immediatamente. onServicesDiscovered segnala che GATT è pronto. close() chiama sequenzialmente disconnect() e close() — senza close() le risorse di sistema non vengono liberate, causando perdite di connessioni BLE.
discoverServices() è il primo metodo GATT chiamato dopo la connessione. Avvia una ricerca asincrona di tutti i servizi sulla periferica BLE. Il risultato arriva in onServicesDiscovered() con un codice di stato: GATT_SUCCESS (0) — successo, 133 — GATT_ERROR, 8 — GATT_CONNECTION_TIMEOUT. Dopo una scoperta riuscita, BluetoothGatt popola l'elenco dei servizi accessibili tramite getServices().
Ogni BluetoothGattService contiene un elenco di BluetoothGattCharacteristic. Una caratteristica ha un UUID, proprietà (PROPERTY_READ, PROPERTY_WRITE, PROPERTY_NOTIFY) e descrittori opzionali. Le proprietà determinano quali operazioni sono consentite: se una caratteristica non ha PROPERTY_READ, chiamare readCharacteristic restituirà un errore. Usa getDescriptors() per ottenere i descrittori della caratteristica.
// Scoperta servizi e ricerca caratteristiche
class GattServiceExplorer {
// Trova servizio per UUID
fun findService(gatt: BluetoothGatt, uuid: UUID): BluetoothGattService? {
return gatt.services?.firstOrNull { it.uuid == uuid }
}
// Trova caratteristica nel servizio
fun findCharacteristic(
service: BluetoothGattService,
uuid: UUID
): BluetoothGattCharacteristic? {
return service.characteristics?.firstOrNull { it.uuid == uuid }
}
// Ottieni tutte le operazioni caratteristica supportate
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("SCRIVI")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.add("SCRIVI_SENZA_RISPOSTA")
if (and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.add("NOTIFY")
if (and(BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.add("INDICATE")
}
return props
}
// Registra l'intera gerarchia GATT
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 classe GattServiceExplorer fornisce utilità per navigare nella gerarchia GATT. findService e findCharacteristic cercano servizi e caratteristiche per UUID. getCharacteristicProperties verifica le maschere di bit delle proprietà tramite and(). dumpGattTree stampa la gerarchia completa nel log — utile durante il debug di dispositivi BLE. Tutte le operazioni su BluetoothGatt devono essere eseguite dopo un onServicesDiscovered riuscito, altrimenti getServices() restituirà un elenco vuoto.
readCharacteristic() è un metodo asincrono BluetoothGatt per leggere il valore di una caratteristica da un dispositivo BLE remoto. Il risultato arriva in onCharacteristicRead() del BluetoothGattCallback. Se un dispositivo ha 2+ caratteristiche corrispondenti (improbabile ma possibile), readCharacteristic() potrebbe leggere una caratteristica non target — è più sicuro chiamare readCharacteristic() su un'istanza BluetoothGattCharacteristic piuttosto che per UUID.
readDescriptor() è un metodo per leggere il valore di un descrittore di caratteristica. Un descrittore tipico è CCCD (Client Characteristic Configuration Descriptor, UUID 0x2902), che determina se le notifiche sono attivate. Il risultato è in onDescriptorRead(). La lettura dei descrittori è raramente necessaria nella pratica — CCCD è gestito da setCharacteristicNotification(), ma per descrittori personalizzati (User Description 0x2901, Presentation Format 0x2904) readDescriptor() è l'unico modo per ottenere metadati.
MTU e lettura di dati grandi — se il valore della caratteristica supera il MTU (23 byte per BLE 4.0), Android frammenta e riassembla automaticamente i dati attraverso una sequenza di richieste di lettura tramite lo stack BLE. Per BLE 5.0+ con MTU esteso (fino a 251 byte), la frammentazione non è necessaria — una singola lettura restituisce dati completi. Prima di leggere, puoi chiamare requestMtu() per negoziare il MTU massimo.
// Leggi caratteristica e descrittore tramite BluetoothGatt
class GattReader {
fun readHeartRate(gatt: BluetoothGatt) {
// UUID servizio frequenza cardiaca = 0x180D
val service = gatt.getService(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
?: return
// UUID misurazione frequenza cardiaca = 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) }
}
// Elabora dati nel callback onCharacteristicRead:
fun parseHeartRate(value: ByteArray): Int {
// Frequenza cardiaca BLE: bytes = flags, second = 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 classe GattReader dimostra la lettura di una caratteristica di frequenza cardiaca. Il servizio 0x180D contiene la caratteristica 0x2A37 (Heart Rate Measurement) — un profilo BLE standard di Bluetooth SIG. Prima della lettura, la proprietà PROPERTY_READ viene verificata tramite hasProperty. parseHeartRate analizza il formato BLE del polso: il primo byte — flags (formato dati), il secondo — valore bpm. Il descrittore CCCD (0x2902) viene letto per verificare lo stato delle notifiche.
writeCharacteristic() è un metodo BluetoothGatt per scrivere dati su una periferica BLE. Su Android API 33+, writeCharacteristic() è stato sostituito da writeCharacteristic(request), dove BluetoothGattCharacteristicWriteRequest è un oggetto richiesta contenente la caratteristica, l'array di byte e il WriteType. Il vecchio metodo writeCharacteristic(characteristic) con setValue() è deprecato. WriteType determina il comportamento della richiesta: WRITE_TYPE_DEFAULT (dipende dalle proprietà della caratteristica), WRITE_TYPE_NO_RESPONSE (senza risposta) e WRITE_TYPE_SIGNED (autorizzazione).
Scegliere il WriteType influisce su velocità e affidabilità. WRITE_TYPE_DEFAULT di solito corrisponde a withResponse (se la caratteristica ha PROPERTY_WRITE) o withoutResponse (se ha PROPERTY_WRITE_NO_RESPONSE). Per dati in streaming (aggiornamenti OTA, log), usa WRITE_TYPE_NO_RESPONSE — massimo throughput. Per comandi con garanzia di consegna (attivazione, configurazione) — WRITE_TYPE_DEFAULT con conferma tramite onCharacteristicWrite.
// Scrittura caratteristica BLE su Android API 33+
class GattWriter {
// Scrivi con risposta (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)
}
}
// Scrivi senza risposta (velocità massima)
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 classe GattWriter supporta entrambi i WriteType per diversi livelli API. writeWithResponse usa WRITE_TYPE_DEFAULT — il dispositivo BLE conferma la scrittura tramite onCharacteristicWrite. writeWithoutResponse usa WRITE_TYPE_NO_RESPONSE — i dati vengono inviati senza conferma, massimo throughput. Su API 33+, si usa il nuovo writeCharacteristic(request) con BluetoothGattCharacteristicWriteRequest. Su API < 33 — il vecchio setValue() + writeCharacteristic().
setCharacteristicNotification() è un metodo BluetoothGatt per iscriversi alle notifiche sui cambiamenti di caratteristica sulla periferica. Dopo aver attivato l'iscrizione, il dispositivo BLE invia nuovi valori tramite onCharacteristicChanged(). Tuttavia, setCharacteristicNotification() attiva solo la notifica locale Android — per abilitare le notifiche sul dispositivo BLE stesso, devi anche scrivere il valore 0x0100 nel descrittore CCCD (0x2902).
CCCD (Client Characteristic Configuration Descriptor) è un descrittore che controlla l'invio di notifiche dalla periferica BLE. Valore 0x0000 — notifiche disattivate, 0x0100 — notifiche attivate, 0x0200 — indicazioni attivate. La scrittura in CCCD viene eseguita tramite writeDescriptor() su BluetoothGatt dopo aver chiamato setCharacteristicNotification(). Android non scrive CCCD automaticamente — questa responsabilità spetta allo sviluppatore.
// Iscrizione corretta alle notifiche BLE
class GattNotificationManager {
// 1. Abilita notifiche
fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// Passaggio 1: iscrizione locale Android
val success = gatt.setCharacteristicNotification(characteristic, true)
if (!success) {
print("Iscrizione fallita")
return
}
// Passaggio 2: scrivi CCCD (0x2902) sul dispositivo BLE
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
// 0x0100 = notifica, 0x0200 = indicazione
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. Scoperta caratteristiche
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 notifica
private val notificationCallback = object : BluetoothGattCallback() {
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
callbackType: Int
) {
// Nuovo valore dalla periferica BLE
print("Notification: ${value.size} bytes")
}
}
}
La classe GattNotificationManager implementa il corretto protocollo in due fasi per l'iscrizione alle notifiche BLE. enableNotification prima chiama setCharacteristicNotification(true) su Android, poi scrive 0x0100 nel descrittore CCCD tramite writeDescriptor. disableNotification esegue le operazioni inverse. Senza la scrittura del CCCD, il dispositivo BLE non invia notifiche — questo è l'errore più comune tra gli sviluppatori BLE su Android.
Esempio completo di un client GATT in Kotlin che combina creazione di BluetoothGatt, scoperta, lettura e iscrizione alle notifiche in un unico gestore utilizzando coroutine per l'elaborazione asincrona.
// Client GATT completo con coroutine in Kotlin
class GattClient(context: Context) {
private val context = context.applicationContext
private var gatt: BluetoothGatt? = null
// Connetti con coroutine
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
)
}
// Leggi caratteristica tramite coroutine
suspend fun readCharacteristicValue(char: BluetoothGattCharacteristic): ByteArray? =
suspendCoroutine { continuation ->
gatt?.let { gatt ->
// Salva tag caratteristica per identificazione callback
gatt.setCharacteristic(char, null) // per API < 33
gatt.readCharacteristic(char)
}
}
// Chiudi connessione
fun release() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
Il client GATT GattClient utilizza coroutine (suspendCoroutine) per trasformare l'API BluetoothGatt basata su callback in chiamate sequenziali. connect() attende onServicesDiscovered, dopo di che la gerarchia GATT è disponibile. readCharacteristicValue() attende onCharacteristicRead. Questo approccio elimina l'annidamento dei callback e rende il codice BLE lineare. release() garantisce la pulizia delle risorse — una chiamata obbligatoria in onDestroy dell'Activity o ViewModel.onCleared.
Domande Frequenti
BluetoothGatt è una classe Android per il client GATT che gestisce una connessione BLE con un dispositivo periferico. Viene creato tramite BluetoothDevice.connectGatt(), fornendo i metodi discoverServices(), readCharacteristic(), writeCharacteristic(), setCharacteristicNotification(). I risultati di tutte le operazioni arrivano in modo asincrono tramite BluetoothGattCallback. Senza BluetoothGatt, la comunicazione BLE bidirezionale su Android è impossibile.
Lo stato 133 (GATT_ERROR) indica un errore interno dello stack BLE Android. Cause: il dispositivo si è disconnesso durante la scoperta, il MTU è inferiore al minimo (23 byte) o lo stack BLE è sovraccarico. Soluzione: riprova discoverServices() con un ritardo di 500 ms, controlla l'RSSI del dispositivo e assicurati che la periferica supporti la scoperta GATT nel suo stato attuale.
Per scrivere con conferma, chiama writeCharacteristic() con WRITE_TYPE_DEFAULT (API 33+: BluetoothGattCharacteristicWriteRequest). In caso di successo, il dispositivo BLE invia una conferma e Android chiama onCharacteristicWrite con GATT_SUCCESS. Se il dispositivo non risponde entro 30 secondi (timeout dello stack), il callback restituisce uno stato di errore. Per il watchdog, usa Handler con postDelayed.
Su Android 13+ (API 33), i metodi BluetoothGatt sono cambiati: writeCharacteristic() ora accetta BluetoothGattCharacteristicWriteRequest, readCharacteristic() — BluetoothGattCharacteristicReadRequest. I vecchi setValue()/writeCharacteristic() sono deprecati. Anche BluetoothGattCallback è cambiato: onCharacteristicRead(), onCharacteristicWrite(), onCharacteristicChanged() ricevono ByteArray value e callbackType. Usa Build.VERSION.SDK_INT per la ramificazione.
Android supporta 4–8 connessioni BLE GATT simultanee (varia in base al produttore e alla versione di Android). Pixel/Google: fino a 7, Samsung: fino a 5, Xiaomi: fino a 4. Quando il limite viene superato, connectGatt restituisce null o onConnectionStateChange con un errore. Per lavorare con un gran numero di dispositivi, usa connessione ciclica o Bluetooth Mesh.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche