Подключите Bluetooth-устройства

Чтобы создать соединение между двумя устройствами, необходимо реализовать механизмы как на стороне сервера, так и на стороне клиента, поскольку одно устройство должно открыть сокет сервера, а другое должно инициировать соединение, используя MAC-адрес серверного устройства. Серверное устройство и клиентское устройство получают необходимый BluetoothSocket разными способами. Сервер получает информацию о сокете, когда принимается входящее соединение. Клиент предоставляет информацию о сокете, когда открывает канал RFCOMM на сервер.

Сервер и клиент считаются подключенными друг к другу, если каждый из них имеет подключенный BluetoothSocket на одном и том же канале RFCOMM. На этом этапе каждое устройство может получить входные и выходные потоки, и может начаться передача данных, о которой речь пойдет в разделе о передаче данных Bluetooth . В этом разделе описывается, как инициировать соединение между двумя устройствами.

Прежде чем пытаться найти устройства Bluetooth, убедитесь, что у вас есть соответствующие разрешения Bluetooth , и настройте свое приложение для Bluetooth .

Методы подключения

Один из методов реализации заключается в автоматической подготовке каждого устройства в качестве сервера, чтобы каждое устройство имело открытый сокет сервера и прослушивало соединения. В этом случае любое устройство может инициировать соединение с другим и стать клиентом. В качестве альтернативы одно устройство может явно размещать соединение и открывать сокет сервера по требованию, а другое устройство инициирует соединение.


Рисунок 1. Диалоговое окно сопряжения Bluetooth.

Подключиться как сервер

Если вы хотите соединить два устройства, одно из них должно действовать как сервер, удерживая открытый BluetoothServerSocket . Назначение сокета сервера — прослушивать входящие запросы на соединение и предоставлять подключенный BluetoothSocket после принятия запроса. Когда BluetoothSocket получается из BluetoothServerSocket , BluetoothServerSocket можно и нужно отбросить, если только вы не хотите, чтобы устройство принимало больше соединений.

Чтобы настроить сокет сервера и принять соединение, выполните следующую последовательность шагов:

  1. Получите BluetoothServerSocket , вызвав listenUsingRfcommWithServiceRecord(String, UUID) .

    Эта строка представляет собой идентифицируемое имя вашей службы, которое система автоматически записывает в новую запись базы данных протокола обнаружения служб (SDP) на устройстве. Имя произвольное и может быть просто названием вашего приложения. Универсальный уникальный идентификатор (UUID) также включается в запись SDP и формирует основу для соглашения о соединении с клиентским устройством. То есть, когда клиент пытается подключиться к этому устройству, он переносит UUID, который однозначно идентифицирует службу, к которой он хочет подключиться. Эти UUID должны совпадать, чтобы соединение было принято.

    UUID — это стандартизированный 128-битный формат строкового идентификатора, используемый для уникальной идентификации информации. UUID используется для идентификации информации, которая должна быть уникальной в системе или сети, поскольку вероятность повторения UUID фактически равна нулю. Он генерируется независимо, без использования централизованного органа. В данном случае он используется для уникальной идентификации службы Bluetooth вашего приложения. Чтобы получить UUID для использования в вашем приложении, вы можете использовать один из многих генераторов случайных UUID в Интернете, а затем инициализировать UUID с помощью fromString(String) .

  2. Начните прослушивать запросы на соединение, вызвав метод accept() .

    Это блокирующий вызов. Он возвращается, когда соединение было принято или произошло исключение. Соединение принимается только тогда, когда удаленное устройство отправило запрос на соединение, содержащий UUID, который соответствует тому, который зарегистрирован в этом сокете прослушивающего сервера. В случае успеха accept() возвращает подключенный BluetoothSocket .

  3. Если вы не хотите принимать дополнительные соединения, вызовите close() .

    Этот вызов метода освобождает сокет сервера и все его ресурсы, но не закрывает подключенный BluetoothSocket , возвращенный функцией accept() . В отличие от TCP/IP, RFCOMM допускает одновременное подключение только одного клиента на канал, поэтому в большинстве случаев имеет смысл вызывать функцию close() для BluetoothServerSocket сразу после принятия подключенного сокета.

