前言
最近一直在思考一個問題,如何寫文章?即內容高質量又通俗易懂,讓新手既明白其中蘊含的真理又能輕鬆跑起第一個程式,同時也能讓高手溫故知新,如獲新歡。經過長時間的思索,最終定位為,內容高質量,描述簡潔,思路清晰,對讀者負責任的文章。初出茅廬,不會高手的底層功力,也不會段子手的套路人心,但,堅持做自己,儘自己所能,為人民服務。
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有一個感性的認知。
不知道看完本文,是否對您開文有益?
Star or 點贊本文 是一種鼓勵與支援哦,努力堅持寫出好文!
Demo的程式碼地址Github