Łączenie urządzeń Bluetooth

Aby utworzyć połączenie między 2 urządzeniami, należy wdrożyć zarówno mechanizm po stronie serwera, jak i po stronie klienta, ponieważ jedno z nich musi otworzyć gniazdo serwera, a drugie – za pomocą adresu MAC urządzenia serwera. Serwer i urządzenie klienckie uzyskują wymagane uprawnienia BluetoothSocket w różny sposób. Serwer otrzymuje informacje o gniazdu, gdy połączenie przychodzące jest akceptowane. Klient dostarcza informacje o gniazdu, gdy otwiera kanał RFCOMM dla serwera.

Serwer i klient są ze sobą połączone, jeśli mają połączenie BluetoothSocket w tym samym kanale RFCOMM. W tym momencie każde urządzenie może otrzymywać strumienie wejściowe i wyjściowe oraz rozpocząć przenoszenie danych, co zostało omówione w sekcji o przesyłaniu danych Bluetooth. W tej sekcji opisujemy, jak zainicjować połączenie między dwoma urządzeniami.

Zanim spróbujesz znaleźć urządzenia Bluetooth, upewnij się, że masz odpowiednie uprawnienia Bluetooth i skonfiguruj Bluetooth w aplikacji.

Techniki nawiązywania połączeń

Jedną z metod implementacji jest automatyczne przygotowanie każdego urządzenia jako serwera, tak aby każde z nich miało otwarte gniazdo serwera i nasłuchiwało połączeń. W takim przypadku każde urządzenie może zainicjować połączenie z innym i przekształcić się w klienta. Jedno urządzenie może też jawnie hostować połączenie i na żądanie otworzyć gniazdo serwera, a drugie inicjuje połączenie.


Rysunek 1. Okno parowania Bluetooth.

Połącz się jako serwer

Jeśli chcesz połączyć 2 urządzenia, jedno z nich musi działać jako serwer, przytrzymując otwartą BluetoothServerSocket. Gniazdo serwera służy do nasłuchiwania przychodzących żądań połączenia i udostępniania połączonego BluetoothSocket po zaakceptowaniu żądania. Po pobraniu BluetoothSocket z modułu BluetoothServerSocket BluetoothServerSocket można – i należy – odrzucić wspomniany element, chyba że chcesz, aby urządzenie akceptuje więcej połączeń.

Aby skonfigurować gniazdo serwera i zaakceptować połączenie, wykonaj te czynności:

  1. Zdobądź BluetoothServerSocket, dzwoniąc pod numer listenUsingRfcommWithServiceRecord(String, UUID).

    Jest to możliwa do zidentyfikowania nazwa usługi, która system automatycznie zapisuje na urządzeniu w nowym wpisie bazy danych protokołu Service Discovery Protocol (SDP). Nazwa jest dowolna i może być po prostu nazwą aplikacji. We wpisie SDP zawarty jest też identyfikator UUID, który jest podstawą do zawarcia umowy o połączenie z urządzeniem klienta. Oznacza to, że gdy klient próbuje połączyć się z tym urządzeniem, niesie identyfikator UUID, który jednoznacznie identyfikuje usługę, z którą chce się połączyć. Te identyfikatory UUID muszą być takie same, aby połączenie zostało zaakceptowane.

    UUID to standardowy 128-bitowy format identyfikatora ciągu znaków używany do jednoznacznej identyfikacji informacji. Identyfikator UUID służy do identyfikowania informacji, które muszą być niepowtarzalne w systemie lub sieci, ponieważ prawdopodobieństwo powtarzania się identyfikatora UUID wynosi zero. Jest generowany niezależnie, bez użycia scentralizowanej instytucji. W tym przypadku jest on jednoznacznie identyfikowany przez usługę Bluetooth aplikacji. Aby uzyskać identyfikator UUID do użycia w aplikacji, możesz użyć jednego z wielu losowych generatorów UUID w internecie, a potem zainicjować identyfikator UUID za pomocą fromString(String).

  2. Zacznij nasłuchiwać próśb o połączenie, wywołując metodę accept().

    To połączenie jest blokowane. Wraca on, gdy połączenie zostało zaakceptowane lub wystąpił wyjątek. Połączenie jest akceptowane tylko wtedy, gdy urządzenie zdalne wysłało żądanie połączenia zawierające identyfikator UUID zgodny z identyfikatorem zarejestrowanym w tym gniazdku serwera nasłuchu. Jeśli operacja się uda, accept() zwróci połączoną z nim BluetoothSocket.

  3. Jeśli nie chcesz akceptować dodatkowych połączeń, wywołaj metodę close().

    To wywołanie metody zwalnia gniazdo serwera i wszystkie jego zasoby, ale nie zamyka połączonego BluetoothSocket zwróconego przez accept(). W przeciwieństwie do TCP/IP RFCOMM zezwala tylko na 1 połączony klient na kanał naraz, więc w większości przypadków sensowne jest wywołanie metody close() w BluetoothServerSocket natychmiast po zaakceptowaniu podłączonego gniazdka.

