前言
最近比較忙硝皂,兩三周沒有更新簡書了常挚,公司正好在做藍牙BLE的項目,本來覺得挺簡單的東西從網上找了個框架稽物,就咔咔地開始搞奄毡,搞完以后才發(fā)現(xiàn)里面還有不少坑呢,故而寫一篇藍牙BLE入門及爬坑指南贝或,旨在幫助剛入藍牙BLE的小伙伴們少走彎路吼过。
注:本文所有的具體代碼實現(xiàn)都在文章最后的github上
經典藍牙和藍牙BLE的區(qū)別
說起藍牙锐秦,大家一定聽過藍牙1.0 2.0 3.0 4.0,不過現(xiàn)在已經不再用版本號區(qū)分藍牙了盗忱,藍牙1.0~3.0都是經典藍牙酱床,在塞班系統(tǒng)就已經開始使用了,確實很經典趟佃。有些人一直認為藍牙4.0就是藍牙BLE扇谣,其實是錯誤的。因為4.0是雙模的揖闸,既包括經典藍牙又包括低能耗藍牙揍堕。經典藍牙和藍牙BLE雖然都是藍牙,但其實還是存在很大區(qū)別的汤纸。藍牙BLE相比于經典藍牙的優(yōu)點是搜索衩茸、連接的速度更快,關鍵就是BLE(Bluetooth Low Energy)低能耗贮泞,缺點呢就是傳輸的速度慢楞慈,傳輸的數據量也很小,每次只有20個字節(jié)啃擦。但是藍牙BLE因為其低能耗的優(yōu)點囊蓝,在智能穿戴設備和車載系統(tǒng)上的應用越來越廣泛,因此令蛉,藍牙BLE開發(fā)已經是我們Android開發(fā)不得不去掌握的一門技術了聚霜。
藍牙BLE的簡介
藍牙BLE是在Android4.3系統(tǒng)及以上引入的,但是僅作為中央設備珠叔,直到5.0以后才可以既作為中央設備又可以作為周邊設備蝎宇。也就是5.0系統(tǒng)以后,可以手機控制手機了祷安,不過絕大多數的場景手機還是作為中央設備去控制其他的周邊設備姥芥。Android BLE 使用的藍牙協(xié)議是 GATT 協(xié)議。關于這個GATT協(xié)議汇鞭,我就不詳細給大家介紹了颖低,放上個鏈接其弊,感興趣的可以看一下http://blog.chinaunix.net/uid-21411227-id-5750680.html
Service和Characteristic
Service是服務购桑,Characteristic是特征值扑庞。藍牙里面有多個Service,一個Service里面又包括多個Characteristic腕巡,具體的關系可以看圖圖中畫的比較少玄坦,實際上一個藍牙協(xié)議里面包含的Service和Characteristic是比較多的 ,這時候你可能會問,這么多的同名屬性用什么來區(qū)分呢煎楣?答案就是UUID豺总,每個Service或者Characteristic都有一個 128 bit 的UUID來標識。Service可以理解為一個功能集合择懂,而Characteristic比較重要喻喳,藍牙設備正是通過Characteristic來進行設備間的交互的(如讀、寫困曙、訂閱等操作)表伦。
小結
經典藍牙和藍牙BLE雖然都是藍牙,但是在連接和數據傳遞上還是存在很大的區(qū)別慷丽,而藍牙BLE依靠著其低能耗的特點蹦哼,逐漸在智能穿戴設備上占有一席之地。藍牙BLE基于GATT協(xié)議傳輸數據要糊,提供了Serivice和Characteristic進行設備之間的通訊纲熏。以上,就是藍牙BLE的基本概念锄俄,下面開始藍牙BLE的正式開發(fā)局劲!
藍牙BLE正確開發(fā)姿勢(本文重點)
第一步:聲明藍牙BLE權限
<!--聲明藍牙權限-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Android6.0系統(tǒng)以上開啟藍牙還需要定位權限,定位權限屬于危險權限奶赠,需要動態(tài)申請鱼填,筆者實現(xiàn)的方法是使用了RxPerssion動態(tài)庫。
/**
* 檢查權限
*/
private void checkPermissions() {
RxPermissions rxPermissions = new RxPermissions(MainActivity.this);
rxPermissions.request(android.Manifest.permission.ACCESS_FINE_LOCATION)
.subscribe(new io.reactivex.functions.Consumer<Boolean>() {
@Override
public void accept(Boolean aBoolean) throws Exception {
if (aBoolean) {
// 用戶已經同意該權限
scanDevice();
} else {
// 用戶拒絕了該權限毅戈,并且選中『不再詢問』
ToastUtils.showLong("用戶開啟權限后才能使用");
}
}
});
}
第二步:連接藍牙前需要初始化的工作
mBluetoothManager= (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
mBluetoothAdapter=mBluetoothManager.getAdapter();
if (mBluetoothAdapter==null||!mBluetoothAdapter.isEnabled()){
Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent,0);
}
拿到BluetoothManager苹丸,在通過BluetoothManager.getAdapter()拿到BluetoothAdapter,然后判斷一下藍牙是否打開苇经,沒打開的話Intent隱式調用打開系統(tǒng)開啟藍牙界面谈跛。
第三步:掃描設備
/**
* 開始掃描 10秒后自動停止
* */
private void scanDevice(){
tvSerBindStatus.setText("正在搜索");
isScaning=true;
pbSearchBle.setVisibility(View.VISIBLE);
mBluetoothAdapter.startLeScan(scanCallback);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//結束掃描
mBluetoothAdapter.stopLeScan(scanCallback);
runOnUiThread(new Runnable() {
@Override
public void run() {
isScaning=false;
pbSearchBle.setVisibility(View.GONE);
}
});
}
},10000);
}
藍牙掃描如果不停止,會持續(xù)掃描塑陵,很消耗資源,一般都是開啟10秒左右停止
BluetoothAdapter.LeScanCallback scanCallback=new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
Log.e(TAG, "run: scanning...");
if (!mDatas.contains(device)){
mDatas.add(device);
mRssis.add(rssi);
mAdapter.notifyDataSetChanged();
}
}
};
這里的scanCallback是上一段代碼里mBluetoothAdapter.startLeScan(scanCallback)里面的對象蜡励,其中onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord)里面的參數都很直觀令花,device是設備對象,rssi掃描到的設備強度凉倚,scanRecord是掃面記錄兼都,沒什么卵用。 掃描過的設備仍然會被再次掃描到稽寒,因此要加入設備列表之前可以判斷一下扮碧,如果已經加入過了就不必再次添加了。
看一下搜索的效果圖吧
第三步:連接設備
BluetoothDevice bluetoothDevice= mDatas.get(position);
//連接設備
tvSerBindStatus.setText("連接中");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,
true, gattCallback, TRANSPORT_LE);
} else {
mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,
true, gattCallback);
}
連接這里大家可能已經發(fā)現(xiàn)了,判斷了一下手機系統(tǒng)慎王,6.0及以上連接設備的方法是bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE)蚓土。這里就是我遇見的第一個大坑了,我的手機是8.0的系統(tǒng)使用
bluetoothDevice.connectGatt(MainActivity.this, true, gattCallback);總是連接失敗赖淤,提示status返回133蜀漆,用了各種方法都不行,后臺一查才發(fā)現(xiàn)6.0及以上系統(tǒng)的手機要使用bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE)咱旱,其中TRANSPORT_LE參數是設置傳輸層模式确丢。傳輸層模式有三種TRANSPORT_AUTO 、TRANSPORT_BREDR 和TRANSPORT_LE吐限。如果不傳默認TRANSPORT_AUTO鲜侥,6.0系統(tǒng)及以上需要使用TRANSPORT_LE這種傳輸模式,具體為啥诸典,我也不知道描函,我猜是因為Android6.0及以上系統(tǒng)重新定義了藍牙BLE的傳輸模式必須使用TRANSPORT_LE這種方式吧。bluetoothDevice.connectGatt()方法返回的對象BluetoothGatt搂赋,這個BluetoothGatt對象非常重要赘阀,甚至可以說是最重要的。一般都是單獨聲明成全局變量來使用的脑奠,因為我們設備的讀基公、寫和訂閱等操作都需要用到這個對象。
private BluetoothGattCallback gattCallback=new BluetoothGattCallback() {
/**
* 斷開或連接 狀態(tài)發(fā)生變化時調用
* */
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
Log.e(TAG,"onConnectionStateChange()");
if (status==BluetoothGatt.GATT_SUCCESS){
//連接成功
if (newState== BluetoothGatt.STATE_CONNECTED){
Log.e(TAG,"連接成功");
//發(fā)現(xiàn)服務
gatt.discoverServices();
}
}else{
//連接失敗
Log.e(TAG,"失敗=="+status);
mBluetoothGatt.close();
isConnecting=false;
}
}
/**
* 發(fā)現(xiàn)設備(真正建立連接)
* */
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//直到這里才是真正建立了可通信的連接
isConnecting=false;
Log.e(TAG,"onServicesDiscovered()---建立連接");
//獲取初始化服務和特征值
initServiceAndChara();
//訂閱通知
mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);
runOnUiThread(new Runnable() {
@Override
public void run() {
bleListView.setVisibility(View.GONE);
operaView.setVisibility(View.VISIBLE);
tvSerBindStatus.setText("已連接");
}
});
}
/**
* 讀操作的回調
* */
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.e(TAG,"onCharacteristicRead()");
}
/**
* 寫操作的回調
* */
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
Log.e(TAG,"onCharacteristicWrite() status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));
}
/**
* 接收到硬件返回的數據
* */
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.e(TAG,"onCharacteristicChanged()"+characteristic.getValue());
final byte[] data=characteristic.getValue();
runOnUiThread(new Runnable() {
@Override
public void run() {
addText(tvResponse,bytes2hex(data));
}
});
}
};
這一段是連接的回調 這里我只重寫了幾個比較重要的方法宋欺,每個方法都有具體的注釋轰豆,需要強調的是有些同學重復連接會報133連接失敗,這個調用一下mBluetoothGatt.close()就可以解決齿诞,還有要注意的就是回調里面的方法不要做耗時的操作酸休,也不要在回調方法里面更新UI,這樣有可能會阻塞線程祷杈。
第四步:發(fā)現(xiàn)服務
/**
* 發(fā)現(xiàn)設備(真正建立連接)
* */
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//直到這里才是真正建立了可通信的連接
isConnecting=false;
Log.e(TAG,"onServicesDiscovered()---建立連接");
//獲取初始化服務和特征值
initServiceAndChara();
//訂閱通知
mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);
runOnUiThread(new Runnable() {
@Override
public void run() {
bleListView.setVisibility(View.GONE);
operaView.setVisibility(View.VISIBLE);
tvSerBindStatus.setText("已連接");
}
});
}
直到這里才是建立了真正可通信的連接斑司,下一步就可以進行讀寫訂閱等操作,之前文章中有提到要通過Service和Characteristic特征值來操作但汞,但是如果獲取到對應的服務和特征值呢宿刮?一般硬件開發(fā)工程師會定義好UUID,通知到我們私蕾,這個時候我們只需要調用下面的方法就能拿到Service和Characteristic
//write_UUID_service和write_UUID_chara是硬件工程師告訴我們的
BluetoothGattService service=mBluetoothGatt.getService(write_UUID_service);
BluetoothGattCharacteristic charaWrite=service.getCharacteristic(write_UUID_chara);
當然也會比較坑爹的僵缺,就是硬件工程師居然不知道Service和Characteristic的UUID是啥(沒錯,我就遇見了)踩叭,這個時候也不要慌磕潮,因為我們可以通過Android拿得到對應UUID.
private void initServiceAndChara(){
List<BluetoothGattService> bluetoothGattServices= mBluetoothGatt.getServices();
for (BluetoothGattService bluetoothGattService:bluetoothGattServices){
List<BluetoothGattCharacteristic> characteristics=bluetoothGattService.getCharacteristics();
for (BluetoothGattCharacteristic characteristic:characteristics){
int charaProp = characteristic.getProperties();
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
read_UUID_chara=characteristic.getUuid();
read_UUID_service=bluetoothGattService.getUuid();
Log.e(TAG,"read_chara="+read_UUID_chara+"----read_service="+read_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
write_UUID_chara=characteristic.getUuid();
write_UUID_service=bluetoothGattService.getUuid();
Log.e(TAG,"write_chara="+write_UUID_chara+"----write_service="+write_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0) {
write_UUID_chara=characteristic.getUuid();
write_UUID_service=bluetoothGattService.getUuid();
Log.e(TAG,"write_chara="+write_UUID_chara+"----write_service="+write_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
notify_UUID_chara=characteristic.getUuid();
notify_UUID_service=bluetoothGattService.getUuid();
Log.e(TAG,"notify_chara="+notify_UUID_chara+"----notify_service="+notify_UUID_service);
}
if ((charaProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0) {
indicate_UUID_chara=characteristic.getUuid();
indicate_UUID_service=bluetoothGattService.getUuid();
Log.e(TAG,"indicate_chara="+indicate_UUID_chara+"----indicate_service="+indicate_UUID_service);
}
}
}
}
BluetoothGattCharacteristic.PROPERTY_READ:對應的就是讀取數據
BluetoothGattCharacteristic.PROPERTY_WRITE和BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE:都是寫入翠胰,區(qū)別就是據說PROPERTY_WRITE_NO_RESPONSE寫入效率更高,而NO_RESPONSE沒有響應自脯,我也沒弄懂這個響應指的是什么響應之景,我用PROPERTY_WRITE_NO_RESPONSE寫入,訂閱中依然得到了回應冤今,這里有知道的朋友可以告訴一下筆者闺兢。
PROPERTY_NOTIFY和PROPERTY_INDICATE:這里都是訂閱的方法,區(qū)別就是PROPERTY_INDICATE一定能接收到訂閱回調戏罢,一般用來接收一些比較重要的必須的回調屋谭,但是不能太頻繁;而PROPERTY_NOTIFY不一定能百分之百接收到回調龟糕,可以頻繁接收桐磁,這個一般也是使用得比較多的訂閱方式。
讀取數據
private void readData() {
BluetoothGattCharacteristic characteristic=mBluetoothGatt.getService(read_UUID_service)
.getCharacteristic(read_UUID_chara);
mBluetoothGatt.readCharacteristic(characteristic);
}
讀取數據用得比較少讲岁,我也就不重點介紹了我擂,一般我們都是先訂閱,再寫入缓艳,在訂閱中回調數據進行交互校摩。
寫入數據
private void writeData(){
BluetoothGattService service=mBluetoothGatt.getService(write_UUID_service);
BluetoothGattCharacteristic charaWrite=service.getCharacteristic(write_UUID_chara);
byte[] data=HexUtil.hexStringToBytes(hex);
if (data.length>20){//數據大于個字節(jié) 分批次寫入
Log.e(TAG, "writeData: length="+data.length);
int num=0;
if (data.length%20!=0){
num=data.length/20+1;
}else{
num=data.length/20;
}
for (int i=0;i<num;i++){
byte[] tempArr;
if (i==num-1){
tempArr=new byte[data.length-i*20];
System.arraycopy(data,i*20,tempArr,0,data.length-i*20);
}else{
tempArr=new byte[20];
System.arraycopy(data,i*20,tempArr,0,20);
}
charaWrite.setValue(tempArr);
mBluetoothGatt.writeCharacteristic(charaWrite);
}
}else{
charaWrite.setValue(data);
mBluetoothGatt.writeCharacteristic(charaWrite);
}
}
這里寫入數據需要說一下,首先拿到寫入的BluetoothGattService和BluetoothGattCharacteristic對象阶淘,把要寫入的內容轉成16進制的字節(jié)(藍牙BLE規(guī)定的數據格式)衙吩,然后要判斷一下字節(jié)大小,如果大于20個字節(jié)就要分批次寫入了溪窒,因為GATT協(xié)議規(guī)定藍牙BLE每次傳輸的有效字節(jié)不能超過20個坤塞,最后通過BluetoothGattCharacteristic.setValue(data); mBluetoothGatt.writeCharacteristic(BluetoothGattCharacteristic);就可以完成寫入了。寫入成功了會回調onCharacteristicWrite方法
/**
* 寫操作的回調
* */
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
Log.e(TAG,"onCharacteristicWrite() status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));
}
訂閱回調
//訂閱通知
mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);
注意一定要寫在寫入之前澈蚌,要不然就收不到寫入的數據摹芙,我一般都是在發(fā)現(xiàn)服務之后就訂閱。關于訂閱收不到這里宛瞄,需要注意一下浮禾,首先你寫入的和訂閱的Characteristic對象一定要屬于同一個Service對象,另外就是保證你寫入的數據沒問題份汗,否則就可能收不到訂閱回調伐厌。
這里在EditText雖然沒有顯示,但其實我直接點擊默認就輸入7B46363941373237323532443741397D 這一串數據裸影,實在懶得打了
總結
第一次打這么多字有點小累,總結這個地方就不多說了军熏,這里就說點注意事項轩猩,在進行藍牙操作的時候最好每次都延遲200ms再執(zhí)行,因為藍牙是線程安全的,當你同時執(zhí)行多次操作的時候會出現(xiàn)busy的情況導致執(zhí)行失敗均践,所以這里建議一般都執(zhí)行一步操作延時一會晤锹,這樣可以保證操作的成功率,另外就是如果大家入了門以后想要快速的開發(fā)的話彤委,建議網上找好輪子鞭铆,找一個好用的,可以先自己看看實現(xiàn)的源碼焦影,當然最好就是自己封裝一個车遂。
最后放上我的github地址:https://github.com/kaka10xiaobang/BlueToothBLE