Una vez que te conectas a un servidor BLE GATT, puedes usar la conexión para averiguar qué servicios están disponibles en el dispositivo, consultar datos del dispositivo y solicitar notificaciones cuando cambia una característica GATT determinada.
Descubre servicios
Lo primero que debes hacer una vez que te conectas al servidor GATT en el dispositivo BLE es realizar el descubrimiento de servicios. Esto proporciona información sobre los servicios disponibles en el dispositivo remoto, así como las características del servicio y sus descriptores. En el siguiente ejemplo, una vez que el servicio se conecta correctamente al dispositivo (indicado por la llamada apropiada a la función onConnectionStateChange()
de BluetoothGattCallback
), la función discoverServices()
consulta la información del dispositivo BLE.
El servicio debe anular la función onServicesDiscovered()
en BluetoothGattCallback
.
Se llama a esta función cuando el dispositivo informa sobre sus servicios disponibles.
Kotlin
class BluetoothLeService : Service() { ... 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 broadcastUpdate(ACTION_GATT_CONNECTED) connectionState = STATE_CONNECTED // Attempts to discover services after successful connection. bluetoothGatt?.discoverServices() } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server broadcastUpdate(ACTION_GATT_DISCONNECTED) connectionState = STATE_DISCONNECTED } } override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED) } else { Log.w(BluetoothLeService.TAG, "onServicesDiscovered received: $status") } } } ... 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" const val ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED" private const val STATE_DISCONNECTED = 0 private const val STATE_CONNECTED = 2 }
Java
class BluetoothLeService extends Service { public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED"; ... private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { // successfully connected to the GATT Server connectionState = STATE_CONNECTED; broadcastUpdate(ACTION_GATT_CONNECTED); // Attempts to discover services after successful connection. bluetoothGatt.discoverServices(); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server connectionState = STATE_DISCONNECTED; broadcastUpdate(ACTION_GATT_DISCONNECTED); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } }; }
El servicio usa transmisiones para notificar la actividad. Una vez que se descubren los servicios, este puede llamar a getServices()
para obtener los datos informados.
Kotlin
class BluetoothLeService : Service() { ... fun getSupportedGattServices(): List<BluetoothGattService?>? { return bluetoothGatt?.services } }
Java
class BluetoothLeService extends Service { ... public List<BluetoothGattService> getSupportedGattServices() { if (bluetoothGatt == null) return null; return bluetoothGatt.getServices(); } }
Luego, la actividad puede llamar a esta función cuando recibe el intent de transmisión, lo que indica que el descubrimiento del servicio finalizó.
Kotlin
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) } BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED -> { // Show all the supported services and characteristics on the user interface. displayGattServices(bluetoothService?.getSupportedGattServices()) } } } } }
Java
class DeviceControlsActivity extends AppCompatActivity { ... private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { connected = true; updateConnectionState(R.string.connected); } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { connected = false; updateConnectionState(R.string.disconnected); } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { // Show all the supported services and characteristics on the user interface. displayGattServices(bluetoothService.getSupportedGattServices()); } } }; }
Leer características de BLE
Una vez que tu app se haya conectado a un servidor GATT y haya detectado servicios, podrá leer y escribir atributos, cuando sean compatibles. Por ejemplo, el siguiente fragmento itera a través de los servicios y las características del servidor y los muestra en la IU:
Kotlin
class DeviceControlActivity : Activity() { // Demonstrates how to iterate through the supported GATT // Services/Characteristics. // In this sample, we populate the data structure that is bound to the // ExpandableListView on the UI. private fun displayGattServices(gattServices: List<BluetoothGattService>?) { if (gattServices == null) return var uuid: String? val unknownServiceString: String = resources.getString(R.string.unknown_service) val unknownCharaString: String = resources.getString(R.string.unknown_characteristic) val gattServiceData: MutableList<HashMap<String, String>> = mutableListOf() val gattCharacteristicData: MutableList<ArrayList<HashMap<String, String>>> = mutableListOf() mGattCharacteristics = mutableListOf() // Loops through available GATT Services. gattServices.forEach { gattService -> val currentServiceData = HashMap<String, String>() uuid = gattService.uuid.toString() currentServiceData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownServiceString) currentServiceData[LIST_UUID] = uuid gattServiceData += currentServiceData val gattCharacteristicGroupData: ArrayList<HashMap<String, String>> = arrayListOf() val gattCharacteristics = gattService.characteristics val charas: MutableList<BluetoothGattCharacteristic> = mutableListOf() // Loops through available Characteristics. gattCharacteristics.forEach { gattCharacteristic -> charas += gattCharacteristic val currentCharaData: HashMap<String, String> = hashMapOf() uuid = gattCharacteristic.uuid.toString() currentCharaData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownCharaString) currentCharaData[LIST_UUID] = uuid gattCharacteristicGroupData += currentCharaData } mGattCharacteristics += charas gattCharacteristicData += gattCharacteristicGroupData } } }
Java
public class DeviceControlActivity extends Activity { ... // Demonstrates how to iterate through the supported GATT // Services/Characteristics. // In this sample, we populate the data structure that is bound to the // ExpandableListView on the UI. private void displayGattServices(List<BluetoothGattService> gattServices) { if (gattServices == null) return; String uuid = null; String unknownServiceString = getResources(). getString(R.string.unknown_service); String unknownCharaString = getResources(). getString(R.string.unknown_characteristic); ArrayList<HashMap<String, String>> gattServiceData = new ArrayList<HashMap<String, String>>(); ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData = new ArrayList<ArrayList<HashMap<String, String>>>(); mGattCharacteristics = new ArrayList<ArrayList<BluetoothGattCharacteristic>>(); // Loops through available GATT Services. for (BluetoothGattService gattService : gattServices) { HashMap<String, String> currentServiceData = new HashMap<String, String>(); uuid = gattService.getUuid().toString(); currentServiceData.put( LIST_NAME, SampleGattAttributes. lookup(uuid, unknownServiceString)); currentServiceData.put(LIST_UUID, uuid); gattServiceData.add(currentServiceData); ArrayList<HashMap<String, String>> gattCharacteristicGroupData = new ArrayList<HashMap<String, String>>(); List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); ArrayList<BluetoothGattCharacteristic> charas = new ArrayList<BluetoothGattCharacteristic>(); // Loops through available Characteristics. for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { charas.add(gattCharacteristic); HashMap<String, String> currentCharaData = new HashMap<String, String>(); uuid = gattCharacteristic.getUuid().toString(); currentCharaData.put( LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString)); currentCharaData.put(LIST_UUID, uuid); gattCharacteristicGroupData.add(currentCharaData); } mGattCharacteristics.add(charas); gattCharacteristicData.add(gattCharacteristicGroupData); } ... } ... }
El servicio GATT proporciona una lista de características que puedes leer del dispositivo. Para consultar los datos, llama a la función readCharacteristic()
en BluetoothGatt
y pasa el BluetoothGattCharacteristic
que desees leer.
Kotlin
class BluetoothLeService : Service() { ... fun readCharacteristic(characteristic: BluetoothGattCharacteristic) { bluetoothGatt?.let { gatt -> gatt.readCharacteristic(characteristic) } ?: run { Log.w(TAG, "BluetoothGatt not initialized") Return } } }
Java
class BluetoothLeService extends Service { ... public void readCharacteristic(BluetoothGattCharacteristic characteristic) { if (bluetoothGatt == null) { Log.w(TAG, "BluetoothGatt not initialized"); return; } bluetoothGatt.readCharacteristic(characteristic); } }
En este ejemplo, el servicio implementa una función para llamar a readCharacteristic()
.
Esta es una llamada asíncrona. Los resultados se envían a la función BluetoothGattCallback
onCharacteristicRead()
.
Kotlin
class BluetoothLeService : Service() { ... private val bluetoothGattCallback = object : BluetoothGattCallback() { ... override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(BluetoothLeService.ACTION_DATA_AVAILABLE, characteristic) } } } }
Java
class BluetoothLeService extends Service { ... private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() { ... @Override public void onCharacteristicRead( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status ) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } }; }
Cuando se activa una devolución de llamada específica, esta llama al método auxiliar broadcastUpdate()
apropiado y le pasa una acción. Ten en cuenta que el análisis de datos en esta sección se realiza de acuerdo con las especificaciones del perfil de medición de frecuencia cardíaca Bluetooth.
Kotlin
private fun broadcastUpdate(action: String, characteristic: BluetoothGattCharacteristic) { val intent = Intent(action) // This is special handling for the Heart Rate Measurement profile. Data // parsing is carried out as per profile specifications. when (characteristic.uuid) { UUID_HEART_RATE_MEASUREMENT -> { val flag = characteristic.properties val format = when (flag and 0x01) { 0x01 -> { Log.d(TAG, "Heart rate format UINT16.") BluetoothGattCharacteristic.FORMAT_UINT16 } else -> { Log.d(TAG, "Heart rate format UINT8.") BluetoothGattCharacteristic.FORMAT_UINT8 } } val heartRate = characteristic.getIntValue(format, 1) Log.d(TAG, String.format("Received heart rate: %d", heartRate)) intent.putExtra(EXTRA_DATA, (heartRate).toString()) } else -> { // For all other profiles, writes the data formatted in HEX. val data: ByteArray? = characteristic.value if (data?.isNotEmpty() == true) { val hexString: String = data.joinToString(separator = " ") { String.format("%02X", it) } intent.putExtra(EXTRA_DATA, "$data\n$hexString") } } } sendBroadcast(intent) }
Java
private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { final Intent intent = new Intent(action); // This is special handling for the Heart Rate Measurement profile. Data // parsing is carried out as per profile specifications. if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { int flag = characteristic.getProperties(); int format = -1; if ((flag & 0x01) != 0) { format = BluetoothGattCharacteristic.FORMAT_UINT16; Log.d(TAG, "Heart rate format UINT16."); } else { format = BluetoothGattCharacteristic.FORMAT_UINT8; Log.d(TAG, "Heart rate format UINT8."); } final int heartRate = characteristic.getIntValue(format, 1); Log.d(TAG, String.format("Received heart rate: %d", heartRate)); intent.putExtra(EXTRA_DATA, String.valueOf(heartRate)); } else { // For all other profiles, writes the data formatted in HEX. final byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { final StringBuilder stringBuilder = new StringBuilder(data.length); for(byte byteChar : data) stringBuilder.append(String.format("%02X ", byteChar)); intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); } } sendBroadcast(intent); }
Recibir notificaciones GATT
Es común que las apps BLE soliciten recibir notificaciones cuando cambia una característica particular en el dispositivo. En el siguiente ejemplo, el servicio implementa una función para llamar al método setCharacteristicNotification()
:
Kotlin
class BluetoothLeService : Service() { ... fun setCharacteristicNotification( characteristic: BluetoothGattCharacteristic, enabled: Boolean ) { bluetoothGatt?.let { gatt -> gatt.setCharacteristicNotification(characteristic, enabled) // This is specific to Heart Rate Measurement. if (BluetoothLeService.UUID_HEART_RATE_MEASUREMENT == characteristic.uuid) { val descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)) descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor) } } ?: run { Log.w(BluetoothLeService.TAG, "BluetoothGatt not initialized") } } }
Java
class BluetoothLeService extends Service { ... public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,boolean enabled) { if (bluetoothGatt == null) { Log.w(TAG, "BluetoothGatt not initialized"); Return; } bluetoothGatt.setCharacteristicNotification(characteristic, enabled); // This is specific to Heart Rate Measurement. if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); bluetoothGatt.writeDescriptor(descriptor); } } }
Una vez que se habilitan las notificaciones para una característica, se activa una devolución de llamada onCharacteristicChanged()
si la característica cambia en el dispositivo remoto:
Kotlin
class BluetoothLeService : Service() { ... private val bluetoothGattCallback = object : BluetoothGattCallback() { ... override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) } } }
Java
class BluetoothLeService extends Service { ... private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() { ... @Override public void onCharacteristicChanged( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic ) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } }; }