Kết nối thiết bị Bluetooth

Để tạo kết nối giữa hai thiết bị, bạn phải triển khai cả cơ chế phía máy chủ và phía máy khách vì một thiết bị phải mở ổ cắm máy chủ và thiết bị còn lại phải bắt đầu kết nối bằng địa chỉ MAC của thiết bị máy chủ. Thiết bị máy chủ và thiết bị khách lấy BluetoothSocket bắt buộc theo những cách khác nhau. Máy chủ nhận thông tin ổ cắm khi kết nối đến được chấp nhận. Ứng dụng cung cấp thông tin cổng khi mở một kênh RFCOMM cho máy chủ.

Máy chủ và ứng dụng được coi là đã kết nối với nhau khi đều có một BluetoothSocket được kết nối trên cùng một kênh RFCOMM. Tại thời điểm này, mỗi thiết bị đều có thể nhận luồng đầu vào và đầu ra, đồng thời quá trình truyền dữ liệu có thể bắt đầu, được thảo luận trong phần về cách chuyển dữ liệu Bluetooth. Phần này mô tả cách bắt đầu kết nối giữa hai thiết bị.

Hãy đảm bảo bạn có quyền truy cập Bluetooth thích hợp và thiết lập ứng dụng để sử dụng Bluetooth trước khi tìm cách tìm thiết bị Bluetooth.

Kỹ thuật kết nối

Một kỹ thuật triển khai là tự động chuẩn bị từng thiết bị làm máy chủ sao cho mỗi thiết bị đều có một ổ cắm máy chủ mở và theo dõi các kết nối. Trong trường hợp này, một trong hai thiết bị có thể bắt đầu kết nối với thiết bị kia và trở thành ứng dụng. Ngoài ra, một thiết bị có thể lưu trữ kết nối một cách rõ ràng và mở một ổ cắm máy chủ theo yêu cầu, rồi thiết bị kia sẽ bắt đầu kết nối.


Hình 1. Hộp thoại ghép nối Bluetooth.

Kết nối với tư cách máy chủ

Khi bạn muốn kết nối hai thiết bị, thiết bị phải hoạt động như một máy chủ bằng cách giữ một BluetoothServerSocket mở. Mục đích của cổng máy chủ là theo dõi các yêu cầu kết nối đến và cung cấp một BluetoothSocket được kết nối sau khi yêu cầu được chấp nhận. Khi BluetoothSocket được thu nạp từ BluetoothServerSocket, BluetoothServerSocket có thể và sẽ bị loại bỏ, trừ phi bạn muốn thiết bị chấp nhận thêm các kết nối khác.

Để thiết lập một cổng máy chủ và chấp nhận kết nối, hãy hoàn tất trình tự các bước sau:

  1. Nhận BluetoothServerSocket bằng cách gọi listenUsingRfcommWithServiceRecord(String, UUID).

    Chuỗi này là tên nhận dạng được của dịch vụ mà hệ thống sẽ tự động ghi vào mục nhập cơ sở dữ liệu Giao thức khám phá dịch vụ (SDP) mới trên thiết bị. Bạn có thể đặt tên này là tuỳ ý và có thể chỉ đơn giản là tên ứng dụng của bạn. Giá trị nhận dạng duy nhất trên toàn cầu (UUID) cũng được đưa vào mục SDP và tạo thành cơ sở cho thoả thuận kết nối với thiết bị ứng dụng. Tức là khi ứng dụng cố gắng kết nối với thiết bị này, thiết bị sẽ mang một mã nhận dạng duy nhất (UUID) nhận dạng duy nhất dịch vụ mà ứng dụng muốn kết nối. Các mã nhận dạng duy nhất (UUID) này phải khớp thì mới được chấp nhận kết nối.

    Mã nhận dạng duy nhất (UUID) là một định dạng 128 bit chuẩn cho mã nhận dạng chuỗi dùng để xác định thông tin riêng biệt. Mã nhận dạng duy nhất (UUID) dùng để xác định thông tin cần là duy nhất trong một hệ thống hoặc một mạng vì xác suất lặp lại mã nhận dạng duy nhất (UUID) thực sự là 0. Dữ liệu này được tạo độc lập mà không cần đến một cơ quan quản lý tập trung. Trong trường hợp này, giá trị này được dùng để xác định duy nhất dịch vụ Bluetooth của ứng dụng. Để nhận UUID để sử dụng với ứng dụng, bạn có thể sử dụng một trong nhiều trình tạo UUID ngẫu nhiên trên web, sau đó khởi chạy UUID bằng fromString(String).

  2. Bắt đầu theo dõi các yêu cầu kết nối bằng cách gọi accept().

    Đây là một lệnh gọi chặn. Phương thức này trả về khi kết nối đã được chấp nhận hoặc xảy ra ngoại lệ. Kết nối chỉ được chấp nhận khi thiết bị từ xa gửi yêu cầu kết nối chứa UUID khớp với mã đã đăng ký bằng ổ cắm máy chủ nghe này. Khi thành công, accept() sẽ trả về một BluetoothSocket đã kết nối.

  3. Nếu bạn không muốn chấp nhận các kết nối bổ sung, hãy gọi close().

    Lệnh gọi phương thức này giải phóng ổ cắm máy chủ và mọi tài nguyên liên quan, nhưng không đóng BluetoothSocket đã kết nối mà accept() trả về. Không giống như TCP/IP, RFCOMM chỉ cho phép một ứng dụng được kết nối trên mỗi kênh tại một thời điểm. Vì vậy, trong hầu hết các trường hợp, bạn nên gọi close() trên BluetoothServerSocket ngay sau khi chấp nhận ổ cắm đã kết nối.

