前言#
在做藍牙技術開發前,首先可以了解一下藍牙模組分類:
現今藍牙模組開發分為經典藍牙和低功耗藍牙開發,目前市面上最火的物聯網技術大多選擇低功耗藍牙模組,效率高且功耗低,缺點是只支持小數據量的數據傳輸,在 Android 4.3 (API 18)後開始引入;而對於數據量較大的傳輸,如音視頻等開發,則需要使用經典藍牙模組。在開發前一定要先區分藍牙模組類型,因為兩種藍牙模組的開發是不同的,在本篇文章兩種藍牙模組的開發步驟都會介紹到。
一般開發步驟#
藍牙模組開發步驟如下:
兩種藍牙模組的開發步驟大致一樣,僅藍牙連接和藍牙通信時有所區分。
相關 API 介紹#
一、通用 API#
1.BluetoothAdapter#
本地藍牙適配器,用於一些藍牙的基本操作,比如判斷藍牙是否開啟、搜索藍牙設備等。
2.BluetoothDevice#
藍牙設備對象,包含一些藍牙設備的屬性,比如設備名稱、mac 地址等。
二、經典藍牙(BT)API#
1.BluetoothSocket#
表示藍牙 socket 的接口(與 TCP Socket 類似,關於 socket 的概念請自行查閱計算機網絡的相關內容)。該類的對象作為應用中數據傳輸的連接點。
2.BluetoothServerSocket#
表示服務器 socket,用來監聽未來的請求(和 TCP ServerSocket 類似)。為了能使兩個藍牙設備進行連接,一個設備必須使用該類開啟服務器 socket,當遠程的藍牙設備請求該服務端設備時,如果連接被接受,BluetoothServerSocket 將會返回一個已連接的 BluetoothSocket 類對象。
3.BluetoothClass#
描述藍牙設備的主要特徵。BluetoothClass 的類對象是一個只讀的藍牙設備的屬性集。儘管該類對象並不能可靠地描述 BluetoothProfile 的所有內容以及該設備支持的所有服務信息,但是該類對象仍然有助於對該設備的類型進行提示。
4.BluetoothProfile#
表示藍牙規範,藍牙規範是兩個基於藍牙設備通信的標準。
三、低功耗藍牙(BLE)API#
1.BluetoothGatt#
藍牙通用屬性協議,定義了 BLE 通訊的基本規則,是 BluetoothProfile 的實現類,Gatt 是 Generic Attribute Profile 的縮寫,用於連接設備、搜索服務等操作。
2.BluetoothGattCallback#
藍牙設備連接成功後,用於回調一些操作的結果,必須連接成功後才會回調。
3.BluetoothGattService#
藍牙設備提供的服務,是藍牙設備特徵的集合。
4.BluetoothGattCharacteristic#
藍牙設備特徵,是構建 GATT 服務的基本數據單元。
5.BluetoothGattDescriptor#
藍牙設備特徵描述符,是對特徵的額外描述。
1. 加入藍牙權限#
(1)android.permission.BLUETOOTH#
為了能夠在你開發的應用設備中使用藍牙功能,必須聲明藍牙的權限 "BLUETOOTH"。在進行藍牙的通信,例如請求連接,接受連接以及交換數據中,需要用到該權限
(2)android.permission.BLUETOOTH_ADMIN#
如果你的應用程序需要實例化藍牙設備的搜索或者對藍牙的設置進行操作,那麼必須聲明 BLUETOOTH_ADMIN 權限。大多數應用需要該權限對本地的藍牙設備進行搜索。該權限的其他能力並不應當被使用,除非你的應用是一個電源管理的應用,需要對藍牙的設置進行修改
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
這裡需要注意的是,Google 在 Android 6.0 之後,為了更好的保護用戶的數據安全,所有需要訪問硬件唯一標識符的地方都需要申請位置權限,而且搜索周圍的藍牙設備,需要手機提供位置服務,否則調用藍牙掃描將搜索不到任何結果。
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
在 AndroidManifest.xml 中加入位置權限,位置權限屬於 Dangerous 級別的權限,別忘了在代碼裡邊還需要動態申請。
2. 是否支持藍牙#
//獲取藍牙適配器 若適配器為空則當前手機不支持藍牙
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
Toast.makeText(this, "當前手機設備不支持藍牙", Toast.LENGTH_SHORT).show()
}
3. 是否打開藍牙#
(1)判斷藍牙是否打開#
//手機設備支持藍牙,判斷藍牙是否已開啟
if (bluetoothAdapter!!.isEnabled) {
Toast.makeText(this, "手機藍牙已開啟", Toast.LENGTH_SHORT).show()
//可以進行搜索藍牙的操作
searchBtDevice()
} else {
//藍牙沒有打開,去打開藍牙。推薦使用第二種打開藍牙方式
//第一種方式:直接打開手機藍牙,沒有任何提示,一般不採用這種方法
// bluetoothAdapter.enable(); //BLUETOOTH_ADMIN權限
//第二種方式:友好提示用戶打開藍牙
val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
(2)友好提示用戶打開藍牙#
//友好提示用戶打開藍牙
val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
(3)監聽用戶是否打開藍牙的操作回調#
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == Activity.RESULT_OK) {
// 藍牙已打開
//可以開始搜索藍牙 獲取藍牙列表
searchBtDevice()
} else {
// 藍牙未打開 一般操作是彈出確認彈框提示用戶繼續打開藍牙
DialogUtil.getInstance().showConfirmDialog(this, "提示", "藍牙沒有開啟,是否打開藍牙?", "打開",
"取消", object : DialogUtil.DialogConfirmListener<Any> {
override fun cancel() {
//如果用戶選擇取消的話 可以關閉當前頁面或者提示藍牙未打開
finish()
}
override fun confirm(data: Any?) {
//繼續友好提示用戶打開藍牙
val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
})
}
}
}
4. 掃描藍牙#
藍牙掃描共有兩種方式,一種是適於經典藍牙開發模式的調用藍牙適配器 bluetoothAdapter.startDiscovery () 搜索藍牙,通過註冊藍牙廣播接收器獲取掃描到的藍牙信息,這種搜索方法不管是經典藍牙還是低功耗藍牙都能全部掃描出來;另一種方法則是針對低功耗藍牙進行搜索掃描,只有低功耗藍牙模式的藍牙信息才會被掃描出來。
(1)經典藍牙搜索#
① 新建藍牙廣播接收器#
public class BroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "BluetoothReceiver";
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
LogUtil.logNormalMsg("----藍牙廣播接收器----------action----------" + action);
//開啟搜索
if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) {
if (onDeviceSearchListener != null) {
onDeviceSearchListener.onDiscoveryStart(); //開啟搜索回調
}
findConnectedBluetooth();
} 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);
LogUtil.logNormalMsg("TAG", "掃描到設備:" + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
if (onDeviceSearchListener != null) {
//3.0搜索到設備回調
onDeviceSearchListener.onDeviceFound(bluetoothDevice, rssi);
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
//藍牙已斷開
//藍牙設備
BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
LogUtil.logNormalMsg("--------------藍牙已斷開---------------");
if (onDeviceSearchListener != null) {
//判斷是否是當前藍牙連接斷開了
if (bluetoothDevice.getAddress().equals(Content.bluetoothMac)){
onDeviceSearchListener.onDeviceDisconnect();
}
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_ACL_CONNECTED)) {
//藍牙已連接
LogUtil.logNormalMsg("--------------藍牙已連接---------------");
if (onDeviceSearchListener != null) {
onDeviceSearchListener.onDeviceConnect();
}
} else if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) {
//經典藍牙的配對請求 可以用於攔截配對框,在代碼裡設置配對碼
}
}
/**
* 藍牙設備搜索監聽者
* 1、開啟搜索
* 2、完成搜索
* 3、搜索到設備
*/
public interface OnDeviceSearchListener {
void onDiscoveryStart(); //開啟搜索
void onDiscoveryStop(); //完成搜索
void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi); //搜索到設備
//設備斷開連接
void onDeviceDisconnect();
//設備連接成功
void onDeviceConnect();
}
private OnDeviceSearchListener onDeviceSearchListener;
public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) {
this.onDeviceSearchListener = onDeviceSearchListener;
}
}
② 動態註冊藍牙廣播接收器#
/**
* 註冊藍牙廣播接收
*/
private fun initBtBroadcast() {
//註冊廣播接收
btBroadcastReceiver = BtBroadcastReceiver()
//這裡需要當前的Activity實現(implements)一下藍牙設備搜索監聽者OnDeviceSearchListener
btBroadcastReceiver!!.setOnDeviceSearchListener(this)
val intentFilter = IntentFilter()
//將優先級調高
intentFilter.priority = 1001
//添加Action
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) //開始掃描
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)//掃描結束
intentFilter.addAction(BluetoothDevice.ACTION_FOUND)//搜索到設備
intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)//斷開連接
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)//連接
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)//藍牙配對請求
registerReceiver(btBroadcastReceiver, intentFilter)
}
② 開啟掃描藍牙#
/**
* 開始搜索藍牙
*/
private fun searchBtDevice() {
if (bluetoothAdapter!!.isDiscovering) {
//當前正在搜索設備...停止當前搜索
bluetoothAdapter!!.cancelDiscovery()
}
//開始搜索
bluetoothAdapter!!.startDiscovery()
}
開啟掃描後在藍牙廣播接收器中可以監聽到手機掃描到的藍牙,然後通過藍牙設備搜索監聽者 OnDeviceSearchListener 方法回調中拿到藍牙設備信息。
override fun onDeviceFound(device: BluetoothDevice?, rssi: Int) {
//每掃描到藍牙信息都會回調該方法 這裡可以拿到藍牙設備信息和信號強度
}
到這一步的話,開發者可以將掃描到的藍牙設備信息列表展示,點擊列表即可做藍牙連接的操作。
(2)低功耗藍牙搜索#
通過 startLeScan () 方法開啟掃描,掃描結果直接在 BluetoothAdapter.LeScanCallback 或者 ScanCallback 中回調處理,不用經典藍牙模式需要註冊廣播才能獲取掃描到的藍牙信息,這裡注意不同版本調用 startScan 方法的邏輯不一樣,目前大多數 android 版本都在 5.0 以上,基本上選用後者的掃描方法即可
//1. Android 4.3以上,Android 5.0以下
mBluetoothAdapter.startLeScan(BluetoothAdapter.LeScanCallback LeScanCallback);
//2. Android 5.0以上,掃描的結果在mScanCallback中進行處理
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
mBluetoothLeScanner.startScan(ScanCallback mScanCallback);
回調處理:
public abstract class ScanCallback {
/**
* Callback when a BLE advertisement has been found.
*
* @param callbackType Determines how this callback was triggered. Could be one of {@link
* ScanSettings#CALLBACK_TYPE_ALL_MATCHES}, {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or
* {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST}
* @param result A Bluetooth LE scan result.
*/
public void onScanResult(int callbackType, ScanResult result) {
//這裡可以處理藍牙掃描結果
BluetoothDevice bluetoothDevice = result.getDevice();
}
/**
* Callback when batch results are delivered.
*
* @param results List of scan results that are previously scanned.
*/
public void onBatchScanResults(List<ScanResult> results) {
}
/**
* Callback when scan could not be started.
*
* @param errorCode Error code (one of SCAN_FAILED_*) for scan failure.
*/
public void onScanFailed(int errorCode) {
}
}
如果只是涉及到低功耗藍牙的技術開發,選擇第二種方法掃描藍牙就可以了,比較簡便,反之選擇第一種方法。
5. 藍牙連接與斷開#
(1)經典藍牙(BT)#
APP 與經典藍牙設備連接一般通過 BluetoothDevice 對象的 createRfcommSocketToServiceRecord 方法獲取 BluetoothSocket,類似 Socket 編程,調用 connect () 方法開始連接,連接成功以後通過 BluetoothSocket 的 getInputStream () 和 getOutputStream () 方法獲取輸入流和輸出流,進而與藍牙進行通信。createRfcommSocketToServiceRecord 方法需要傳入 UUID 參數,一般通過需要連接的藍牙設備參數得知,沒法獲取 UUID 也沒關係,可以通過反射機制得到 BluetoothSocket 對象,再進行連接,這裡需要注意藍牙連接是一個耗時的過程,不能在主線程中執行。
① createRfcommSocketToServiceRecord 獲取 BluetoothSocket#
//1、獲取BluetoothSocket
try {
//建立安全的藍牙連接,會彈出配對框
mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG, "獲取BluetoothSocket異常!" + e.getMessage());
}
② 反射獲取 BluetoothSocket#
//1、獲取BluetoothSocket
try {
//通過反射獲取Socket
mSocket = (BluetoothSocket) mDevice.getClass().
getMethod("createRfcommSocket", new Class[]{int.class})
.invoke(mDevice, 1);
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG, "獲取BluetoothSocket異常!" + e.getMessage());
}
③ 連接藍牙#
獲取 BluetoothSocket 後就可以與藍牙設備連接了,調用 connect () 連接方法後一般手機就會彈出藍牙配對框,有一些藍牙設備不會出現彈框因為藍牙模組沒有設置配對碼,這裡只針對有配對碼的藍牙模組,手動輸入配對碼後就成功連接了,一般藍牙開發設備的配對碼是 0000 或者 1234,在考慮用戶體驗的時候,一般不讓用戶自己輸入配對碼,這一步操作顯得繁瑣多餘,APP 開發時一般都考慮在連接時靜默設置藍牙的配對碼,在用戶無感知的情況下進行連接和配對操作。做這一步操作就需要開發者攔截藍牙配對的 Action (BluetoothDevice.ACTION_PAIRING_REQUEST),在代碼中進行配對處理。在 BroadcastReceiver 藍牙廣播接收器中增加對藍牙配對請求的攔截處理。
mSocket.connect();
④ 靜默設置配對碼#
if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) {
BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//藍牙配對
Log.i(TAG, "##### 是配對請求的請求 ######");
Bundle extras = intent.getExtras();
Log.i(TAG, "-->" + extras.toString());
Object device = extras.get("android.bluetooth.device.extra.DEVICE");
Object pairKey = BaseData.bluetoothPassword;
Log.i(TAG, "device-->" + String.valueOf(device));
Log.i(TAG, "pairkey-->" + String.valueOf(pairKey));
try {
//中斷配對廣播傳遞 如果沒有將廣播終止,則會出現一個一閃而過的配對框。
abortBroadcast();
//調用setPin方法進行配對...
setPin(btDevice.getClass(), btDevice, String.valueOf(pairKey));
} catch (Exception e) {
e.printStackTrace();
}
}
記得在動態註冊藍牙廣播接收器的時候添加一下藍牙配對請求的 Action
//藍牙配對請求Action
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
設置配對碼
/**
* 設置藍牙配對碼
* @param btClass btClass
* @param btDevice BluetoothDevice
* @param str str
* @return boolean
*/
public boolean setPin(Class btClass, BluetoothDevice btDevice, String str) {
boolean flag = false;
try {
Class[] arrayOfClass = new Class[1];
arrayOfClass[0] = byte[].class;
Method removeBondMethod = btClass.getDeclaredMethod("setPin", arrayOfClass);
Object[] arrayOfObject = new Object[1];
arrayOfObject[0] = str.getBytes();
flag = (Boolean) removeBondMethod.invoke(btDevice, arrayOfObject);
LogUtil.logNormalMsg(TAG, "setPin result: " + flag);
} catch (Exception e) {
e.printStackTrace();
}
if (flag) {
LogUtil.logNormalMsg("-------設置藍牙配對碼成功");
} else {
LogUtil.logNormalMsg("-------設置藍牙配對碼失敗");
}
return flag;
}
這裡在貼一下我自己藍牙連接的代碼:
public class ConnectThread {
private static final String TAG = "ConnectThread";
private final BluetoothAdapter mBluetoothAdapter;
private BluetoothSocket mmSocket;
private final BluetoothDevice mDevice;
public ConnectThread(BluetoothAdapter bluetoothAdapter, BluetoothDevice bluetoothDevice, String uuid) {
this.mBluetoothAdapter = bluetoothAdapter;
this.mDevice = bluetoothDevice;
//使用一個臨時變量,等會賦值給mmSocket
//因為mmSocket是靜態的
BluetoothSocket tmp = null;
if (mmSocket != null) {
LogUtil.logNormalMsg(TAG, "ConnectThread-->mmSocket != null先去釋放");
try {
mmSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
LogUtil.logNormalMsg(TAG, "ConnectThread-->mmSocket != null已釋放");
//1、獲取BluetoothSocket
try {
//建立安全的藍牙連接,會彈出配對框
tmp = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG, "ConnectThread-->獲取BluetoothSocket異常!" + e.getMessage());
}
mmSocket = tmp;
if (mmSocket != null) {
LogUtil.logNormalMsg(TAG, "ConnectThread-->已獲取BluetoothSocket");
}
}
public void connectBluetooth() {
connect();
}
private void connect() {
//連接之前先取消發現設備,否則會大幅降低連接嘗試的速度,並增加連接失敗的可能性
if (mBluetoothAdapter == null) {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->mBluetoothAdapter == null");
return;
}
//取消發現設備
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
if (mmSocket == null) {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->mmSocket == null");
return;
}
boolean firstFlag = false;
//2、通過socket去連接設備
try {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->去連接...");
if (onBluetoothConnectListener != null) {
onBluetoothConnectListener.onStartConn(); //開始去連接回調
}
mmSocket.connect();
firstFlag = true;
} catch (Exception e) {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->第一次連接異常!" + e.getMessage());
boolean flag = false;
try {
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->嘗試第二次通過反射獲取Socket連接");
mmSocket = (BluetoothSocket) mDevice.getClass().
getMethod("createRfcommSocket", new Class[]{int.class})
.invoke(mDevice, 1);
mmSocket.connect();
flag = true;
Log.e(TAG, "mmSocket.isConnected():" + mmSocket.isConnected());
} catch (Exception e2) {
LogUtil.logNormalMsg("mmSocket連接失敗:" + e.getMessage());
e2.printStackTrace();
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->連接異常2!" + e2.getMessage());
if (onBluetoothConnectListener != null) {
onBluetoothConnectListener.onConnFailure("連接異常:" + e.getMessage());
}
//釋放
cancel();
} finally {
if (flag) {
if (onBluetoothConnectListener != null) {
//連接成功回調
onBluetoothConnectListener.onConnSuccess(mmSocket);
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->連接成功");
}
} else {
if (onBluetoothConnectListener != null) {
//連接失敗回調
onBluetoothConnectListener.onConnFailure("");
}
}
}
} finally {
if (firstFlag) {
if (onBluetoothConnectListener != null) {
//連接成功回調
onBluetoothConnectListener.onConnSuccess(mmSocket);
LogUtil.logNormalMsg(TAG, "ConnectThread:run-->連接成功");
}
}
}
}
/**
* 釋放
*/
public void cancel() {
try {
if (mmSocket != null && mmSocket.isConnected()) {
LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected());
mmSocket.close();
mmSocket = null;
return;
}
if (mmSocket != null) {
mmSocket.close();
mmSocket = null;
}
LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->關閉已連接的套接字釋放資源");
} catch (IOException e) {
LogUtil.logNormalMsg(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); //連接失敗
}
}
藍牙連接成功後,就可通過 BluetoothSocket 的輸入輸出流和藍牙設備進行通信了。
需要注意的是,靜默設置藍牙配對碼一般需要和藍牙設備廠家協商一個固定的配對碼,不然藍牙配對碼一直改變的話會導致 APP 連接不上藍牙而且沒有任何錯誤提示。
⑤ 斷開連接#
斷開經典藍牙連接其實就是關閉 BluetoothSocket 和輸入輸出流,這裡注意需要先把輸入輸出流關閉再將 BluetoothSocket 關閉。
try {
if(mInStream != null){
mInStream.close(); //關閉輸入流
}
if(mOutStream != null){
mOutStream.close(); //關閉輸出流
}
if(mSocket != null){
mSocket.close(); //關閉socket
}
mInStream = null;
mOutStream = null;
mmSocket = null;
LogUtil.logNormalMsg(TAG,"成功斷開連接");
} catch (Exception e) {
e.printStackTrace();
// 任何一部分報錯,都將強制關閉socket連接
mInStream = null;
mOutStream = null;
mmSocket = null;
LogUtil.logNormalMsg(TAG, "斷開連接異常!" + e.getMessage());
}
(2)低功耗藍牙(BLE)#
低功耗藍牙的連接稍微複雜一點:
連接之前#
在連接藍牙這之前需要對 GATT 了解一下,方便加深我們對低功耗藍牙開發技術的理解。
GATT 層次結構#
該層次結構的頂層是一個 profile(概要文件)。profile 由滿足 use case(用例)所需的一個或多個 services(服務)組成。services(服務)由對其他服務的 characteristics(特徵)或 references(引用)組成。每個 characteristics(特徵)都包含一個值,並且可以包含關於該值的可選信息。service(服務)和 characteristic(特性)以及 characteristic(特性)的組成部分 (即、值和 descriptors(描述符)) 包含 profile data,並且都存儲在 server(服務器)上的 Attributes(屬性)中。
各成員作用#
1、Profile
Profile 規格規定了交換配置文件數據的架構。此架構定義了配置文件所用的基本元素,例如服務和特徵。該層級的最高層是配置文件(profile)。配置文件由實現用例所需的一個或多個服務組成。服務由特徵或有關其它服務的引用組成。每一個特徵包括一個值,還可能包括有關該值的可選信息。服務、特徵以及特徵的組件(即特徵值和特徵描述符)構成了配置文件數據,並全部存儲在服務器的屬性中。沒錯,上面這是摘抄的,說簡單點就是協議規範,各個藍牙兼容都靠這個規範,使用標準的 Profile 就不需要過多關注它了。
2、Service
服務分管不同的數據處理,例如有電池電量的,有心率的,它不參與具體的數據交互,承擔的是功能分區的職責。每個服務都有一個 UUID 進行區分。
3、Characteristic
特徵是直接參與數據交互的成員之一。特徵也有一個 UUID 進行標識,通過 UUID 獲取到特徵,修改特徵裡面的 Value 達到通訊目的。這樣的通訊模式和觀察者模式相似,當 Value 有改變的時候會通知監聽這個特徵的觀察者,告訴它數據有改變,觀察者去獲取數據就完成了一次通訊,具體的實現往後詳述。
4、Descriptor
對特徵值的描述,是用來定義特徵值的已定義屬性。例如:描述符可以指定可讀的描述,特徵值的可接受範圍或者特徵值特定的度量單位。這個東西用的不是很多,假如中心設備收不到數據,那麼就看看是不是 Descriptor 沒有設置可通知。
5、Advertising
廣播,外圍設備讓自身可以被發現的手段,只有發出廣播了,才能通過 Ble 掃描到,一旦設備連接上,廣播就會停止。
低功耗藍牙通過 BluetoothGatt 進行連接和管理,通過 BluetoothDevice 的 connectGatt () 方法可以得到 BluetoothGatt 對象,其中需要傳入三個參數 context 上下文,autoConnect 是否自動連接,callback 連接回調,關鍵在於第三個參數 BluetoothGattCallback,因為開發者需要在 BluetoothGattCallback 回調中發現服務和配置通信管道,同時可以在其中監聽下位機(藍牙模組)回覆的消息。這裡需要注意的是,獲取到 GATT 中的服務 Service、特徵 Characteristic 和特徵值描述 Descriptor 都需要指定 UUID,這些 UUID 一般從藍牙模組廠家獲取,獲取通過代碼方式遍歷所有的 UUID 實驗出正確的 UUID,後續會講到。
connectGatt () 方法源碼:
/**
* Connect to GATT Server hosted by this device. Caller acts as GATT client.
* The callback is used to deliver results to Caller, such as connection status as well
* as any further GATT client operations.
* The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
* GATT client operations.
*
* @param callback GATT callback handler that will receive asynchronous callbacks.
* @param autoConnect Whether to directly connect to the remote device (false) or to
* automatically connect as soon as the remote device becomes available (true).
* @throws IllegalArgumentException if callback is null
*/
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) {
return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
}
① 設置自定義回調#
/**
* 低功耗藍牙連接回調
* 接收藍牙數據
*/
inner class MyBluetoothGattCallback : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
Content.isConnectBluetoothNow = false
//連接成功 開始發現服務
when {
newState == BluetoothAdapter.STATE_CONNECTED -> {
//到這一步 gatt已經連接成功了 可以調用發現gatt的服務
mGatt?.discoverServices()
}
newState == BluetoothAdapter.STATE_DISCONNECTED -> {
//連接斷開
}
status == 133 -> {
//連接失敗
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
//到這一步 已經發現了gatt的所有服務 可以通過gatt.services獲取
//如果我們知道gatt相關的所有UUID 則可以開始配置通信管道
//獲取寫入的特徵值
writeCharact = this.bluetoothGattService?.getCharacteristic(writeUUID)
//獲取通知監聽的特徵值 相當於讀取監聽 開啟監聽後可實時監聽下位機消息
notifyCharact = this.bluetoothGattService?.getCharacteristic(notifyUUID)
//設置開啟監聽 實時監聽下位機消息
//1.設置特徵值通知
mGatt?.setCharacteristicNotification(notifyCharact , true)
//2.獲取descriptor
val descriptor: BluetoothGattDescriptor = notifyCharact ?.getDescriptor(descriptorUUID)!!
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
//3.設置descriptor 這裡設置結果會在onDescriptorWrite方法中回調
mGatt?.writeDescriptor(descriptor)
//至此低功耗藍牙連接和配置完成
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, status)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
val value = characteristic.value
val data = Util.bytesToAscii(value)
//發送數據成功後會回調該方法
LogUtil.logNormalMsg("onCharacteristicWrite", "發送成功:$data")
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
super.onCharacteristicChanged(gatt, characteristic)
// value為設備發送的數據,根據數據協議進行解析
//接收處理數據
handBluetoothData(characteristic.value)
}
override fun onDescriptorRead(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorRead(gatt, descriptor, status)
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorWrite(gatt, descriptor, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
LogUtil.logNormalMsg("LockBluetoothService", "設置Descriptor成功,可以發送數據了")
//一些藍牙模組需要在連接成功後設置mtu 根據開發需要配置
mGatt?.requestMtu(103)
}
}
}
② 啟動藍牙連接#
啟動藍牙藍牙連接後,通過 connectGatt 方法得到 BluetoothGatt 對象,連接結果可在上一步自定義的 BluetoothGattCallback 中回調處理。
mGatt = bluetoothDevice1!!.connectGatt(appContext, false, bluetoothGattCallback)
③ 發現 GATT 服務#
這一步操作需在連接成功後執行,發現服務後才可配置藍牙通信
mGatt?.discoverServices()
④ 配置通信#
獲取 BluetoothGattService 服務和用於讀寫的 BluetoothGattCharacteristic 特徵值,配置藍牙通信。
//獲取寫入的特徵值
writeCharact = this.bluetoothGattService?.getCharacteristic(writeUUID)
//獲取通知監聽的特徵值 相當於讀取監聽 開啟監聽後可實時監聽下位機消息
notifyCharact = this.bluetoothGattService?.getCharacteristic(notifyUUID)
//設置開啟監聽 實時監聽下位機消息
//1.設置特徵值通知
mGatt?.setCharacteristicNotification(notifyCharact , true)
//2.獲取descriptor
val descriptor: BluetoothGattDescriptor = notifyCharact ?.getDescriptor(descriptorUUID)!!
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
//3.設置descriptor 這裡設置結果會在onDescriptorWrite方法中回調
mGatt?.writeDescriptor(descriptor)
//至此低功耗藍牙連接和配置完成 可以進行通信了
一般藍牙開發廠商都會告知相關的 UUID,如果不知道藍牙模組的各 UUID,可以通過遍歷 Gatt 的所有服務查找,直至找到正確的 UUID。
/**
* 查找低功耗藍牙uuid
*/
private fun findUUID(gatt: BluetoothGatt) {
LogUtil.logNormalMsg("gatt.getServices().size():" + gatt.services.size)
if (gatt.services != null && gatt.services.size > 0) {
//遍歷所有服務
for (i in gatt.services.indices) {
val service = gatt.services[i]
LogUtil.logNormalMsg("---------------------------------------------")
//輸出當前服務的UUID
LogUtil.logNormalMsg("service.getUuid=" + service.uuid)
//判斷當前服務是否有特徵值
if (service.characteristics != null && service.characteristics.size > 0) { //遍歷當前所有服務的特徵值
for (j in service.characteristics.indices) {
val characteristic =
service.characteristics[j]
//輸出當前特徵值的UUID
LogUtil.logNormalMsg("characteristic.getUuid=" + characteristic.uuid)
//獲取特徵值屬性
val charaProp = characteristic.properties
//是否支持讀取
if (charaProp or BluetoothGattCharacteristic.PROPERTY_READ > 0) {
LogUtil.logNormalMsg("-------type:PROPERTY_READ")
}
//是否支持寫入
if (charaProp or BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {
LogUtil.logNormalMsg("-------type:PROPERTY_WRITE")
}
//是否支持開啟監聽
if (charaProp or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {
LogUtil.logNormalMsg("-------type:PROPERTY_NOTIFY")
}
if (characteristic.descriptors != null && characteristic.descriptors.size > 0) {
for (descriptor in characteristic.descriptors) {
LogUtil.logNormalMsg("descriptor.getUuid()=" + descriptor.uuid)
}
}
}
}
}
}
}
這裡注意要找全面一點的服務,有些服務下沒有特徵,有些特徵不支持讀寫,有些特徵下沒有描述,無法開啟讀取監聽。
⑤ 斷開連接#
mGatt?.let {
//斷開BluetoothGatt連接
it.disconnect()
//關閉BluetoothGatt
it.close()
}
6. 通信#
(1)經典藍牙通信#
根據 5 中提到的連接邏輯,可以得到 BluetoothSocket,類似 Socket 編程,通過 BluetoothSocket 的到輸入輸出流與藍牙模組進行數據通信。這裡建議另起線程進行通信操作,需要實時監聽 InputStream 輸入流裡邊是否有數據可讀取。
① 獲取輸入輸出流#
//獲取 InputStream 和 OutputStream
try {
mInStream = socket.getInputStream();
mOutStream = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
LogUtil.logNormalMsg(TAG,"獲取InputStream 和 OutputStream異常!");
}
② 發送數據#
//發送數據
public boolean write(byte[] bytes){
try {
if(mOutStream == null){
LogUtil.logNormalMsg(TAG, "mmOutStream == null");
return false;
}
//發送數據
mOutStream.write(bytes);
mOutStream.flush();
Log.d(TAG, "寫入成功:"+ bytes2HexString(bytes, bytes.length));
return true;
} catch (IOException e) {
LogUtil.logNormalMsg("發送數據出錯:"+e.getMessage());
e.printStackTrace();
return false;
}
}
③ 讀取數據#
//線程的run方法
@Override
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(mInStream == null){
LogUtil.logNormalMsg(TAG,"ConnectedThread:run-->輸入流mmInStream == null");
break;
}
//先判斷是否有數據,有數據再讀取
if(mInStream.available() != 0){
//從(mmInStream)輸入流中(讀取內容)讀取的一定數量字節數,並將它們存儲到緩衝區buffer數組中,bytes為實際讀取的字節數
bytes = mInStream.read(buffer);
LogUtil.logNormalMsg(TAG,"讀取數據長度:"+bytes);
//存放實際讀取的數據內容
byte[] b = Arrays.copyOf(buffer,bytes);
//處理藍牙數據
handBluetoothData(b);
}
Thread.sleep(150);
} catch (Exception e) {
LogUtil.logNormalMsg(TAG,"接收消息異常!" + e.getMessage());
//關閉流和socket
boolean isClose = cancel();
if(isClose){
LogUtil.logNormalMsg(TAG,"接收消息異常,成功斷開連接!");
}
break;
}
}
//關閉流和socket
boolean isClose = cancel();
if(isClose){
Log.d(TAG,"接收消息結束,斷開連接!");
}
}
(2)低功耗藍牙通信#
低功耗藍牙通信較為簡單一些,直接通過寫入的特徵值 Characteristic 就可發送數據,而讀取數據在 BluetoothGattCallback 的
onCharacteristicChanged 回調方法中監聽讀取藍牙設備發送過來的數據。
① 發送數據#
fun sendMsgToBluetooth(msg: ByteArray): Boolean {
if (writeCharact != null) {
//設置特徵值的value
writeCharact?.value = msg
//設置特徵值的寫入類型 這裡根據需要選擇類型 詳細可閱讀源碼
writeCharact?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
//通過gatt寫入數據
return mGatt!!.writeCharacteristic(writeCharact)
}
return false
}
② 讀取數據#
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
super.onCharacteristicChanged(gatt, characteristic)
//接收處理數據
handBluetoothData(characteristic.value)
}
7. 補充#
(1)在多次調試中發現,在經典藍牙模式開發中,有兩種情況會掃描不到藍牙設備:
- APP 內斷開藍牙連接,再次掃描藍牙或是重啟 APP 再次掃描;
- 再啟動 APP 時,藍牙設備已與手機 APP 配對連接;
出現這種情況不要慌,只要把與手機藍牙連接的藍牙設備找到並加入到掃描的藍牙列表中,重新點擊藍牙連接就可以解決。
搜索已與手機連接的藍牙:
/**
* 搜索已連接的藍牙
*/
private void findConnectedBluetooth() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
Class<BluetoothAdapter> bluetoothAdapterClass = BluetoothAdapter.class;//得到BluetoothAdapter的Class對象
try {//得到連接狀態的方法
Method method = bluetoothAdapterClass.getDeclaredMethod("getConnectionState", (Class[]) null);
//打開權限
method.setAccessible(true);
int state = (int) method.invoke(adapter, (Object[]) null);
if(state == BluetoothAdapter.STATE_CONNECTED){
LogUtil.logNormalMsg("BLUETOOTH","BluetoothAdapter.STATE_CONNECTED");
Set<BluetoothDevice> devices = adapter.getBondedDevices();
LogUtil.logNormalMsg("BLUETOOTH","devices:"+devices.size());
for(BluetoothDevice device : devices){
Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected", (Class[]) null);
method.setAccessible(true);
boolean isConnected = (boolean) isConnectedMethod.invoke(device, (Object[]) null);
if(isConnected){
//找到已連接的藍牙設備 進行下一步處理
if (device != null) {
onDeviceSearchListener.onDeviceFound(device, 50);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
(2)低功耗藍牙 GATT 的默認 MTU 支持發送的數據量為 23 字節(byte), 除去 GATT 的 opcode 一個字節以及 GATT 的 handle 2 個字節之後,剩下的 20 個字節便是留給 GATT 的了。GATT 默認支持最大 512 字節的數據通信,可以通過設置 MTU 的大小來改變,一般在藍牙連接成功後設置,這裡推薦在 onDescriptorWrite 回調方法中配置:
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorWrite(gatt, descriptor, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
//藍牙連接配置通信成功 可以設置mtu了
mGatt?.requestMtu(103)
}
}
設置 MTU 是否的結果在 onMtuChanged 中可以得到
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
//MTU設置成功 返回當前所支持的mtu大小
this.supportedMTU = mtu
}
}
參考#
安卓 BLE 藍牙開發詳解
Android 從開發角度來看經典藍牙和低功耗(BLE)藍牙的區別
Android 連接經典藍牙
Android 藍牙 BLE 開發詳解