Connetti dispositivi Bluetooth

Per creare una connessione tra due dispositivi, devi implementare entrambi i meccanismi lato server e lato client, perché un dispositivo deve aprire un socket server e l'altro deve avviare la connessione utilizzando l'indirizzo MAC del dispositivo server. Il dispositivo server e il dispositivo client ricevono ciascuno il dispositivo BluetoothSocket richiesto in modi diversi. Il server riceve informazioni sul socket quando viene accettata una connessione in entrata. Il client fornisce informazioni sul socket quando apre un canale RFCOMM sul server.

Il server e il client vengono considerati connessi tra loro quando hanno entrambi un BluetoothSocket connesso sullo stesso canale RFCOMM. A questo punto, ogni dispositivo può ottenere flussi di input e di output e può iniziare il trasferimento di dati, come descritto nella sezione relativa al trasferimento dei dati Bluetooth. Questa sezione descrive come avviare la connessione tra due dispositivi.

Assicurati di disporre delle autorizzazioni Bluetooth appropriate e di configurare la tua app per il Bluetooth prima di tentare di trovare i dispositivi Bluetooth.

Tecniche di connessione

Una tecnica di implementazione consiste nel preparare automaticamente ogni dispositivo come server, in modo che ogni dispositivo abbia un socket server aperto e in ascolto delle connessioni. In questo caso, uno dei due dispositivi può avviare una connessione con l'altro e diventare il client. In alternativa, un dispositivo può ospitare esplicitamente la connessione e aprire un socket server on demand e l'altro dispositivo avvia la connessione.


Figura 1. La finestra di dialogo di accoppiamento Bluetooth.

Connetti come server

Quando vuoi connettere due dispositivi, uno deve fungere da server tenendo premuto un elemento BluetoothServerSocket aperto. Lo scopo del socket del server è ascoltare le richieste di connessione in entrata e fornire un BluetoothSocket connesso dopo l'accettazione di una richiesta. Quando l'elemento BluetoothSocket viene acquisito da BluetoothServerSocket, BluetoothServerSocket può e deve essere eliminato, a meno che tu non voglia che il dispositivo accetti più connessioni.

Per configurare un socket del server e accettare una connessione, segui questi passaggi:

  1. Ricevi un BluetoothServerSocket chiamando il numero listenUsingRfcommWithServiceRecord(String, UUID).

    La stringa è un nome identificabile del servizio, che il sistema scrive automaticamente in una nuova voce del database Service Discovery Protocol (SDP) sul dispositivo. Il nome è arbitrario e può essere semplicemente il nome dell'app. L'UUID (Universally Unique Identifier) è anche incluso nella voce SDP e costituisce la base del contratto di connessione con il dispositivo client. In altre parole, quando il client tenta di connettersi con questo dispositivo, porta un UUID che identifica in modo univoco il servizio con cui vuole connettersi. Questi UUID devono corrispondere affinché la connessione venga accettata.

    Un UUID è un formato standardizzato a 128 bit per un ID stringa utilizzato per identificare in modo univoco le informazioni. L'UUID viene utilizzato per identificare le informazioni che devono essere univoche all'interno di un sistema o di una rete perché la probabilità che un UUID venga ripetuto è di fatto zero. Viene generato in modo indipendente, senza l'uso di un'autorità centralizzata. In questo caso, viene usato per identificare in modo univoco il servizio Bluetooth dell'app. Per ottenere un UUID da utilizzare con la tua app, puoi usare uno dei tanti generatori casuali di UUID sul web e inizializzare un UUID con fromString(String).

  2. Inizia ad ascoltare le richieste di connessione chiamando il numero accept().

    Questa è una chiamata di blocco. Viene restituito quando una connessione è stata accettata o si è verificata un'eccezione. Una connessione viene accettata solo quando un dispositivo remoto ha inviato una richiesta di connessione contenente un UUID corrispondente a quello registrato con questo socket del server di ascolto. Se l'operazione ha esito positivo, accept() restituisce un BluetoothSocket connesso.

  3. A meno che tu non voglia accettare altre connessioni, chiama close().

    Questa chiamata di metodo rilascia il socket del server e tutte le relative risorse, ma non chiude il BluetoothSocket connesso restituito da accept(). A differenza di TCP/IP, RFCOMM consente un solo client connesso per canale alla volta. Di conseguenza, nella maggior parte dei casi ha senso chiamare close() su BluetoothServerSocket immediatamente dopo aver accettato un socket connesso.

