前言
最近一直在思考一個問題,如何寫文章?即內容高質量又通俗易懂,讓新手既明白其中蘊含的真理又能輕松跑起第一個程序,同時也能讓高手溫故知新,如獲新歡。經過長時間的思索,最終定位為,內容高質量,描述簡潔,思路清晰,對讀者負責任的文章。初出茅廬,不會高手的底層功力,也不會段子手的套路人心,但,堅持做自己,盡自己所能,為人民服務。
BLE的一些關鍵概念
在Android應用層開發BLE,不懂一些理論和協議也沒關系,照樣可以上手開發。本著知其然知其所以然,下面知識點的理解,能夠有力支撐使用Android API。
藍牙類別
低功耗藍牙是不能兼容經典藍牙的,需要兼容,只能選擇雙模藍牙。
- 低功耗藍牙:字如其名,第一特點就是低功耗,一個紐扣電池可以支持其運行數月至數年,至于怎么實現低功耗,看下文。小體積,低成本,在某寶上的價格有提供郵票體積大小,價格三四塊前的藍牙模塊,可以想象,廠商批發價格會更低。應用場景廣,可以想想,現在的智能家居,智能音箱,智能手表等等物聯網設備,大多數通過BLE進行配網和數據交互。
- 經典藍牙:經典藍牙,泛指藍牙4.0以下的都是經典藍牙,藍牙4.0以上的,你還懷念通過藍牙讓音箱播放手機的音樂么?經典藍牙常用在語音、音樂等較高數據量傳輸的應用場景上。
- 雙模藍牙:即在藍牙模塊中兼容BLE和BT.
在Android 4.3及更高版本,Android 藍牙堆棧可提供實現藍牙低功耗 (BLE) 的功能,在 Android 8.0 中,原生藍牙堆棧完全符合藍牙 5 的要求。也就是說在Android 4.3以上,我們可以通過Android 原生API和藍牙設備交互。
GAP(Generic Access Profile)
GAP用來控制藍牙設備的廣播和連接。GAP可以使藍牙設備被其他藍牙設備發現,并決定是否可以被連接。GAP協議將藍牙設備分為中心設備和外圍設備。
- 中心設備功能比強大,用來連接外圍設備,處理數據等。例如手機。
- 外圍設備一般指非常小和低功耗的設備,用來提供數據,連接功能相對較強大的中心設備。例如體溫計,小米手環等。
外圍設備通過廣播數據或掃描回復兩種方式之一讓中心設備發現,然后進行連接,從而達到進行數據交互的前提條件。為了達到低功耗,外圍設備并不是一直廣播,會設定一個廣播間隔,每個廣播間隔中,它會重新發送自己的廣播數據。廣播間隔越長,越省電,同時也不太容易掃描到。
在Android開發中,常通過藍牙MAC進行連接,連接成功后就可以進行交互嘹。
GATT(Generic Attribute Profile)
簡單理解為普通屬性描述,BLE連接成功后,BLE設備基于該描述進行發送和接收類似“屬性”的較短數據。目前大多數BLE屬性描述是基于GATT。一般一個Profile代表了一個特殊的功能應用,例如心率或者電量應用。
ATT(Attribute Protocol)
GATT是基于ATT上實現的,ATT是運行在BLE設備中,它們之間以盡可能小的屬性在進行交互,而屬性則是以Service和Characteristic的形式在ATT上傳輸。下圖是GATT的結構。
- Characteristic 一個特性(Characteristic)包含一個值(value)和0至n個描述符(descriptors),而每個描述符又可以代表特性的值。
- Descriptor 描述符是用來定義代表Characteristic的值的屬性。例如用來描述心率的取值范圍和單位。
- Service 一個Profile代表著一個應用,而Service代表該應用可以提供多少種服務。例如心率監視器提供心率值檢測服務,Service內包含著多個Characteristic。
Service和Characteristic都通過16位或128位的UUID進行識別,16位的UUID需要向官方購買,全球唯一,而120位可以自己定義。一般UUID由硬件部門或者廠商提供。數據的交互都是客戶端發起請求,服務端響應,客戶端進行讀寫從而達到全雙工。
在BLE連接中,定義者兩個角色,GATT客戶端和Gatt服務端,一般認為,主動發起數據請求的是Client,而響應數據結果的是Server。例如手機和手環。在數據交互的過程中,永遠是Client單方面發起請求,然后讀寫Server相關屬性達到全雙工效果。
理論知識就講到這里了哇,下面進行Android應用層的開發哦。
實戰
實戰部分的內容,大多數和藍牙實現聊天功能是一致的。但為了沒有看過這邊文章的同學,我就Ctrl+c
和Ctrl-v
一下,順便修改一下代碼。
聲明權限
在AndroidManifest.xml配置下面代碼,讓APP具有藍牙訪問權限和發現周邊藍牙權限。
//使用藍牙需要該權限
<uses-permission android:name="android.permission.BLUETOOTH"/>
//使用掃描和設置需要權限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
//Android 6.0以上聲明一下兩個權限之一即可。聲明位置權限,不然掃描或者發現藍牙功能用不了哦
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
為了適配Android 6.0,在主Activity中添加動態申請定位權限代碼,不添加掃描不到藍牙代碼哦。
/**
* Android 6.0 動態申請授權定位信息權限,否則掃描藍牙列表為空
*/
private void requestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_COARSE_LOCATION)) {
Toast.makeText(this, "使用藍牙需要授權定位信息", Toast.LENGTH_LONG).show();
}
//請求權限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
REQUEST_ACCESS_COARSE_LOCATION_PERMISSION);
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_ACCESS_COARSE_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//用戶授權
} else {
finish();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
檢測設備是否支持BLE功能
避免部分同學在不支持藍牙的手機或者設備安裝了Demo,或者安裝在模擬器了。
/**
* 是否支持BLE
*/
private boolean isSupportBLE() {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = manager.getAdapter();
//設備是否支持藍牙
if (mBluetoothAdapter == null
//系統是否支持BLE
&& !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.e(TAG, "not support bluetooth");
return true;
} else {
Log.e(TAG, " support bluetooth");
return false;
}
}
/**
* 彈出不支持低功耗藍牙對話框
*/
private void showNotSupportBluetoothDialog() {
AlertDialog dialog = new AlertDialog.Builder(this).setTitle("當前設備不支持BLE").create();
dialog.show();
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
finish();
}
});
}
開啟藍牙
有了支持BLE的手機,那么要檢測手機藍牙是否打開。如果沒有打開則打開藍牙和監聽藍牙的狀態變化的廣播。藍牙打開后,掃描周邊藍牙設備。
//開啟藍牙
private void enableBLE() {
if (mBluetoothAdapter.isEnabled()) {
startScan();
} else {
mBluetoothAdapter.enable();
}
}
//注冊監聽藍牙狀態變化廣播
private void registerBluetoothReceiver() {
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(bluetoothReceiver, filter);
}
BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
int state = mBluetoothAdapter.getState();
if (state == BluetoothAdapter.STATE_ON) {
startScan();
}
}
}
};
掃描
Android 5.0以上的掃描API和Android 5.0以下的API已經不一樣了。藍牙掃描是非常耗電的,Android 默認在手機息屏停止掃描,在手機亮屏后開始掃描。為了更好的降低耗電,正式APP應該主動關閉掃描,不應該循環掃描。BLE掃描速度非常快,我們根據掃描到的藍牙設備MAC保存Set集合中,過濾掉重復的設備。
private void startScan() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
//android 5.0之前的掃描方式
mBluetoothAdapter.startLeScan(new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
}
});
} else {
//android 5.0之后的掃描方式
scanner = mBluetoothAdapter.getBluetoothLeScanner();
scanCallback=new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
//停止掃描
if (firstScan){
handler.postDelayed(new Runnable() {
@Override
public void run() {
scanner.stopScan(scanCallback);
}
},SCAN_TIME);
firstScan=false;
}
String mac=result.getDevice().getAddress();
Log.i(TAG,"mac:"+mac);
//過濾重復的mac
if (!macSet.contains(mac)){
macSet.add(result.getDevice().getAddress());
deviceList.add(result.getDevice());
deviceAdapter.notifyDataSetChanged();
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
//需要藍牙芯片支持,支持批量掃描結果。此方法和onScanResult是互斥的,只會回調其中之一
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG,"掃描失敗:"+errorCode);
}
};
scanner.startScan(scanCallback);
}
}
這里主要實現的Android 5.0后的掃描,通過將掃描到的設備添加到list,并顯示到界面上。由于可能掃描到重復的藍牙設備,通過Set過濾掉重復的設備。
抽象類ScanCallback作為BLE掃描的回調,重寫其中三個抽象方法。
- onScanResult 一般情況,我們重寫該方法,每掃描到設備則回調一次。
- onBatchScanResults 接口文檔注釋是回調之前已經掃描的的藍牙列表,但實際在測試沒有結果,網上搜了一下,結果在代碼中備注了。
- onScanFailed 掃描失敗
ScanResult掃描結果內包含掃描到的周邊BLE設備BluetoothDevice。通過BluetoothDevice,我們可以獲取周邊BLE的相關信息,例如MAC,連接狀態等。
連接BLE
在上一步獲得我們的BLE列表后,選擇我們要連接的BLE設備,進行連接。處理listview 的點擊效果,進行連接BLE設備。
lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
BluetoothDevice device = deviceList.get(position);
bluetoothGatt = device.connectGatt(MainActivity.this, true, gattCallback);
}
});
通過BluetoothDevice的connectGatt()
方法連接周邊BLE設備。現在明白為何要先了解GATT了吧。connectGatt()
方法有三個參數,第二個參數表示當設備可用時,是否自動連接,第三個參數是BluetoothGattCallback類型,通過該回調,我們可以知道BLE的連接狀態和對Service、Charateristic進行操作,從而進行數據交互。connectGatt()
方法會返回類型BluetoothGatt的實例,通過該實例,我們可以發送請求服務端。
BluetoothGattCallback
抽象類BluetoothGattCallback有很多方法需要我們重寫,我們這里說幾個比較重要的,其他可以看Demo。我們通過定義 GattCallback繼承BluetoothGattCallback,并在類中重寫其方法。這里假設我們通過手機去連接小米手環,那么手機就是Gatt客戶端,小米手環就是Gatt服務端。
-
onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
該方法手機連接或者斷開連接到小米手環會回調該方法。參數一代表當前Gatt客戶端,也就是我們的手機。參數二表示連接或者斷開連接的操作是否成功,只有參數二status
值為GATT_SUCCESS
,參數三才有效。參數三會返回STATE_CONNECTED
和STATE_DISCONNECTED
表示當前客戶端和服務端的連接狀態。連接成功后,我們通過bluetoothGatt
對象的discoverServices()
。 -
onServicesDiscovered(BluetoothGatt gatt, int status)
當發現Service就會回調該方法,參數二值為GATT_SUCCESS
表示服務端的所有服務已經被搜索完畢,此時可以調用bluetoothGatt.getServices()
獲得Service列表,進而獲得所有Characteristic。
也可以通過指定的UUID獲得Service和Characteristic。
private void updateValue() {
BluetoothGattService service = bluetoothGatt.getService(UUID.fromString(serviceUuid));
if (service == null) return;
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(charUuid));
enableNotification(characteristic, charUuid);
characteristic.setValue("on");
}
設置GATT通知
這樣當我們修改characteristic成功后,會回調告知我們。
private void enableNotification(BluetoothGattCharacteristic characteristic,String uuid){
bluetoothGatt.setCharacteristicNotification(characteristic,true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(uuid));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
}
上面代碼設置成功后,會回調BluetoothGattCallback的onCharacteristicChanged()
方法。
如果Characteristic的值被修改,會回調BluetoothGattCallback的onCharacteristicChanged()
方法,在這里我們可以進一步提高用戶體驗。需要注意一下,類BluetoothGattCallback有很多方法需要我們實現,因為Gatt的響應結果都是回調該對象的方法。
小結一下
Gatt客戶端通過BluetoothDevice的connectGatt()
方法與服務端連接成功后,利用返回的BluetoothGatt對象,請求Gatt服務端相關數據。Gatt服務端根據請求,將自身的狀態通過回調客戶端傳入的BluetoothGattCallback對象的相關方法,從而告知客戶端。
關閉BLE
當我們使用完BLE之后,應該及時關閉,以釋放相關資源和降低功耗。
public void close() {
if (bluetoothGatt == null) {
return;
}
bluetoothGatt.close();
bluetoothGatt = null;
}
總結
在應用層操作BLE難度不大,因為Android屏蔽了很多藍牙棧協議的細節。但應用層開發會苦于沒有硬件設備支持。通過本文,我們知道BLE的AP和GATT等等一些概念,了解Android BLE開發的整體流程,對BLE有一個感性的認知。
堅持初心,寫優質好文章
開文有益,點贊支持好文
Demo的代碼地址Github