El primer paso para interactuar con un dispositivo BLE es conectarse a él. Más específicamente, la conexión al servidor GATT en el dispositivo. Para conectarte a un servidor GATT en un dispositivo BLE, usa el método connectGatt()
. Este método tiene tres parámetros: un objeto Context
, autoConnect
(un valor booleano que indica si se conecta automáticamente al dispositivo BLE en cuanto esté disponible) y una referencia a un BluetoothGattCallback
:
Kotlin
var bluetoothGatt: BluetoothGatt? = null ... bluetoothGatt = device.connectGatt(this, false, gattCallback)
Java
bluetoothGatt = device.connectGatt(this, false, gattCallback);
Esto se conecta al servidor GATT alojado en el dispositivo BLE y muestra una instancia de BluetoothGatt
, que luego puedes usar para realizar operaciones de cliente GATT. El llamador (la app para Android) es el cliente GATT. BluetoothGattCallback
se usa para entregar resultados al cliente, como el estado de conexión y cualquier otra operación de cliente GATT.
Cómo configurar un servicio vinculado
En el siguiente ejemplo, la app BLE proporciona una actividad (DeviceControlActivity
) para conectarse a dispositivos Bluetooth, mostrar datos del dispositivo y visualizar 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 usando un servicio vinculado que permite que la actividad se conecte a BluetoothLeService
y llame a funciones para conectarse a los dispositivos. BluetoothLeService
necesita una implementación de Binder
que proporcione acceso al servicio para la actividad.
Kotlin
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 } } }
Java
class BluetoothLeService extends Service { private Binder binder = new LocalBinder(); @Nullable @Override public IBinder onBind(Intent intent) { return binder; } class LocalBinder extends Binder { public BluetoothLeService getService() { return BluetoothLeService.this; } } }
La actividad puede iniciar el servicio usando bindService()
, pasando un elemento 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.
Kotlin
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) } }
Java
class DeviceControlActivity extends AppCompatActivity { private BluetoothLeService bluetoothService; private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bluetoothService = ((LocalBinder) service).getService(); if (bluetoothService != null) { // call functions on service to check connection and connect to devices } } @Override public void onServiceDisconnected(ComponentName name) { bluetoothService = null; } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.gatt_services_characteristics); Intent gattServiceIntent = new Intent(this, BluetoothLeService.class); bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE); } }
Cómo configurar BluetoothAdapter
Una vez que el servicio está vinculado, necesita acceder a BluetoothAdapter
. Se debería verificar que el adaptador esté disponible en el dispositivo. Consulta Cómo configurar Bluetooth para obtener más información sobre BluetoothAdapter
. En el siguiente ejemplo, se une este código de configuración en una función initialize()
que muestra un valor Boolean
que indica que la prueba se realizó correctamente.
Kotlin
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 } ... }
Java
class BluetoothLeService extends Service { public static final String TAG = "BluetoothLeService"; private BluetoothAdapter bluetoothAdapter; public boolean initialize() { 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
.
Manejar un valor falso que se muestra de la función initialize()
depende de tu aplicación. Puedes mostrarle un mensaje de error al usuario en el que se indique que el dispositivo actual no admite el funcionamiento de Bluetooth o inhabilitar alguna función que requiera Bluetooth para funcionar. En el siguiente ejemplo, se llama a finish()
en la actividad para enviar al usuario a la pantalla anterior.
Kotlin
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 } } ... }
Java
class DeviceControlsActivity extends AppCompatActivity { private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bluetoothService = ((LocalBinder) service).getService(); if (bluetoothService != null) { if (!bluetoothService.initialize()) { Log.e(TAG, "Unable to initialize Bluetooth"); finish(); } // perform device connection } } @Override public void onServiceDisconnected(ComponentName name) { bluetoothService = null; } }; ... }
Conectarse a un dispositivo
Una vez que se inicializa la instancia BluetoothLeService
, puede conectarse 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 una IllegalArgumentException
.
Kotlin
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 } ?: run { Log.w(TAG, "BluetoothAdapter not initialized") return false } }
Java
public boolean connect(final String address) { if (bluetoothAdapter == null || address == null) { Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); return false; } try { final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); } catch (IllegalArgumentException exception) { Log.w(TAG, "Device not found with provided address."); return false; } // connect to the GATT server on the device }
El 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 intent adicional.
Kotlin
// 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 bluetooth.connect(deviceAddress) } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } }
Java
private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bluetoothService = ((LocalBinder) service).getService(); if (bluetoothService != null) { if (!bluetoothService.initialize()) { Log.e(TAG, "Unable to initialize Bluetooth"); finish(); } // perform device connection bluetoothService.connect(deviceAddress); } } @Override public void onServiceDisconnected(ComponentName name) { bluetoothService = null; } };
Cómo declarar la devolución de llamada GATT
Una vez que la actividad le indica al servicio a qué dispositivo conectarse y que el servicio se conecta a él, el servicio debe conectarse al servidor GATT en el dispositivo BLE. Esta conexión requiere un BluetoothGattCallback
para recibir notificaciones sobre el estado de conexión, el descubrimiento del servicio, las lecturas de características y las notificaciones de características.
En este tema, nos centraremos en las notificaciones del estado de conexión. Consulta Transfiere datos de BLE para obtener información sobre cómo realizar el descubrimiento de servicios y lecturas de características, y solicitar notificaciones de características.
La función onConnectionStateChange()
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 BluetoothDevice
una vez que el servicio se conecte a ella.
Kotlin
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 } } }
Java
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 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server } } };
Conéctate al servicio GATT
Una vez que se declara el BluetoothGattCallback
, el servicio puede usar el objeto BluetoothDevice
de la función connect()
para conectarse al servicio GATT en el dispositivo.
Se usa la función connectGatt()
. Esto requiere un objeto Context
, una marca booleana autoConnect
y 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.
Kotlin
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 } } }
Java
class BluetoothLeService extends Service { ... private BluetoothGatt bluetoothGatt; ... public boolean connect(final String address) { if (bluetoothAdapter == null || address == null) { Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); return false; } try { final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address); // connect to the GATT server on the device bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback); return true; } catch (IllegalArgumentException exception) { Log.w(TAG, "Device not found with provided address. Unable to connect."); return false; } } }
Actualizaciones de anuncios
Cuando el servidor se conecta o se desconecta del servidor GATT, debe notificar a la actividad del estado nuevo. Hay varias formas de lograr esto. 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 que se transmita al sistema.
Kotlin
private fun broadcastUpdate(action: String) { val intent = Intent(action) sendBroadcast(intent) }
Java
private void broadcastUpdate(final String action) { final Intent intent = new Intent(action); sendBroadcast(intent); }
Una vez que se implementa la función de emisión, se utiliza en BluetoothGattCallback
para enviar información sobre el estado de 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
.
Kotlin
class BluetoothLeService : Service() { private var connectionState = STATE_DISCONNECTED private val bluetoothGattCallback: 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 } }
Java
class BluetoothLeService extends Service { public final static String ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"; public final static String ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"; private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTED = 2; private int connectionState; ... 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); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server connectionState = STATE_DISCONNECTED; broadcastUpdate(ACTION_GATT_DISCONNECTED); } } }; … }
Cómo detectar 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 cancela el registro cuando esta salga de la pantalla. Cuando escuchas los eventos del servicio, la actividad puede actualizar la interfaz de usuario en función del estado de conexión actual con el dispositivo BLE.
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) } } } } override fun onResume() { super.onResume() registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter()) if (bluetoothService != null) { val result = bluetoothService!!.connect(deviceAddress) Log.d(DeviceControlsActivity.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) } } }
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); } } }; @Override protected void onResume() { super.onResume(); registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter()); if (bluetoothService != null) { final boolean result = bluetoothService.connect(deviceAddress); Log.d(TAG, "Connect request result=" + result); } } @Override protected void onPause() { super.onPause(); unregisterReceiver(gattUpdateReceiver); } private static IntentFilter makeGattUpdateIntentFilter() { final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED); intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED); return intentFilter; } }
En Transfiere datos BLE, el BroadcastReceiver
también se usa para comunicar el descubrimiento del servicio y los datos de características del dispositivo.
Cerrar conexión GATT
Un paso importante cuando trabajas con conexiones Bluetooth es cerrar la conexión cuando hayas terminado. Para ello, llama a la función close()
en el objeto BluetoothGatt
. En el siguiente ejemplo, el servicio contiene la referencia al BluetoothGatt
. Cuando la actividad se desvincula del servicio, se cierra la conexión para evitar que se agote la batería del dispositivo.
Kotlin
class BluetoothLeService : Service() { ... override fun onUnbind(intent: Intent?): Boolean { close() return super.onUnbind(intent) } private fun close() { bluetoothGatt?.let { gatt -> gatt.close() bluetoothGatt = null } } }
Java
class BluetoothLeService extends Service { ... @Override public boolean onUnbind(Intent intent) { close(); return super.onUnbind(intent); } private void close() { if (bluetoothGatt == null) { Return; } bluetoothGatt.close(); bluetoothGatt = null; } }