Conecta dispositivos Bluetooth

Para crear una conexión entre dos dispositivos, debes implementar los mecanismos del servidor y del cliente, ya que un dispositivo debe abrir un socket de servidor y el otro debe iniciar la conexión mediante la dirección MAC del dispositivo del servidor. El dispositivo del servidor y el dispositivo del cliente obtienen el BluetoothSocket requerido de diferentes maneras. El servidor recibe la información del socket cuando se acepta una conexión entrante. El cliente proporciona información del socket cuando abre un canal RFCOMM al servidor.

El servidor y el cliente se consideran conectados entre sí cuando tienen un BluetoothSocket conectado en el mismo canal RFCOMM. En este punto, cada dispositivo puede obtener flujos de entrada y salida, y se puede iniciar la transferencia de datos, que se analiza en la sección sobre cómo transferir datos de Bluetooth. En esta sección, se describe cómo iniciar la conexión entre dos dispositivos.

Asegúrate de tener los permisos de Bluetooth adecuados y configura tu app para Bluetooth antes de intentar encontrar dispositivos Bluetooth.

Técnicas de conexión

Una técnica de implementación consiste en preparar automáticamente cada dispositivo como un servidor, de modo que cada uno tenga un socket de servidor abierto y escuche las conexiones. En este caso, cualquiera de los dispositivos puede iniciar una conexión con el otro y convertirse en cliente. Como alternativa, un dispositivo puede alojar explícitamente la conexión y abrir un socket de servidor a pedido, y el otro dispositivo inicia la conexión.


Figura 1: Cuadro de diálogo de vinculación por Bluetooth

Conectarse como servidor

Cuando desees conectar dos dispositivos, uno deberá actuar como servidor y mantener un BluetoothServerSocket abierto. El propósito del socket de servidor es escuchar las solicitudes de conexión entrantes y proporcionar un BluetoothSocket conectado cuando se acepta una solicitud. Cuando el BluetoothSocket se adquiere del BluetoothServerSocket, el BluetoothServerSocket puede (y debería) descartarse, a menos que desees que el dispositivo acepte más conexiones.

Para configurar un socket de servidor y aceptar una conexión, completa la siguiente secuencia de pasos:

  1. Llama a listenUsingRfcommWithServiceRecord(String, UUID) para obtener un BluetoothServerSocket.

    La string es un nombre de identificación de tu servicio, que el sistema escribe automáticamente en una nueva entrada de la base de datos del protocolo de detección de servicios (SDP) en el dispositivo. El nombre es arbitrario y puede ser simplemente el nombre de tu app. El identificador único universal (UUID) también se incluye en la entrada del SDP y constituye la base del acuerdo de conexión con el dispositivo del cliente. Es decir, cuando el cliente intenta conectarse con este dispositivo, lleva un UUID que identifica de manera única el servicio con el que desea conectarse. Estos UUID deben coincidir para que se acepte la conexión.

    Un UUID es un formato estandarizado de 128 bits para un ID de cadena que se usa con el objetivo de identificar información de manera única. Un UUID se usa para identificar la información que debe ser única dentro de un sistema o una red porque la probabilidad de que se repita un UUID es efectivamente cero. Se generan de forma independiente, sin el uso de una autoridad centralizada. En este caso, se usa para identificar de manera única el servicio Bluetooth de tu app. A fin de obtener un UUID para usar con tu app, puedes utilizar uno de los numerosos generadores aleatorios de UUID en la Web y, luego, inicializar un UUID con fromString(String).

  2. Llama a accept() para comenzar a detectar solicitudes de conexión.

    Esta es una llamada de bloqueo. Se muestra cuando se acepta una conexión o se produce una excepción. Una conexión se acepta solo cuando un dispositivo remoto envía una solicitud de conexión que contiene un UUID que coincide con el que se registró con este socket de servidor de escucha. Si se ejecuta de forma correcta, accept() muestra un BluetoothSocket conectado.

  3. A menos que desees aceptar conexiones adicionales, llama a close().

    Este método libera el socket de servidor y todos sus recursos, pero no cierra el BluetoothSocket conectado que muestra accept(). A diferencia de TCP/IP, RFCOMM solo permite un cliente conectado por canal a la vez, por lo que en la mayoría de los casos tiene sentido llamar a close() en BluetoothServerSocket inmediatamente después de aceptar un socket conectado.