Wywołanie accept() to połączenie blokujące, dlatego nie wykonuj go w wątku interfejsu głównej aktywności. Jeśli wykonasz go w innym wątku, aplikacja będzie mogła nadal reagować na inne interakcje użytkowników. Zazwyczaj rozsądnie jest wykonać wszystkie działania z elementami BluetoothServerSocket lub BluetoothSocket w nowym wątku zarządzanym przez aplikację. Aby przerwać zablokowane wywołanie, na przykład accept(), użyj wywołania close() na metodzie BluetoothServerSocket lub BluetoothSocket z innego wątku. Pamiętaj, że wszystkie metody w BluetoothServerSocket i BluetoothSocket są bezpieczne w wątkach.

Przykład

Oto uproszczony wątek komponentu serwera, który akceptuje połączenia przychodzące:

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

W tym przykładzie wymagane jest tylko 1 połączenie przychodzące. Gdy tylko połączenie zostanie zaakceptowane i pozyskany BluetoothSocket, aplikacja przekazuje pozyskane BluetoothSocket do osobnego wątku, zamyka BluetoothServerSocket i wychodzi z pętli.

Zwróć uwagę, że gdy accept() zwraca BluetoothSocket, gniazdo jest już podłączone. Dlatego nie należy wywoływać connect(), jak robisz to po stronie klienta.

Metoda manageMyConnectedSocket() specyficzna dla aplikacji inicjuje wątek przenoszenia danych, co zostało omówione w temacie dotyczącym przenoszenia danych Bluetooth.

Zwykle zamykane jest BluetoothServerSocket zaraz po zakończeniu nasłuchiwania połączeń przychodzących. W tym przykładzie funkcja close() jest wywoływana natychmiast po pozyskaniu BluetoothSocket. Możesz też podać w wątku metodę publiczną, która może zamknąć prywatny BluetoothSocket, gdy przestaniesz nasłuchiwać w tym gnieździe serwera.

Połącz się jako klient

Aby zainicjować połączenie z urządzeniem zdalnym, które akceptuje połączenia w otwartym gnieździe serwera, musisz najpierw uzyskać obiekt BluetoothDevice reprezentujący urządzenie zdalne. Aby dowiedzieć się, jak utworzyć urządzenie BluetoothDevice, przeczytaj sekcję Znajdowanie urządzeń Bluetooth. Następnie musisz uzyskać BluetoothSocket i zainicjować połączenie za pomocą BluetoothDevice.

Podstawowa procedura wygląda tak:

  1. Korzystając z: BluetoothDevice, zadzwoń pod numer createRfcommSocketToServiceRecord(UUID), aby otrzymać BluetoothSocket.

    Ta metoda inicjuje obiekt BluetoothSocket, który pozwala klientowi na nawiązanie połączenia z BluetoothDevice. Przekazany tutaj identyfikator UUID musi być zgodny z identyfikatorem UUID używanym przez urządzenie serwera w momencie wywołania listenUsingRfcommWithServiceRecord(String, UUID) w celu otwarcia jego BluetoothServerSocket. Aby użyć pasującego identyfikatora UUID, zakoduj na stałe ciąg UUID w aplikacji, a potem odwołać się do niego z kodu serwera i klienta.

  2. Zainicjuj połączenie, wywołując connect(). Zwróć uwagę, że ta metoda to wywołanie blokujące.

    Po wywołaniu tej metody przez klienta system przeprowadza wyszukiwanie SDP, aby znaleźć urządzenie zdalne z pasującym identyfikatorem UUID. Jeśli wyszukiwanie się powiedzie, a zdalne urządzenie zaakceptuje połączenie, współdzieli kanał RFCOMM, który będzie używany w trakcie połączenia, i metoda connect() zostanie zwrócona. Jeśli połączenie nie powiedzie się lub zostanie przekroczony limit czasu dla metody connect() (po około 12 sekundach), metoda zwróci IOException.

connect() to połączenie blokujące, dlatego tę procedurę połączenia zawsze należy wykonywać w wątku niezależnym od głównego wątku aktywności.

Przykład

Oto podstawowy przykład wątku klienta, który inicjuje połączenie 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);
       }
   }
}

Zwróć uwagę, że w tym fragmencie kodu cancelDiscovery() jest wywoływany przed próbą połączenia. Pamiętaj, by zawsze wywoływać metodę cancelDiscovery() przed connect(), zwłaszcza że funkcja cancelDiscovery() działa bez względu na to, czy trwa wykrywanie urządzeń. Jeśli Twoja aplikacja musi określać, czy trwa wykrywanie urządzeń, możesz to zrobić za pomocą narzędzia isDiscovering().

Metoda manageMyConnectedSocket() specyficzna dla aplikacji inicjuje wątek przenoszenia danych, co zostało omówione w sekcji poświęconej przenoszeniu danych Bluetooth.

Gdy skończysz pracę z urządzeniem BluetoothSocket, zawsze dzwoń na numer close(). Spowoduje to natychmiastowe zamknięcie podłączonego gniazda i zwolnienie wszystkich powiązanych zasobów wewnętrznych.