Подключите 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() . Unlike TCP/IP, RFCOMM allows only one connected client per channel at a time, so in most cases it makes sense to call close() on the BluetoothServerSocket immediately after accepting a connected socket.

Поскольку вызов 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() .

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

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