Poiché la chiamata accept() è una chiamata di blocco, non eseguirla nel thread dell'interfaccia utente delle attività principale. L'esecuzione in un altro thread garantisce che l'app possa ancora rispondere alle interazioni di altri utenti. Di solito ha senso svolgere tutte le operazioni che coinvolgono BluetoothServerSocket o BluetoothSocket in un nuovo thread gestito dalla tua app. Per interrompere una chiamata bloccata come accept(), chiama close() su BluetoothServerSocket o BluetoothSocket da un altro thread. Tieni presente che tutti i metodi in un BluetoothServerSocket o BluetoothSocket sono thread-safe.

Esempio

Di seguito è riportato un thread semplificato per il componente server che accetta le connessioni in entrata:

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);
       }
   }
}

In questo esempio, è desiderata una sola connessione in entrata. Pertanto, non appena viene accettata una connessione e viene acquisito BluetoothSocket, l'app passa l'oggetto BluetoothSocket acquisito a un thread separato, chiude la BluetoothServerSocket ed esce dal loop.

Tieni presente che quando accept() restituisce BluetoothSocket, il socket è già collegato. Pertanto, non dovresti chiamare connect(), come fai dal lato client.

Il metodo manageMyConnectedSocket() specifico dell'app è progettato per avviare il thread per il trasferimento dei dati, di cui si parlerà nell'argomento relativo al trasferimento dei dati Bluetooth.

In genere, devi chiudere BluetoothServerSocket non appena hai finito di ascoltare le connessioni in entrata. In questo esempio, close() viene chiamato non appena viene acquisito BluetoothSocket. Puoi anche fornire un metodo pubblico nel thread per chiudere il BluetoothSocket privato nel caso in cui tu debba interrompere l'ascolto sul socket del server.

Connetti come cliente

Per avviare una connessione con un dispositivo remoto che accetta connessioni su un socket server aperto, devi prima ottenere un oggetto BluetoothDevice che rappresenti il dispositivo remoto. Per informazioni su come creare un BluetoothDevice, consulta Trovare dispositivi Bluetooth. Devi quindi utilizzare BluetoothDevice per acquisire un BluetoothSocket e avviare la connessione.

La procedura di base è la seguente:

  1. Utilizzando il BluetoothDevice, ricevi un BluetoothSocket chiamando il numero createRfcommSocketToServiceRecord(UUID).

    Questo metodo inizializza un oggetto BluetoothSocket che consente al client di connettersi a un BluetoothDevice. L'UUID passato qui deve corrispondere all'UUID utilizzato dal dispositivo del server quando ha chiamato listenUsingRfcommWithServiceRecord(String, UUID) per aprire il relativo BluetoothServerSocket. Per utilizzare un UUID corrispondente, imposta la stringa UUID hardcoded nella tua app, quindi fai riferimento alla stringa sia dal codice server sia dal codice client.

  2. Avvia la connessione chiamando il numero connect(). Tieni presente che questo metodo è una chiamata che blocca la chiamata.

    Dopo che un client chiama questo metodo, il sistema esegue una ricerca SDP per trovare il dispositivo remoto con l'UUID corrispondente. Se la ricerca ha esito positivo e il dispositivo remoto accetta la connessione, condivide il canale RFCOMM da utilizzare durante la connessione e viene restituito il metodo connect(). Se la connessione non riesce o se il metodo connect() scade (dopo circa 12 secondi), il metodo genera un IOException.

Poiché connect() è una chiamata di blocco, devi sempre eseguire questa procedura di connessione in un thread separato dal thread dell'attività principale (UI).

Esempio

Di seguito è riportato un esempio base di thread client che avvia una connessione 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);
       }
   }
}

Tieni presente che in questo snippet, l'elemento cancelDiscovery() viene chiamato prima che si verifichi il tentativo di connessione. Devi sempre chiamare cancelDiscovery() prima del giorno connect(), soprattutto perché cancelDiscovery() ha esito positivo indipendentemente dal fatto che l'individuazione dei dispositivi sia attualmente in corso. Se l'app deve determinare se è in corso il rilevamento del dispositivo, puoi verificarlo utilizzando isDiscovering().

Il metodo manageMyConnectedSocket() specifico dell'app è progettato per avviare il thread per il trasferimento dei dati, di cui parleremo nella sezione relativa al trasferimento dei dati Bluetooth.

Quando hai finito con il tuo BluetoothSocket, chiama sempre close(). In questo modo si chiude immediatamente il socket connesso e si rilasciano tutte le risorse interne correlate.