Debido a que la llamada a accept() es de bloqueo, no la ejecutes en el subproceso de IU de la actividad principal. Ejecutarla en otro subproceso garantiza que tu app pueda responder a otras interacciones del usuario. Por lo general, conviene hacer todo el trabajo que involucra un BluetoothServerSocket o BluetoothSocket en un subproceso nuevo que administra tu app. Para anular una llamada bloqueada, como accept(), llama a close() en BluetoothServerSocket o BluetoothSocket desde otro subproceso. Ten en cuenta que todos los métodos en BluetoothServerSocket o BluetoothSocket cuentan con protección de subprocesos.

Ejemplo

A continuación, se muestra un subproceso simplificado para el componente de servidor que acepta conexiones entrantes:

Kotlin

private inner class AcceptThread : Thread() {

   private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
       bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
   }

   override fun run() {
       // Keep listening until exception occurs or a socket is returned.
       var shouldLoop = true
       while (shouldLoop) {
           val socket: BluetoothSocket? = try {
               mmServerSocket?.accept()
           } catch (e: IOException) {
               Log.e(TAG, "Socket's accept() method failed", e)
               shouldLoop = false
               null
           }
           socket?.also {
               manageMyConnectedSocket(it)
               mmServerSocket?.close()
               shouldLoop = false
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   fun cancel() {
       try {
           mmServerSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the connect socket", e)
       }
   }
}

Java

private class AcceptThread extends Thread {
   private final BluetoothServerSocket mmServerSocket;

   public AcceptThread() {
       // Use a temporary object that is later assigned to mmServerSocket
       // because mmServerSocket is final.
       BluetoothServerSocket tmp = null;
       try {
           // MY_UUID is the app's UUID string, also used by the client code.
           tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's listen() method failed", e);
       }
       mmServerSocket = tmp;
   }

   public void run() {
       BluetoothSocket socket = null;
       // Keep listening until exception occurs or a socket is returned.
       while (true) {
           try {
               socket = mmServerSocket.accept();
           } catch (IOException e) {
               Log.e(TAG, "Socket's accept() method failed", e);
               break;
           }

           if (socket != null) {
               // A connection was accepted. Perform work associated with
               // the connection in a separate thread.
               manageMyConnectedSocket(socket);
               mmServerSocket.close();
               break;
           }
       }
   }

   // Closes the connect socket and causes the thread to finish.
   public void cancel() {
       try {
           mmServerSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the connect socket", e);
       }
   }
}

En este ejemplo, solo se desea una conexión entrante, por lo que tan pronto como se acepta una conexión y se adquiere BluetoothSocket, la app pasa el BluetoothSocket adquirido a un subproceso separado, cierra BluetoothServerSocket y sale del bucle.

Ten en cuenta que, cuando accept() muestra el BluetoothSocket, el socket ya está conectado. Por lo tanto, no debes llamar a connect(), como lo haces del lado del cliente.

El método manageMyConnectedSocket() específico de la app está diseñado para iniciar el subproceso de transferencia de datos, que se analiza en el tema sobre la transferencia de datos de Bluetooth.

Por lo general, debes cerrar tu BluetoothServerSocket en cuanto termines de escuchar las conexiones entrantes. En este ejemplo, se llama a close() en cuanto se adquiere el objeto BluetoothSocket. También te recomendamos que proporciones un método público en tu subproceso que pueda cerrar el BluetoothSocket privado en caso de que necesites dejar de escuchar en ese socket de servidor.

Conectarse como cliente

Para iniciar una conexión con un dispositivo remoto que acepta conexiones en un socket de servidor abierto, primero debes obtener un objeto BluetoothDevice que represente al dispositivo remoto. Para obtener información sobre cómo crear un BluetoothDevice, consulta Cómo buscar dispositivos Bluetooth. Luego, debes usar BluetoothDevice para adquirir un BluetoothSocket e iniciar la conexión.

El procedimiento básico es el siguiente:

  1. Con el BluetoothDevice, obtén un BluetoothSocket llamando a createRfcommSocketToServiceRecord(UUID).

    Este método inicializa un objeto BluetoothSocket que permite al cliente conectarse a un BluetoothDevice. El UUID que se pasa aquí debe coincidir con el que usó el dispositivo del servidor cuando llamó a listenUsingRfcommWithServiceRecord(String, UUID) para abrir su BluetoothServerSocket. Para usar un UUID que coincida, codifica la string del UUID en tu app y, luego, haz referencia a ella desde el código del servidor y del cliente.

  2. Para iniciar la conexión, llama a connect(). Ten en cuenta que este método es una llamada de bloqueo.

    Después de que un cliente llama a este método, el sistema realiza una búsqueda del SDP para encontrar el dispositivo remoto con el UUID que coincide. Si la búsqueda se realiza correctamente y el dispositivo remoto acepta la conexión, este comparte el canal RFCOMM que se usará durante la conexión, y se muestra el método connect(). Si la conexión falla o si se agota el tiempo de espera del método connect() (después de unos 12 segundos), el método arroja una IOException.

Como connect() es una llamada de bloqueo, siempre debes realizar este procedimiento de conexión en un subproceso independiente del subproceso de la IU de la actividad principal.

Ejemplo

A continuación, se muestra un ejemplo básico de un subproceso de cliente que inicia una conexión Bluetooth:

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

   private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
       device.createRfcommSocketToServiceRecord(MY_UUID)
   }

   public override fun run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter?.cancelDiscovery()

       mmSocket?.let { socket ->
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           socket.connect()

           // The connection attempt succeeded. Perform work associated with
           // the connection in a separate thread.
           manageMyConnectedSocket(socket)
       }
   }

   // Closes the client socket and causes the thread to finish.
   fun cancel() {
       try {
           mmSocket?.close()
       } catch (e: IOException) {
           Log.e(TAG, "Could not close the client socket", e)
       }
   }
}

