Bluetooth-Geräte verbinden

Um eine Verbindung zwischen zwei Geräten herzustellen, müssen Sie sowohl den serverseitigen als auch den clientseitigen Mechanismus implementieren, da ein Gerät einen Server-Socket öffnen und das andere Gerät die Verbindung über die MAC-Adresse des Servergeräts initiieren muss. Die Server- und Clientgeräte erhalten das erforderliche BluetoothSocket jeweils auf unterschiedliche Weise. Der Server empfängt Socket-Informationen, wenn eine eingehende Verbindung akzeptiert wird. Der Client stellt Socket-Informationen bereit, wenn er einen RFCOMM-Kanal für den Server öffnet.

Der Server und der Client gelten als miteinander verbunden, wenn sie jeweils eine verbundene BluetoothSocket auf demselben RFCOMM-Kanal haben. An dieser Stelle kann jedes Gerät Eingabe- und Ausgabestreams erhalten und die Datenübertragung kann beginnen. Dies wird im Abschnitt zur Übertragung von Bluetooth-Daten erläutert. In diesem Abschnitt wird beschrieben, wie Sie die Verbindung zwischen zwei Geräten initiieren.

Prüfen Sie, ob Sie die entsprechenden Bluetooth-Berechtigungen haben und richten Sie Ihre App für Bluetooth ein, bevor Sie nach Bluetooth-Geräten suchen.

Verbindungsmethoden

Eine Implementierungstechnik besteht darin, jedes Gerät automatisch als Server vorzubereiten, sodass auf jedem Gerät ein Server-Socket geöffnet ist und Verbindungen überwacht werden. In diesem Fall kann jedes Gerät eine Verbindung mit dem anderen herstellen und zum Client werden. Alternativ kann ein Gerät die Verbindung explizit hosten und bei Bedarf einen Server-Socket öffnen. Das andere Gerät initiiert die Verbindung.


Abbildung 1: Das Dialogfeld für die Bluetooth-Kopplung.

Als Server verbinden

Wenn du zwei Geräte verbinden möchtest, muss eines als Server fungieren und dabei ein offenes BluetoothServerSocket halten. Der Server-Socket hat den Zweck, auf eingehende Verbindungsanfragen zu warten und eine verbundene BluetoothSocket bereitzustellen, nachdem eine Anfrage akzeptiert wurde. Wenn das BluetoothSocket vom BluetoothServerSocket übernommen wird, kann und sollte das BluetoothServerSocket verworfen werden, es sei denn, das Gerät soll weitere Verbindungen akzeptieren.

Führen Sie die folgenden Schritte aus, um einen Server-Socket einzurichten und eine Verbindung zu akzeptieren:

  1. Rufen Sie listenUsingRfcommWithServiceRecord(String, UUID) auf, um ein BluetoothServerSocket zu erhalten.

    Der String ist ein identifizierbarer Name Ihres Dienstes, den das System automatisch in einen neuen SDP-Datenbankeintrag (Service Discovery Protocol) auf dem Gerät schreibt. Der Name ist beliebig und kann einfach der Name Ihrer Anwendung sein. Die UUID (Universally Unique Identifier) ist ebenfalls im SDP-Eintrag enthalten und bildet die Grundlage für die Verbindungsvereinbarung mit dem Clientgerät. Das heißt, wenn der Client versucht, eine Verbindung zu diesem Gerät herzustellen, enthält er eine UUID, die den Dienst eindeutig identifiziert, zu dem er eine Verbindung herstellen möchte. Diese UUIDs müssen übereinstimmen, damit die Verbindung akzeptiert wird.

    Eine UUID ist ein standardisiertes 128-Bit-Format für eine String-ID, mit der Informationen eindeutig identifiziert werden. Mit einer UUID werden Informationen identifiziert, die innerhalb eines Systems oder Netzwerks eindeutig sein müssen, da die Wahrscheinlichkeit einer Wiederholung einer UUID praktisch null ist. Sie werden unabhängig und ohne Verwendung einer zentralen Behörde generiert. In diesem Fall wird sie verwendet, um den Bluetooth-Dienst Ihrer App eindeutig zu identifizieren. Um eine UUID für Ihre Anwendung zu erhalten, können Sie einen der vielen Zufallsgeneratoren für UUID im Web verwenden und dann eine UUID mit fromString(String) initialisieren.

  2. Starten Sie das Warten auf Verbindungsanfragen, indem Sie accept() aufrufen.

    Dieser Anruf wird blockiert. Sie gibt zurück, wenn entweder eine Verbindung akzeptiert oder eine Ausnahme aufgetreten ist. Eine Verbindung wird nur akzeptiert, wenn ein Remote-Gerät eine Verbindungsanfrage mit einer UUID gesendet hat, die mit der für diesen überwachten Server-Socket registrierten übereinstimmt. Bei Erfolg gibt accept() ein verbundenes BluetoothSocket zurück.

  3. Wenn Sie keine weiteren Verbindungen akzeptieren möchten, rufen Sie close() auf.

    Dieser Methodenaufruf gibt den Server-Socket und alle zugehörigen Ressourcen frei, schließt jedoch nicht die verbundene BluetoothSocket, die von accept() zurückgegeben wurde. Im Gegensatz zu TCP/IP lässt RFCOMM jeweils nur einen verbundenen Client pro Kanal zu. Daher ist es in den meisten Fällen sinnvoll, close() im BluetoothServerSocket aufzurufen, sobald ein verbundener Socket akzeptiert wurde.

