目錄
一、前言
去年畢業進入公司以來,工作內容主要和藍牙打交道,幾個月的學習和實踐讓我這個Android藍牙小白逐漸成長起來。但是,很多時候知識溫故才能知新,每一次實踐都會帶來新的理解和體會。於是決定從今天開始,將這幾個月以來的成長在博客中一一分享出來,給有需要的朋友作些參考,也歡迎大家提出指點和建議。
二、經典藍牙的介紹
關於經典藍牙的介紹,google官網上有詳細的解釋,此處貼上鍊接:https://developer.android.google.cn/guide/topics/connectivity/bluetooth
經典藍牙的使用過程大致可分爲以下幾個步驟:
1、開啓掃描,搜索周圍藍牙設備
2、掃描到設備後,與設備配對、建立連接
3、與設備成功連接後,實現數據通訊即收發數據
4、與設備通訊結束後,關閉與藍牙的連接
三、經典藍牙項目實戰
1、在開始使用藍牙之前,我們需要添加藍牙相關的權限
要在應用中使用藍牙功能,必須聲明藍牙相關權限。在項目AndroidManifest.xml文件中添加藍牙權限:
<!-- 應用使用藍牙的權限 --> <uses-permission android:name="android.permission.BLUETOOTH"/> <!--啓動設備發現或操作藍牙設置的權限--> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
2、創建發起藍牙連接的線程ConnectThread.java 和 管理藍牙連接、收發數據的線程ConnectedThread.java。
(1)ConnectThread.java,代碼中已經有詳細的註釋。
package yc.bluetooth.androidbt; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.util.Log; import java.io.IOException; import java.util.UUID; /** * 發起藍牙連接 */ public class ConnectThread extends Thread { private static final String TAG = "ConnectThread"; private final BluetoothAdapter mBluetoothAdapter; private BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothAdapter bluetoothAdapter,BluetoothDevice bluetoothDevice,String uuid) { this.mBluetoothAdapter = bluetoothAdapter; this.mmDevice = bluetoothDevice; //使用一個臨時變量,等會賦值給mmSocket //因爲mmSocket是靜態的 BluetoothSocket tmp = null ; if(mmSocket != null){ Log.e(TAG,"ConnectThread-->mmSocket != null先去釋放"); try { mmSocket.close(); } catch (IOException e) { e.printStackTrace(); } } Log.d(TAG,"ConnectThread-->mmSocket != null已釋放"); //1、獲取BluetoothSocket try { //建立安全的藍牙連接,會彈出配對框 tmp = mmDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid)); } catch (IOException e) { Log.e(TAG,"ConnectThread-->獲取BluetoothSocket異常!" + e.getMessage()); } mmSocket = tmp; if(mmSocket != null){ Log.w(TAG,"ConnectThread-->已獲取BluetoothSocket"); } } @Override public void run(){ //連接之前先取消發現設備,否則會大幅降低連接嘗試的速度,並增加連接失敗的可能性 if(mBluetoothAdapter == null){ Log.e(TAG,"ConnectThread:run-->mBluetoothAdapter == null"); return; } //取消發現設備 if(mBluetoothAdapter.isDiscovering()){ mBluetoothAdapter.cancelDiscovery(); } if(mmSocket == null){ Log.e(TAG,"ConnectThread:run-->mmSocket == null"); return; } //2、通過socket去連接設備 try { Log.d(TAG,"ConnectThread:run-->去連接..."); if(onBluetoothConnectListener != null){ onBluetoothConnectListener.onStartConn(); //開始去連接回調 } mmSocket.connect(); //connect()爲阻塞調用,連接失敗或 connect() 方法超時(大約 12 秒之後),它將會引發異常 if(onBluetoothConnectListener != null){ onBluetoothConnectListener.onConnSuccess(mmSocket); //連接成功回調 Log.w(TAG,"ConnectThread:run-->連接成功"); } } catch (IOException e) { Log.e(TAG,"ConnectThread:run-->連接異常!" + e.getMessage()); if(onBluetoothConnectListener != null){ onBluetoothConnectListener.onConnFailure("連接異常:" + e.getMessage()); } //釋放 cancel(); } } /** * 釋放 */ public void cancel() { try { if (mmSocket != null && mmSocket.isConnected()) { Log.d(TAG,"ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected()); mmSocket.close(); mmSocket = null; return; } if (mmSocket != null) { mmSocket.close(); mmSocket = null; } Log.d(TAG,"ConnectThread:cancel-->關閉已連接的套接字釋放資源"); } catch (IOException e) { Log.e(TAG,"ConnectThread:cancel-->關閉已連接的套接字釋放資源異常!" + e.getMessage()); } } private OnBluetoothConnectListener onBluetoothConnectListener; public void setOnBluetoothConnectListener(OnBluetoothConnectListener onBluetoothConnectListener) { this.onBluetoothConnectListener = onBluetoothConnectListener; } //連接狀態監聽者 public interface OnBluetoothConnectListener{ void onStartConn(); //開始連接 void onConnSuccess(BluetoothSocket bluetoothSocket); //連接成功 void onConnFailure(String errorMsg); //連接失敗 } }
(2)ConnectedThread.java,代碼中已經有詳細的註釋。
package yc.bluetooth.androidbt; import android.bluetooth.BluetoothSocket; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; /** * 管理連接 * 1、發送數據 * 2、接收數據 */ public class ConnectedThread extends Thread{ private static final String TAG = "ConnectedThread"; private BluetoothSocket mmSocket; private InputStream mmInStream; private OutputStream mmOutStream; //是否是主動斷開 private boolean isStop = false; //發起藍牙連接的線程 private ConnectThread connectThread; public void terminalClose(ConnectThread connectThread){ isStop = true; this.connectThread = connectThread; } public ConnectedThread(BluetoothSocket socket){ mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; //使用臨時對象獲取輸入和輸出流,因爲成員流是靜態類型 //1、獲取 InputStream 和 OutputStream try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { Log.e(TAG,"ConnectedThread-->獲取InputStream 和 OutputStream異常!"); } mmInStream = tmpIn; mmOutStream = tmpOut; if(mmInStream != null){ Log.d(TAG,"ConnectedThread-->已獲取InputStream"); } if(mmOutStream != null){ Log.d(TAG,"ConnectedThread-->已獲取OutputStream"); } } public void run(){ //最大緩存區 存放流 byte[] buffer = new byte[1024 * 2]; //buffer store for the stream //從流的read()方法中讀取的字節數 int bytes = 0; //bytes returned from read() //持續監聽輸入流直到發生異常 while(!isStop){ try { if(mmInStream == null){ Log.e(TAG,"ConnectedThread:run-->輸入流mmInStream == null"); break; } //先判斷是否有數據,有數據再讀取 if(mmInStream.available() != 0){ //2、接收數據 bytes = mmInStream.read(buffer); //從(mmInStream)輸入流中(讀取內容)讀取的一定數量字節數,並將它們存儲到緩衝區buffer數組中,bytes爲實際讀取的字節數 byte[] b = Arrays.copyOf(buffer,bytes); //存放實際讀取的數據內容 Log.w(TAG,"ConnectedThread:run-->收到消息,長度" + b.length + "->" + bytes2HexString(b, b.length)); //有空格的16進制字符串 if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onReceiveDataSuccess(b); //成功收到消息 } } } catch (IOException e) { Log.e(TAG,"ConnectedThread:run-->接收消息異常!" + e.getMessage()); if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onReceiveDataError("接收消息異常:" + e.getMessage()); //接收消息異常 } //關閉流和socket boolean isClose = cancel(); if(isClose){ Log.e(TAG,"ConnectedThread:run-->接收消息異常,成功斷開連接!"); } break; } } //關閉流和socket boolean isClose = cancel(); if(isClose){ Log.d(TAG,"ConnectedThread:run-->接收消息結束,斷開連接!"); } } //發送數據 public boolean write(byte[] bytes){ try { if(mmOutStream == null){ Log.e(TAG, "mmOutStream == null"); return false; } //發送數據 mmOutStream.write(bytes); Log.d(TAG, "寫入成功:"+ bytes2HexString(bytes, bytes.length)); if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onSendDataSuccess(bytes); //發送數據成功回調 } return true; } catch (IOException e) { Log.e(TAG, "寫入失敗:"+ bytes2HexString(bytes, bytes.length)); if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onSendDataError(bytes,"寫入失敗"); //發送數據失敗回調 } return false; } } /** * 釋放 * @return true 斷開成功 false 斷開失敗 */ public boolean cancel(){ try { if(mmInStream != null){ mmInStream.close(); //關閉輸入流 } if(mmOutStream != null){ mmOutStream.close(); //關閉輸出流 } if(mmSocket != null){ mmSocket.close(); //關閉socket } if(connectThread != null){ connectThread.cancel(); } connectThread = null; mmInStream = null; mmOutStream = null; mmSocket = null; Log.w(TAG,"ConnectedThread:cancel-->成功斷開連接"); return true; } catch (IOException e) { // 任何一部分報錯,都將強制關閉socket連接 mmInStream = null; mmOutStream = null; mmSocket = null; Log.e(TAG, "ConnectedThread:cancel-->斷開連接異常!" + e.getMessage()); return false; } } /** * 字節數組-->16進制字符串 * @param b 字節數組 * @param length 字節數組長度 * @return 16進制字符串 有空格類似“0A D5 CD 8F BD E5 F8” */ public static String bytes2HexString(byte[] b, int length) { StringBuffer result = new StringBuffer(); String hex; for (int i = 0; i < length; i++) { hex = Integer.toHexString(b[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } result.append(hex.toUpperCase()).append(" "); } return result.toString(); } private OnSendReceiveDataListener onSendReceiveDataListener; public void setOnSendReceiveDataListener(OnSendReceiveDataListener onSendReceiveDataListener) { this.onSendReceiveDataListener = onSendReceiveDataListener; } //收發數據監聽者 public interface OnSendReceiveDataListener{ void onSendDataSuccess(byte[] data); //發送數據結束 void onSendDataError(byte[] data, String errorMsg); //發送數據出錯 void onReceiveDataSuccess(byte[] buffer); //接收到數據 void onReceiveDataError(String errorMsg); //接收數據出錯 } }
3、使用藍牙之前,首先要檢查當前手機是否支持藍牙。如果支持藍牙,檢查手機藍牙是否已開啓。如果沒有開啓,則需要先打開藍牙。打開手機藍牙,有兩種方式,推薦使用第二種打開方式。
private void initBluetooth() { bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if(bluetoothAdapter == null){ Toast.makeText(this, "當前手機設備不支持藍牙", Toast.LENGTH_SHORT).show(); }else{ //手機設備支持藍牙,判斷藍牙是否已開啓 if(bluetoothAdapter.isEnabled()){ Toast.makeText(this, "手機藍牙已開啓", Toast.LENGTH_SHORT).show(); }else{ //藍牙沒有打開,去打開藍牙。推薦使用第二種打開藍牙方式 //第一種方式:直接打開手機藍牙,沒有任何提示 // bluetoothAdapter.enable(); //BLUETOOTH_ADMIN權限 //第二種方式:友好提示用戶打開藍牙 Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivity(enableBtIntent); } } }
4、確保手機藍牙已打開,就可以開始搜索設備。搜索設備只需調用startDiscovery()方法,但搜索的結果是通過廣播來獲取的,所以,還需要定義廣播來獲取搜索到的設備。
(1)搜索設備
private void searchBtDevice() { if(bluetoothAdapter.isDiscovering()){ //當前正在搜索設備... return; } //開始搜索 bluetoothAdapter.startDiscovery(); }
(2)自定義廣播接收器,接收搜索到的設備。
/** * 藍牙廣播接收器 */ private static class BtBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) { //開啓搜索 if (onDeviceSearchListener != null) { onDeviceSearchListener.onDiscoveryStart(); //開啓搜索回調 } } else if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {//完成搜素 if (onDeviceSearchListener != null) { onDeviceSearchListener.onDiscoveryStop(); //完成搜素回調 } } else if (TextUtils.equals(action, BluetoothDevice.ACTION_FOUND)) { //3.0搜索到設備 //藍牙設備 BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //信號強度 int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE); Log.d(TAG, "掃描到設備:" + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress()); if (onDeviceSearchListener != null) { onDeviceSearchListener.onDeviceFound(bluetoothDevice,rssi); //3.0搜素到設備回調 } } } /** * 藍牙設備搜索監聽者 * 1、開啓搜索 * 2、完成搜索 * 3、搜索到設備 */ public interface OnDeviceSearchListener { void onDiscoveryStart(); //開啓搜索 void onDiscoveryStop(); //完成搜索 void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi); //搜索到設備 } private OnDeviceSearchListener onDeviceSearchListener; public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) { this.onDeviceSearchListener = onDeviceSearchListener; } }
(3)註冊廣播接收器
private void initBtBroadcast() { //註冊廣播接收 btBroadcastReceiver = new BtBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //開始掃描 intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//掃描結束 intentFilter.addAction(BluetoothDevice.ACTION_FOUND);//搜索到設備 registerReceiver(btBroadcastReceiver,intentFilter); }
(4)註冊過廣播之後,要記得在onDestroy()中註銷廣播
@Override protected void onDestroy() { super.onDestroy(); //註銷廣播接收 unregisterReceiver(btBroadcastReceiver); }
5、搜索到目標設備之後,就可以與藍牙設備建立連接。
(1)發起連接
/** * 開始連接設備 * @param bluetoothDevice 藍牙設備 * @param uuid 發起連接的UUID * @param conOutTime 連接超時時間 */ public void startConnectDevice(final BluetoothDevice bluetoothDevice, String uuid, long conOutTime){ if(bluetoothDevice == null){ Log.e(TAG,"startConnectDevice-->bluetoothDevice == null"); return; } if(bluetoothAdapter == null){ Log.e(TAG,"startConnectDevice-->bluetooth3Adapter == null"); return; } //發起連接 connectThread = new ConnectThread(bluetoothAdapter,curBluetoothDevice,uuid); connectThread.setOnBluetoothConnectListener(new ConnectThread.OnBluetoothConnectListener() { @Override public void onStartConn() { Log.d(TAG,"startConnectDevice-->開始連接..." + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress()); } @Override public void onConnSuccess(BluetoothSocket bluetoothSocket) { //移除連接超時 mHandler.removeCallbacks(connectOuttimeRunnable); Log.d(TAG,"startConnectDevice-->移除連接超時"); Log.w(TAG,"startConnectDevice-->連接成功"); Message message = new Message(); message.what = CONNECT_SUCCESS; mHandler.sendMessage(message); //標記當前連接狀態爲true curConnState = true; //管理連接,收發數據 managerConnectSendReceiveData(bluetoothSocket); } @Override public void onConnFailure(String errorMsg) { Log.e(TAG,"startConnectDevice-->" + errorMsg); Message message = new Message(); message.what = CONNECT_FAILURE; mHandler.sendMessage(message); //標記當前連接狀態爲false curConnState = false; //斷開管理連接 clearConnectedThread(); } }); connectThread.start(); //設置連接超時時間 mHandler.postDelayed(connectOuttimeRunnable,conOutTime); } //連接超時 private Runnable connectOuttimeRunnable = new Runnable() { @Override public void run() { Log.e(TAG,"startConnectDevice-->連接超時" ); Message message = new Message(); message.what = CONNECT_FAILURE; mHandler.sendMessage(message); //標記當前連接狀態爲false curConnState = false; //斷開管理連接 clearConnectedThread(); } };
(2)管理連接,數據收發
managerConnectSendReceiveData()方法中,connectedThread對象進行數據發送結果、接收結果監聽。
/** * 管理已建立的連接,收發數據 * @param bluetoothSocket 已建立的連接 */ public void managerConnectSendReceiveData(BluetoothSocket bluetoothSocket){ //管理已有連接 connectedThread = new ConnectedThread(bluetoothSocket); connectedThread.start(); connectedThread.setOnSendReceiveDataListener(new ConnectedThread.OnSendReceiveDataListener() { @Override public void onSendDataSuccess(byte[] data) { Log.w(TAG,"發送數據成功,長度" + data.length + "->" + bytes2HexString(data,data.length)); Message message = new Message(); message.what = SEND_SUCCESS; message.obj = "發送數據成功,長度" + data.length + "->" + bytes2HexString(data,data.length); mHandler.sendMessage(message); } @Override public void onSendDataError(byte[] data,String errorMsg) { Log.e(TAG,"發送數據出錯,長度" + data.length + "->" + bytes2HexString(data,data.length)); Message message = new Message(); message.what = SEND_FAILURE; message.obj = "發送數據出錯,長度" + data.length + "->" + bytes2HexString(data,data.length); mHandler.sendMessage(message); } @Override public void onReceiveDataSuccess(byte[] buffer) { Log.w(TAG,"成功接收數據,長度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length)); Message message = new Message(); message.what = RECEIVE_SUCCESS; message.obj = "成功接收數據,長度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length); mHandler.sendMessage(message); } @Override public void onReceiveDataError(String errorMsg) { Log.e(TAG,"接收數據出錯:" + errorMsg); Message message = new Message(); message.what = RECEIVE_FAILURE; message.obj = "接收數據出錯:" + errorMsg; mHandler.sendMessage(message); } }); }
sendData()方法中,connectedThread對象發送數據。
/** * 發送數據 * @param data 要發送的數據 字符串 * @param isHex 是否是16進制字符串 * @return true 發送成功 false 發送失敗 */ public boolean sendData(String data,boolean isHex){ if(connectedThread == null){ Log.e(TAG,"sendData:string -->connectedThread == null"); return false; } if(data == null || data.length() == 0){ Log.e(TAG,"sendData:string-->要發送的數據爲空"); return false; } if(isHex){ //是16進制字符串 data.replace(" ",""); //取消空格 //檢查16進制數據是否合法 if(data.length() % 2 != 0){ //不合法,最後一位自動填充0 String lasts = "0" + data.charAt(data.length() - 1); data = data.substring(0,data.length() - 2) + lasts; } Log.d(TAG,"sendData:string -->準備寫入:" + data); //加空格顯示 return connectedThread.write(hexString2Bytes(data)); } //普通字符串 Log.d(TAG,"sendData:string -->準備寫入:" + data); return connectedThread.write(data.getBytes()); }
6、與藍牙設備通訊結束之後,可與藍牙設備斷開連接。
/** * 斷開已有的連接 */ public void clearConnectedThread(){ Log.d(TAG,"clearConnectedThread-->即將斷開"); //connectedThread斷開已有連接 if(connectedThread == null){ Log.e(TAG,"clearConnectedThread-->connectedThread == null"); return; } connectedThread.terminalClose(connectThread); //等待線程運行完後再斷開 mHandler.postDelayed(new Runnable() { @Override public void run() { connectedThread.cancel(); //釋放連接 connectedThread = null; } },10); Log.w(TAG,"clearConnectedThread-->成功斷開連接"); Message message = new Message(); message.what = DISCONNECT_SUCCESS; mHandler.sendMessage(message); }
7、項目演示
(1)掃描到設備,點擊“連接”按鈕, 會在“搜索”按鈕下方顯示連接結果 。注意經典藍牙連接是,第一次連接時會有彈出一個配對框,這個具體配對方式是藍牙設備開發人員設置的。
(2)手機給藍牙設備(設備名爲:BTyqy)發送數據成功之後,藍牙設備把接收到的數據再回發送給手機。
(3)斷開連接。點擊“斷開”按鈕, 會在“搜索”按鈕下方顯示斷開結果 。