Java

private class ConnectThread extends Thread {
   private final BluetoothSocket mmSocket;
   private final BluetoothDevice mmDevice;

   public ConnectThread(BluetoothDevice device) {
       // Use a temporary object that is later assigned to mmSocket
       // because mmSocket is final.
       BluetoothSocket tmp = null;
       mmDevice = device;

       try {
           // Get a BluetoothSocket to connect with the given BluetoothDevice.
           // MY_UUID is the app's UUID string, also used in the server code.
           tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
       } catch (IOException e) {
           Log.e(TAG, "Socket's create() method failed", e);
       }
       mmSocket = tmp;
   }

   public void run() {
       // Cancel discovery because it otherwise slows down the connection.
       bluetoothAdapter.cancelDiscovery();

       try {
           // Connect to the remote device through the socket. This call blocks
           // until it succeeds or throws an exception.
           mmSocket.connect();
       } catch (IOException connectException) {
           // Unable to connect; close the socket and return.
           try {
               mmSocket.close();
           } catch (IOException closeException) {
               Log.e(TAG, "Could not close the client socket", closeException);
           }
           return;
       }

       // The connection attempt succeeded. Perform work associated with
       // the connection in a separate thread.
       manageMyConnectedSocket(mmSocket);
   }

   // Closes the client socket and causes the thread to finish.
   public void cancel() {
       try {
           mmSocket.close();
       } catch (IOException e) {
           Log.e(TAG, "Could not close the client socket", e);
       }
   }
}

Ten en cuenta que, en este fragmento, se llama a cancelDiscovery() antes de que se produzca el intento de conexión. Siempre debes llamar a cancelDiscovery() antes de connect(), especialmente porque cancelDiscovery() funciona correctamente independientemente de si el descubrimiento de dispositivos está en curso. Si tu app necesita determinar si el descubrimiento del dispositivo está en curso, puedes verificarlo con isDiscovering().

El método manageMyConnectedSocket() específico de la app está diseñado para iniciar el subproceso de transferencia de datos, que se analiza en la sección sobre la transferencia de datos de Bluetooth.

Cuando termines con tu BluetoothSocket, siempre llama a close(). Cuando lo hagas, se cerrará de inmediato el socket conectado y se liberarán todos los recursos internos relacionados.