Android 藍牙連接通訊實作心得

最近因為專案需求,進行了一些藍牙通訊的研究。
首先來看官方線上文件:Bluetooth overview - Connect devices

前面描述開啟藍牙或搜尋、被搜尋……等較為基本的部份先跳過。從連接裝置(Connect devices)這一節開始,首先要將兩個裝置配對,然後使用 BluetoothAdapter 的 getBondedDevices() 方法取得已配對的裝置資訊,並從中取出 MAC address。

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// If there are paired devices
if (pairedDevices.size() > 0) {
// Loop through paired devices
for (BluetoothDevice device : pairedDevices) {
// Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
接下來我們需要一組 UUID (通用唯一識別號)作為藍牙服務的識別碼。在網路上有許多 UUID 產生器,例如:https://www.uuidgenerator.net/。找一個喜歡的使用即可。

private final String NAME = "MyBTService";
private final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
因為藍牙的連接模式是由一個 Client 端主動發起連線給被動監聽的 Server 端,因此程式碼需要分別撰寫 Server 與 Client 端的動作。

首先來看接收藍牙連接的 Server 端程式碼:

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 = mBluetoothAdapter.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);
}
}
}
接受連接的步驟為:
  1. 調用 listenUsingRfcommWithServiceRecord(String, UUID) 取得 BluetoothServerSocket
  2. 調用 accept() 開始監聽連線請求
  3. 在 manageConnectedSocket(socket) 副程式中管理 socket 連線。這部份後面再詳述。
  4. 調用 close() 關閉監聽連線請求


然後是藍牙連接的 Client 端程式碼:

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.
mBluetoothAdapter.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);
}
}
}
發起連接的步驟為:
  1. 調用 createRfcommSocketToServiceRecord(UUID) 取得 BluetoothSocket。
  2. 調用 connect() 發起連接
  3. 在 manageConnectedSocket(socket) 副程式中管理 socket 連線。


到這邊可以發現到,Client 和 Server 都在第三步驟調用了 manageConnectedSocket(socket) 副程式,但是官方線上文件並沒有進一步說明這個副程式的內容。
所幸我在官方範例程式中找到了藍牙連接的範例程式:BluetoothChat
從中得知,原來是要啟動另一個 Thread:ConnectedThread

private void manageConnectedSocket(BluetoothSocket socket) {
connectedThread = new ConnectedThread(socket);
connectedThread.start();
}
而 ConnectedThread 的程式碼如下:

private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the input and output streams, using temp objects because
// member streams are final
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()
// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI activity
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}
/* Call this from the main activity to send data to the remote device */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
/* Call this from the main activity to shutdown the connection */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
ConnectedThread 透過 BluetoothSocket 取得了 InputStream 和 OutputStream 來處理數據傳輸,然後再透過 read(byte[]) 和 write(byte[]) 方法來讀取和寫入數據。

之後只要在 Activity(或 Fragment)中宣告一個 Handler,就可以用來接收對方傳來的數據並更新 UI;也能直接調用 ConnectedThread 的 write(byte[]) 來寫入數據給對方。

private class UIHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MESSAGE_READ:
byte[] writeBuf = (byte[]) msg.obj;
String writeMessage = new String(writeBuf);
msgList.add(writeMessage);
break;
default:
break;
}
}
}
最後附上一張傳輸成功的截圖

留言

這個網誌中的熱門文章

在 Android 上自訂 Zxing 掃描框樣式與大小位置