Da der accept()-Aufruf ein blockierender Aufruf ist, führen Sie ihn nicht im UI-Thread der Hauptaktivität aus. Durch die Ausführung in einem anderen Thread wird sichergestellt, dass die Anwendung weiterhin auf andere Nutzerinteraktionen reagieren kann. Normalerweise ist es sinnvoll, alle Arbeiten auszuführen, die ein BluetoothServerSocket oder BluetoothSocket in einem neuen Thread, der von Ihrer Anwendung verwaltet wird, auszuführen. Um einen blockierten Aufruf wie accept() abzubrechen, rufen Sie close() im BluetoothServerSocket oder BluetoothSocket in einem anderen Thread auf. Alle Methoden für BluetoothServerSocket oder BluetoothSocket sind Thread-sicher.

Beispiel

Im Folgenden finden Sie einen vereinfachten Thread für die Serverkomponente, die eingehende Verbindungen akzeptiert:

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 diesem Beispiel wird nur eine eingehende Verbindung gewünscht. Sobald eine Verbindung akzeptiert und die BluetoothSocket abgerufen wurde, übergibt die Anwendung die abgerufene BluetoothSocket an einen separaten Thread, schließt den BluetoothServerSocket und bricht aus der Schleife.

Wenn accept() den BluetoothSocket zurückgibt, ist der Socket bereits verbunden. Daher sollten Sie connect() nicht aufrufen, wie dies auf Clientseite der Fall ist.

Die anwendungsspezifische Methode manageMyConnectedSocket() dient dazu, den Thread für die Datenübertragung zu initiieren, der im Thema zur Übertragung von Bluetooth-Daten erläutert wird.

Normalerweise sollten Sie BluetoothServerSocket schließen, sobald Sie auf eingehende Verbindungen gewartet haben. In diesem Beispiel wird close() aufgerufen, sobald BluetoothSocket erfasst wurde. Sie können auch eine öffentliche Methode in Ihrem Thread bereitstellen, mit der das private BluetoothSocket geschlossen werden kann, falls Sie den Server-Socket nicht mehr überwachen müssen.

Als Kunde verbinden

Um eine Verbindung mit einem Remote-Gerät zu initiieren, das Verbindungen auf einem offenen Server-Socket akzeptiert, müssen Sie zuerst ein BluetoothDevice-Objekt abrufen, das das Remote-Gerät darstellt. Informationen zum Erstellen eines BluetoothDevice finden Sie unter Bluetooth-Geräte finden. Sie müssen dann den BluetoothDevice verwenden, um eine BluetoothSocket abzurufen und die Verbindung zu initiieren.

Das grundlegende Verfahren sieht so aus:

  1. Wenn Sie BluetoothDevice verwenden, können Sie durch Aufrufen von createRfcommSocketToServiceRecord(UUID) ein BluetoothSocket abrufen.

    Diese Methode initialisiert ein BluetoothSocket-Objekt, mit dem der Client eine Verbindung zu einer BluetoothDevice herstellen kann. Die hier übergebene UUID muss mit der UUID übereinstimmen, die vom Servergerät beim Aufruf von listenUsingRfcommWithServiceRecord(String, UUID) zum Öffnen von BluetoothServerSocket verwendet wurde. Wenn Sie eine übereinstimmende UUID verwenden möchten, hartcodieren Sie den UUID-String in Ihrer Anwendung und verweisen Sie dann sowohl vom Server- als auch vom Clientcode darauf.

  2. Initiieren Sie die Verbindung, indem Sie connect() aufrufen. Diese Methode ist ein blockierender Aufruf.

    Nachdem ein Client diese Methode aufgerufen hat, führt das System eine SDP-Suche durch, um das Remote-Gerät mit der entsprechenden UUID zu finden. Wenn die Suche erfolgreich ist und das Remote-Gerät die Verbindung akzeptiert, teilt es den RFCOMM-Kanal, der während der Verbindung verwendet werden soll, und die Methode connect() wird zurückgegeben. Wenn die Verbindung fehlschlägt oder die Methode connect() nach etwa 12 Sekunden das Zeitlimit überschreitet, gibt die Methode eine IOException aus.

Da connect() ein blockierender Aufruf ist, sollten Sie diesen Verbindungsvorgang immer in einem Thread ausführen, der vom Thread der Hauptaktivität (UI) getrennt ist.

Beispiel

Im Folgenden finden Sie ein einfaches Beispiel für einen Clientthread, der eine Bluetooth-Verbindung initiiert:

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

Beachten Sie, dass in diesem Snippet cancelDiscovery() vor dem Verbindungsversuch aufgerufen wird. Sie sollten cancelDiscovery() immer vor connect() aufrufen, insbesondere weil cancelDiscovery() erfolgreich ist, unabhängig davon, ob gerade die Geräteerkennung läuft. Wenn Ihre App feststellen muss, ob die Geräteerkennung ausgeführt wird, können Sie dies mit isDiscovering() prüfen.

Die anwendungsspezifische manageMyConnectedSocket()-Methode dient dazu, den Thread für die Datenübertragung zu initiieren, der im Abschnitt zur Übertragung von Bluetooth-Daten beschrieben wird.

Wenn du mit deinem BluetoothSocket fertig bist, rufe immer close() auf. Dadurch wird der verbundene Socket sofort geschlossen und alle zugehörigen internen Ressourcen freigegeben.