前言
最近一直在思考一个问题,如何写文章?即内容高质量又通俗易懂,让新手既明白其中蕴含的真理又能轻松跑起第一个程序,同时也能让高手温故知新,如获新欢。经过长时间的思索,最终定位为,内容高质量,描述简洁,思路清晰,对读者负责任的文章。初出茅庐,不会高手的底层功力,也不会段子手的套路人心,但,坚持做自己,尽自己所能,为人民服务。
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具有蓝牙访问权限和发现周边蓝牙权限。
//使用蓝牙需要该权限
"android.permission.BLUETOOTH"/>
//使用扫描和设置需要权限
"android.permission.BLUETOOTH_ADMIN"/>
//Android 6.0以上声明一下两个权限之一即可。声明位置权限,不然扫描或者发现蓝牙功能用不了哦
"android.permission.ACCESS_FINE_LOCATION"/>
"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 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