Vì lệnh gọi accept() là một lệnh gọi chặn, nên đừng thực thi lệnh này trong luồng giao diện người dùng hoạt động chính. Việc thực thi luồng này trong một luồng khác đảm bảo rằng ứng dụng của bạn vẫn có thể phản hồi các lượt tương tác khác của người dùng. Thông thường, bạn nên thực hiện tất cả công việc liên quan đến BluetoothServerSocket hoặc BluetoothSocket trong một luồng mới do ứng dụng của bạn quản lý. Để huỷ một lệnh gọi bị chặn như accept(), hãy gọi close() trên BluetoothServerSocket hoặc BluetoothSocket từ một luồng khác. Xin lưu ý rằng mọi phương thức trên BluetoothServerSocket hoặc BluetoothSocket đều an toàn cho luồng.

Ví dụ

Sau đây là một luồng được đơn giản hoá cho thành phần máy chủ chấp nhận các kết nối đến:

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

Trong ví dụ này, chỉ cần một kết nối đến, vì vậy ngay sau khi một kết nối được chấp nhận và có được BluetoothSocket, ứng dụng sẽ truyền BluetoothSocket đã nhận được sang một luồng riêng, đóng BluetoothServerSocket và thoát khỏi vòng lặp.

Lưu ý rằng khi accept() trả về BluetoothSocket, có nghĩa là ổ cắm đã được kết nối. Do đó, bạn không nên gọi connect(), như cách bạn thực hiện từ phía máy khách.

Phương thức manageMyConnectedSocket() dành riêng cho ứng dụng được thiết kế để bắt đầu luồng truyền dữ liệu. Phương thức này sẽ được thảo luận trong chủ đề về cách chuyển dữ liệu Bluetooth.

Thông thường, bạn nên đóng BluetoothServerSocket ngay khi nghe xong các kết nối đến. Trong ví dụ này, close() được gọi ngay khi nhận được BluetoothSocket. Cũng có thể bạn muốn cung cấp một phương thức công khai trong luồng có thể đóng BluetoothSocket riêng tư trong trường hợp bạn cần ngừng nghe trên cổng máy chủ đó.

Kết nối với tư cách là ứng dụng

Để bắt đầu kết nối với một thiết bị từ xa chấp nhận các kết nối trên ổ cắm máy chủ mở, trước tiên, bạn phải có được một đối tượng BluetoothDevice đại diện cho thiết bị từ xa. Để tìm hiểu cách tạo BluetoothDevice, hãy xem phần Tìm thiết bị Bluetooth. Sau đó, bạn phải sử dụng BluetoothDevice để lấy BluetoothSocket và bắt đầu kết nối.

Quy trình cơ bản như sau:

  1. Sử dụng BluetoothDevice, hãy nhận BluetoothSocket bằng cách gọi createRfcommSocketToServiceRecord(UUID).

    Phương thức này khởi chạy một đối tượng BluetoothSocket cho phép ứng dụng kết nối với một BluetoothDevice. Mã nhận dạng duy nhất (UUID) được truyền ở đây phải khớp với UUID mà thiết bị máy chủ sử dụng khi gọi listenUsingRfcommWithServiceRecord(String, UUID) để mở BluetoothServerSocket. Để sử dụng một UUID phù hợp, hãy mã hoá cứng chuỗi UUID vào ứng dụng của bạn, sau đó tham chiếu chuỗi đó từ cả mã máy chủ và mã ứng dụng khách.

  2. Bắt đầu kết nối bằng cách gọi connect(). Lưu ý rằng phương thức này là lệnh gọi chặn.

    Sau khi ứng dụng gọi phương thức này, hệ thống sẽ thực hiện quy trình tra cứu SDP để tìm thiết bị từ xa có mã nhận dạng duy nhất (UUID) trùng khớp. Nếu tra cứu thành công và thiết bị từ xa chấp nhận kết nối, thì thiết bị sẽ chia sẻ kênh RFCOMM để sử dụng trong quá trình kết nối và phương thức connect() sẽ trả về. Nếu kết nối không thành công hoặc nếu phương thức connect() hết thời gian chờ (sau khoảng 12 giây), thì phương thức này sẽ gửi ra một IOException.

connect() là một lệnh gọi chặn, nên bạn phải luôn thực hiện quy trình kết nối này trong một luồng tách biệt với luồng hoạt động chính (UI).

Ví dụ

Sau đây là ví dụ cơ bản về một luồng ứng dụng bắt đầu kết nối 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);
       }
   }
}

Xin lưu ý rằng trong đoạn mã này, cancelDiscovery() được gọi trước khi cố gắng kết nối. Bạn phải luôn gọi cancelDiscovery() trước connect(), đặc biệt là vì cancelDiscovery() thành công bất kể quá trình khám phá thiết bị hiện có đang diễn ra hay không. Nếu ứng dụng của bạn cần xác định xem quá trình khám phá thiết bị có đang diễn ra hay không, thì bạn có thể kiểm tra bằng cách sử dụng isDiscovering().

Phương thức manageMyConnectedSocket() dành riêng cho ứng dụng được thiết kế để bắt đầu luồng để truyền dữ liệu, điều này được thảo luận trong phần về cách chuyển dữ liệu Bluetooth.

Khi bạn hoàn tất BluetoothSocket, hãy luôn gọi close(). Thao tác này sẽ đóng ngay ổ cắm đã kết nối và giải phóng mọi tài nguyên nội bộ có liên quan.