Поскольку вызов accept() является блокирующим вызовом, не выполняйте его в основном потоке пользовательского интерфейса активности. Выполнение его в другом потоке гарантирует, что ваше приложение по-прежнему сможет реагировать на другие взаимодействия с пользователем. Обычно имеет смысл выполнять всю работу, связанную с BluetoothServerSocket или BluetoothSocket в новом потоке, управляемом вашим приложением. Чтобы прервать заблокированный вызов, например accept() , вызовите close() для BluetoothServerSocket или BluetoothSocket из другого потока. Обратите внимание, что все методы BluetoothServerSocket или BluetoothSocket являются потокобезопасными.

Пример

Ниже приведен упрощенный поток для серверного компонента, который принимает входящие соединения:

Котлин

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

Ява

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

В этом примере требуется только одно входящее соединение, поэтому как только соединение принято и BluetoothSocket получен, приложение передает полученный BluetoothSocket в отдельный поток, закрывает BluetoothServerSocket и выходит из цикла.

Обратите внимание: когда accept() возвращает BluetoothSocket , сокет уже подключен. Поэтому вам не следует вызывать connect() , как вы это делаете со стороны клиента.

Специфический для приложения метод manageMyConnectedSocket() предназначен для инициации потока передачи данных, который обсуждается в теме о передаче данных Bluetooth .

Обычно вам следует закрыть BluetoothServerSocket , как только вы закончите прослушивать входящие соединения. В этом примере close() вызывается сразу после получения BluetoothSocket . Вы также можете захотеть предоставить в своем потоке общедоступный метод, который может закрыть частный BluetoothSocket в случае, если вам нужно прекратить прослушивание этого сокета сервера.

Подключиться как клиент

Чтобы инициировать соединение с удаленным устройством, которое принимает соединения через открытый сокет сервера, необходимо сначала получить объект BluetoothDevice , представляющий удаленное устройство. Чтобы узнать, как создать BluetoothDevice , см. раздел Поиск устройств Bluetooth . Затем вы должны использовать BluetoothDevice для получения BluetoothSocket и инициации соединения.

Основная процедура заключается в следующем:

  1. Используя BluetoothDevice , получите BluetoothSocket , вызвав createRfcommSocketToServiceRecord(UUID) .

    Этот метод инициализирует объект BluetoothSocket , который позволяет клиенту подключаться к BluetoothDevice . Передаваемый здесь UUID должен соответствовать UUID, используемому серверным устройством при вызове listenUsingRfcommWithServiceRecord(String, UUID) для открытия BluetoothServerSocket . Чтобы использовать соответствующий UUID, жестко закодируйте строку UUID в своем приложении, а затем ссылайтесь на нее как из серверного, так и из клиентского кода.

  2. Инициируйте соединение, вызвав метод connect() . Обратите внимание, что этот метод является блокирующим вызовом.

    После того, как клиент вызывает этот метод, система выполняет поиск SDP, чтобы найти удаленное устройство с соответствующим UUID. Если поиск успешен и удаленное устройство принимает соединение, оно совместно использует канал RFCOMM, который будет использоваться во время соединения, и метод connect() возвращает значение. Если соединение не удалось или время ожидания метода connect() истекло (примерно через 12 секунд), метод выдает IOException .

Поскольку connect() является блокирующим вызовом, эту процедуру подключения всегда следует выполнять в потоке, отдельном от потока основного действия (UI).

Пример

Ниже приведен базовый пример клиентского потока, который инициирует соединение Bluetooth:

Котлин

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

Ява

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

Обратите внимание, что в этом фрагменте cancelDiscovery() вызывается до того, как произойдет попытка подключения. Всегда следует вызывать cancelDiscovery() перед connect() , особенно потому, что cancelDiscovery() завершается успешно независимо от того, выполняется ли в данный момент обнаружение устройства. Если вашему приложению необходимо определить, выполняется ли обнаружение устройства, вы можете проверить это с помощью isDiscovering() .

Метод manageMyConnectedSocket() , специфичный для приложения, предназначен для инициирования потока передачи данных, который обсуждается в разделе о передаче данных Bluetooth .

Когда вы закончите работу с BluetoothSocket , всегда вызывайте close() . При этом подключенный сокет немедленно закрывается и освобождаются все связанные внутренние ресурсы.