原文地址java
本文旨在提供一個方便沒接觸過Android上低功耗藍牙(Bluetooth Low Energy)的同窗快速上手使用的簡易教程,所以對其中的一些細節不作過度深刻的探討,此外,爲了讓沒有Ble設備的同窗也能模擬與設備的交互過程,本文還提供了中央設備(central)和外圍設備(peripheral)的示例代碼,只需2部手機你們就能夠愉快的「左右互搏」了。android
上面咱們提到了中央設備(central)和外圍設備(peripheral),在這裏咱們能夠這樣簡單的理解:git
注:Android從4.3(API Level 18) 開始支持低功耗藍牙,可是剛開始只支持做爲中央設備(central)模式,從 Android 5.0(API Level 21) 開始才支持做爲外圍設備(peripheral)的模式,所以咱們最好使用Android 5.0以上版本的手機進行下面的操做。github
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
//使用ble掃描時還須要咱們到’設置 > 安全性和位置信息 > 位置信息‘處打開位置信息,
//不然將會搜索不到周圍的設備
複製代碼
可能有人會問爲何使用低功耗藍牙還須要位置權限?簡單來講就是藍牙也有定位的功能。數組
接下來咱們就準備開始實際操做了,首先咱們準備2臺手機,手機A做爲中央設備,手機B做爲外圍設備,在打開B手機的ble廣播後,咱們使用A手機進行打開藍牙-->掃描-->鏈接-->獲取服務,特徵-->打開通知-->寫特徵-->讀特徵-->斷開鏈接,經過這些步驟咱們就能學會Android Ble 的基本方法的使用。安全
從掃描開始,接下來的這些操做中你可能會遇到各類奇奇怪怪的問題,爲了減小你們踩坑的機率,我會在後面的操做中分享一些可能會遇到的問題和解決方法,有的問題在官方文檔中可能有提到,有的在一些論壇帖子中有說起,還有的一些就是本身的經驗之談。app
打開藍牙有如下兩種方式:ide
//方法一
BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
if (mBluetoothAdapter != null){
mBluetoothAdapter.enable();
}
複製代碼
//方法二
BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = bluetoothManager.getAdapter();
if (!mBluetoothAdapter.isEnabled() && !mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(
BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
複製代碼
掃描是一個很是耗電的操做,所以當咱們找到咱們須要的設備後應該立刻中止掃描。官方提供了2個掃描的方法:ui
//舊API
//啓動掃描
private void scan(){
BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothManager.getAdapter().startLeScan(mLeScanCallback);
//若是想要指定搜索設備,可使用下面這個構造方法,傳入外圍設備廣播出的服務的UUID數組
UUID[] uuids=new UUID[]{UUID_ADV_SERVER};
bluetoothManager.getAdapter().startLeScan(uuids,mLeScanCallback);
}
//中止掃描
private void stopScan(){
BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothManager.getAdapter().stopLeScan(mLeScanCallback);
}
//掃描結果回調
LeScanCallback mLeScanCallback = new LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
//device:掃描到的藍牙設備對象
//rssi:掃描到的設備的信號強度,這是一個負值,值越大表明信號強度越大
//scanRecord:掃描到的設備廣播的數據,包含設備名,服務UUID等
}
};
複製代碼
↑ 這是個在Android 5.0時被標註deprecated的API,該方法目前仍能使用。因爲onLeScan中回調出的設備的廣播數據須要本身手動解析,這是個比較麻煩的過程。google
在新的API中已經封裝了方法來解析廣播數據,若是爲了適配性使用這個舊的掃描方法,同時又但願解析獲得廣播中的數據,咱們可使用源碼中新API使用的解析方法(須要稍許修改,直接使用會報錯),或者使用我本身修改過的方法,若是你想了解更多關於廣播數據的解析能夠看Core Specifications 5.0中Volume 3, Part C, Section 11這一節。
//新API,須要Android 5.0(API Level 21)及以上版本才能使用
//啓動掃描
private void scanNew() {
BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
//基本的掃描方法
bluetoothManager
.getAdapter()
.getBluetoothLeScanner()
.startScan(mScanCallback);
//設置一些掃描參數
ScanSettings settings=new ScanSettings
.Builder()
//例如這裏設置的低延遲模式,也就是更快的掃描到周圍設備,相應耗電也更厲害
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
//你須要設置的過濾條件,不僅能夠像舊API中的按服務UUID過濾
//還能夠按設備名稱,MAC地址等條件過濾
List<ScanFilter> scanFilters=new ArrayList<>();
//若是你須要過濾掃描到的設備能夠用下面的這種構造方法
bluetoothManager
.getAdapter()
.getBluetoothLeScanner()
.startScan(scanFilters,settings,mScanCallback);
}
//掃描結果回調
ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
//callbackType:掃描模式
//result:掃描到的設備數據,包含藍牙設備對象,解析完成的廣播數據等
}
};
//中止掃描
private void stopNewScan(){
BluetoothManager bluetoothManager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothManager.getAdapter().getBluetoothLeScanner().stopScan(mScanCallback);
}
複製代碼
相比舊API,新API的功能更全面,可是須要Android 5.0以上才能使用,究竟須要使用哪一種方法,你們能夠根據本身的實際狀況選擇。
注意坑來了:
1.若是搜索不到設備,請檢查對於Android 6.0及以上版本ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION權限是否已經動態授予,同時檢查位置信息(也就是GPS)是否已經打開,通常來講搜不到設備就是這兩個緣由。
2.不論是新舊API的掃描結果回調都是不停的回調掃描到的設備,就算是相同的設備也會重複回調,直到你中止掃描,所以最好不要在回調方法中作過多的耗時操做,不然可能會出現這個問題,若是須要處理回調的數據能夠把數據放到另一個線程處理,讓回調儘快返回。
同一時間咱們只能對一個外圍設備發起鏈接,若是須要對多個設備鏈接能夠等上一個鏈接成功後再進行下一個鏈接,不然若是前面的某個鏈接操做失敗了沒有回調,後面的操做會被一直阻塞。
//發起鏈接
private void connect(BluetoothDevice device){
mBluetoothGatt = device.connectGatt(context, false, mBluetoothGattCallback);
}
//Gatt操做回調,此回調很重要,後面全部的操做結果都會在此方法中回調
BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//newState:當前鏈接處於的狀態,例如鏈接成功,斷開鏈接等
//當鏈接狀態改變時觸發此回調
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//成功獲取服務時觸發此回調,「獲取服務,特徵」一節會介紹
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//characteristic:被讀的特徵
//當對特徵的讀操做完成時觸發此回調,「讀特徵」一節會介紹
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//characteristic:被寫的特徵
//當對特徵的寫操做完成時觸發此回調,「寫特徵」一節會介紹
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//characteristic:特徵值改變的特徵
//當特徵值改變時觸發此回調,「打開通知」一節會介紹
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//descriptor:被讀的descriptor
//當對descriptor的讀操做完成時觸發
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//gatt:GATT客戶端
//status:這次操做的狀態碼,返回0時表明操做成功,返回其餘值就是各類異常
//descriptor:被寫的descriptor
//當對descriptor的寫操做完成時觸發,「打開通知」一節會介紹
}
};
複製代碼
當咱們調用connectGatt方法後會觸發onConnectionStateChange這個回調,回調中的status咱們用來判斷此次操做的成功與否,newState用來判斷當前的鏈接狀態。
注意坑來了:
當咱們鏈接成功後,GATT客戶端(手機A)能夠經過發現方法檢索GATT服務端(手機B)的服務和特徵,以便後面操做使用。
//鏈接成功後掉用發現服務
gatt.discoverServices();
//當服務檢索完成後會回調該方法,檢索完成後咱們就能夠拿到須要的服務和特徵
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//獲取特定UUID的服務
BluetoothGattService service = gatt.getService(UUID_SERVER);
//獲取全部服務
List<BluetoothGattService> services = gatt.getServices();
if (service!=null){
//獲取該服務下特定UUID的特徵
mCharacteristic = service.getCharacteristic(UUID_CHARWRITE);
//獲取該服務下全部特徵
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
}
}
複製代碼
打開通知官方的標準作法分兩步:
//官方文檔作法
private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
//第一步,開啓手機A(本地)對這個特徵的通知
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
//第二步,經過對手機B(遠程)中須要開啓通知的那個特徵的CCCD寫入開啓通知命令,來打開通知
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
複製代碼
因爲Android7.0之前版本存在一個bug:對descriptor的寫操做會複用父特徵的寫入類型,這個bug在7.0以後進行了修復,爲了提升兼容性,咱們能夠對官方作法稍許修改:
private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
//第一步,開啓手機A(本地)對這個特徵的通知
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
//第二步,經過對手機B(遠程)中須要開啓通知的那個特徵的CCCD寫入開啓通知命令,來打開通知
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
//獲取特徵的寫入類型,用於後面還原
int parentWriteType = characteristic.getWriteType();
//設置特徵的寫入類型爲默認類型
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
//還原特徵的寫入類型
characteristic.setWriteType(parentWriteType);
複製代碼
接下來咱們來看看回調
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
//當手機B的通知發過來的時候會觸發這個回調
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//第二步會觸發此回調
}
複製代碼
注意:
//默認的寫入類型,須要外圍設備響應
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
//無需設備響應的寫入類型
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
mCharacteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(mCharacteristic);
//寫入特徵回調
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
}
複製代碼
寫特徵的用法和前面打開通知中的寫descriptor相似。
注意:
若是使用WRITE_TYPE_DEFAULT這種類型寫入,而外圍設備沒有迴應,那後面的操做都會被阻塞。所以,使用哪一種方式須要你們根據本身的外圍設備決定,你們能夠嘗試把示例工程中的這一行註釋掉而後在來寫入數據,結合日誌看看會能更好的理解。
//讀特徵
mBluetoothGatt.readCharacteristic(mCharacteristic);
//讀特徵的回調
@Override
public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
}
複製代碼
讀特徵這個操做沒多少坑,只是須要前面提到的成功回調之後纔算執行完成
private void disConnect(){
if (mBluetoothGatt!=null){
//斷開鏈接
mBluetoothGatt.disconnect();
// mBluetoothGatt.close();
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState==BluetoothProfile.STATE_DISCONNECTED){
//關閉GATT客戶端
gatt.close();
}
}
複製代碼
注意:
其實這篇文章除了給你們列舉了一些使用的API和可能遇到的問題外,最主要是要強調一個藍牙操做的節奏,也就是一個任務完成下一個任務才能開始的原則,爲了便於你們入門,上面這些使用簡化了不少須要考慮的邏輯,例如:讀、寫、通知一直沒回調怎麼辦?(能夠給這些操做都加上超時時間)等等,不過若是你們按照本文提供的方法使用就已經能避開不少可能會遇到的奇怪問題了。
若是你們須要瞭解更多更詳細的使用方法,這裏給你們推薦2個開源的ble庫: