文章目录

* 前言 <https://blog.csdn.net/langlitaojin/article/details/88360521#_1>
* 传统蓝牙 VS Ble蓝牙
<https://blog.csdn.net/langlitaojin/article/details/88360521#__VS__Ble_6>
* cosplay 角色扮演
<https://blog.csdn.net/langlitaojin/article/details/88360521#cosplay__9>
* 一些基本概念 <https://blog.csdn.net/langlitaojin/article/details/88360521#_12>
* Ble开发的几个步骤
<https://blog.csdn.net/langlitaojin/article/details/88360521#Ble_30>
* 准备 <https://blog.csdn.net/langlitaojin/article/details/88360521#_31>
* 第一步 配置清单文件
<https://blog.csdn.net/langlitaojin/article/details/88360521#___41>
* 第二步 检查设备,获取BluetoothAdapter
<https://blog.csdn.net/langlitaojin/article/details/88360521#__BluetoothAdapter_49>
* 第三步 注册广播,开启蓝牙
<https://blog.csdn.net/langlitaojin/article/details/88360521#___66>
* 第四步 扫描指定类型的设备
<https://blog.csdn.net/langlitaojin/article/details/88360521#___114>
* 第五步 连接设备,获取特征值
<https://blog.csdn.net/langlitaojin/article/details/88360521#___154>
* 容易碰到的一些错误和问题
<https://blog.csdn.net/langlitaojin/article/details/88360521#_296>
* 总结 <https://blog.csdn.net/langlitaojin/article/details/88360521#_309>


<>前言

   
以前一直忙于不断的做项目,总是觉得没有必要抽时间出来写博客,但看到业界的很多大牛都有坚持写博客的习惯,自己也应该向大佬们看齐才有机会成为大牛。其实坚持写博客的好处还是不少的,一方可以作为自己对所学知识的一个总结归纳,另外一个方面可以作为和其他人的一个技术分享交流,好处多多。但万事开头难,希望自己能坚持下来,keep
moving!

    之前也做过几个Ble的项目,现在来对这一块做一个简单的介绍和总结。

<>传统蓝牙 VS Ble蓝牙

    在Android中蓝牙根据协议主要分为两种:一种是基于spp协议的传统蓝牙或者叫经典蓝牙,另外一种就是Android 4.3
才引进的基于GATT协议的BLE(Bluetooth Low Energy)低功耗蓝牙,相比较于传统蓝牙其主要优点就是功耗低,搜索、连接速度相对较快
,那是不是说相较于传统蓝牙它就没有缺点呢?答案是否定的,相较于传统蓝牙而言,Ble的数据传输效率低,每次可以传输的数据量小得多,单次最多只能传输20个byte
,而传统蓝牙单次可传输的数据量远不止于此,但也正是由于功耗低这一主要优点,使得现在ble被广泛应用于智能穿戴设备、智能锁、心率测量仪等领域
(本文主要介绍的是ble蓝牙,若要了解spp协议蓝牙的相关开发,可以查看我我另一篇博客蓝牙开发(二)----- 基于SPP蓝牙协议的Android应用开发
<https://blog.csdn.net/langlitaojin/article/details/88578869>)。

<>cosplay 角色扮演

    在BLE协议中,有两个角色,周边(Periphery)和中央(Central)。周边是数据的提供者,中央是数据的使用和处理者。在Android
SDK里面,Android4.3以后手机只能作为中央设备使用,直到Android5.0以后手机才可以作为周边设备使用,即此时的手机既可以作为BLE周边设备来为中央提供数据,也可以作为中央设备接收处理周边其它蓝牙设备传递过来的数据进行处理。
一个中央设备可以同时连接多个周边设备,但一个周边设备某一时刻只能连接一个中央设备。

<>一些基本概念

* GATT协议
    我们已经知道了Ble是基于GATT协议的,那么什么是GATT协议呢?下面就来简单介绍一下:
    GATT是Generic Attributes的缩写,也就是通用属性的意思,它是ble低功耗蓝牙设备之间的标准通信协议。根据蓝牙技术官网
<https://www.bluetooth.com/specifications/gatt/generic-attributes-overview>
的介绍,GATT协议定义了一个分层数据结构,该结构暴露给连接的Ble设备进行通信。其数据分层结构如下图所示:
GATT协议数据结构图
   
简单描述一下结构图的意思就是:Profile是一组服务(Service)的集合,这个文件包含广播的种类、所使用的连接间隔、所需的安全等级等配置信息,每个Service下面可以包含多个特征(characteristic),每个特征包含属性(properties)和值(value),还可以包含多个描述(descriptor)。
*
Service

服务是特征和与其它服​​务关系的集合。每个Service包含一个或多个characteristic,每个Service由一个唯一的UUID标识,UUID可以分为两种:一种是经过
官方 <https://www.bluetooth.com/specifications/gatt/services>
认证的16位UUID,另外一种是由开发者自己定义的128位的UUID,类似于0x0000xxxx-0000-1000-8000-00805F9B34FB,这是蓝牙技术联盟定义的一个基本格式,蓝牙模块的开发者通常只要定义xxxx的部分即可,开发时通常由硬件或嵌入式工程师会告诉我们该模块包含哪些Service及其对应的UUID。

*
Characteristic

特征被定义为包含单个值的属性类型。和Service一样,Characteristic也是由一个UUID进行标识,通常也是由硬件或嵌入式工程师提供。Characteristic是我们进行数据通信的一个重要载体,我们的数据读、写、通知等都通过这个类来实现,是比较重要的一个类。

   
这里就只介绍Service和Characteristic这两个比较重要的数据结构,Characteristic中的属性和描述等就不一一介绍了,有兴趣的可以去查阅相关资料。



<>Ble开发的几个步骤
<>准备
从硬件或嵌入式工程师手上拿到需要的UUID:
public static final UUID UUID_SERVICE =
UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb"); //主Service的UUID public
static final UUID UUID_NOTIFY =
UUID.fromString("0000fff3-0000-1000-8000-00805f9b34fb"); //具有通知属性的UUID public
static final UUID UUID_READ =
UUID.fromString("0000fff5-0000-1000-8000-00805f9b34fb"); //具有读取属性的UUID public
static final UUID UUID_WRITE =
UUID.fromString("0000fff7-0000-1000-8000-00805f9b34fb"); //具有写入属性的UUID <>第一步
配置清单文件 <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!--
Android6.0及以上必须获取位置权限,否则无法扫描到周边的蓝牙设备 --> <uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION" /> <>第二步
检查设备,获取BluetoothAdapter
进行这步之前记得先进行关于定位权限的动态适配
/** * 判断该设备是否支持Ble并获取BluetoothAdapter */ public Boolean ensureBLEExists() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE))
{ return false; } //获取BluetoothAdapter BluetoothManager bm = (BluetoothManager)
getSystemService(Context.BLUETOOTH_SERVICE); if (bm!=null) mBluetoothAdapter =
bm.getAdapter(); return true; } <>第三步 注册广播,开启蓝牙 /** * 注册蓝牙状态改变的监听广播 */
private void registerBlueToothReceiver(){ if
(mBluetoothStateReceiver==null)mBluetoothStateReceiver=new
BluetoothEnableStateReceiver(); IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mBluetoothStateReceiver,filter); }
//蓝牙开关状态的广播接收者,可以通过设置接口回调进行监听, //以方便在蓝牙状态变化的时候做出相应操作或提示 public class
BluetoothEnableStateReceiver extends BroadcastReceiver { @Override public void
onReceive(Context context, Intent intent) { String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { int state =
intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0); Log.i(TAG,
"BluetoothOnOffStateReceiver: state: " + state); if(state ==
BluetoothAdapter.STATE_ON) { //蓝牙打开 } else if(state ==
BluetoothAdapter.STATE_TURNING_OFF){ //蓝牙正在关闭 } else if(state ==
BluetoothAdapter.STATE_OFF){ //蓝牙已关闭 } } } } /** * 开启蓝牙 */ public void
enableBluetooth() { if (mBluetoothAdapter!=null){ if
(!mBluetoothAdapter.isEnabled()) { //蓝牙未开启,通过隐式意图请求开启蓝牙 Intent enableBtIntent =
new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, 0); } } } <>第四步 扫描指定类型的设备
用户允许开启蓝牙之后接下来就可以扫描周边的设备了
public BluetoothAdapter.LeScanCallback mScanCallback = new
BluetoothAdapter.LeScanCallback() { @Override public void
onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
//device是设备对象,rssi是信号强度,scanRecord是扫描记录 if (device != null) { //接口回调扫描到的设备
synchronized (mCallBacks){ for (BleAdapterCallBack callBack : mCallBacks) {
callBack.onDeviceFound(device, rssi); } } } }; /** * 开始扫描 10秒后自动停止 * */ private
void startScan(){ UUID[] uuid = {UUID_SERVICE }; if(mIsScanning){
//如果当前正在扫描则先停止扫描 mBluetoothAdapter.stopLeScan(mScanCallback); }
//mBluetoothAdapter.startLeScan(scanCallback);//不进行特定设备过滤,扫描所有设备
//进行特定uuid过滤,只扫描具有指定Service UUID的设备 mBluetoothAdapter.startLeScan(uuid,
mScanCallback); new Handler().postDelayed(new Runnable() { @Override public
void run() { //结束扫描 mBluetoothAdapter.stopLeScan(scanCallback); } },10000); }
    蓝牙扫描是比较耗费资源的,如果扫描频率比较高或者时间比较长,在性能差一点手机上会出现电量消耗比较大和发热比较严重的情况,所以要设置适当的扫描时间。
    另外,我们可以调用mBluetoothAdapter.startLeScan(uuid, mScanCallback),扫描具有指定Service
UUID的设备,也可以调用mBluetoothAdapter.startLeScan(scanCallback),扫描所有的蓝牙设备,可以根据不同的方法自行选择。
<>第五步 连接设备,获取特征值 BluetoothGattCallback gattCallback = new
BluetoothGattCallback() { //连接状态变化的回调 @Override public void
onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState); Log.i(TAG,
"连接状态:status:" + status + ",newState:" + newState) if (status ==
BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED)
{ //连接成功,调用发现服务的方法 gatt.discoverServices();
mHandler.sendEmptyMessage(STATE_CONNECTED); } else if (newState ==
BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, "断开连接");
mHandler.sendEmptyMessage(STATE_DISCONNECT); gatt.disconnect(); gatt.close(); }
} else { Log.i(TAG, "连接失败"); gatt.close(); } } //发现Service的回调 @Override public
void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status); if (status ==
BluetoothGatt.GATT_SUCCESS) { //if(D) Log.i(TAG, "onServicesDiscovered
success."); mBluetoothGatt = gatt; BluetoothGattService service =
gatt.getService(UUID_SERVICE); if (service == null) { close(); return; }
//获取对应的特征值 mReadCharacteristic = service.getCharacteristic(UUID_READ);
mWriteCharacteristic = service.getCharacteristic(UUID_WRITE);
mNotifyCharacteristic = service.getCharacteristic(UUID_NOTIFY);
//开启mNotifyCharacteristic特征的通知
gatt.setCharacteristicNotification(mNotifyCharacteristic, true); } } //读操作回调
@Override public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status); } //写操作的回调 @Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status); //
Log.i(TAG,"onCharacteristicWrite:"+status); if
(characteristic.getUuid().equals(UUID_WRITE)) { if (status ==
BluetoothGatt.GATT_SUCCESS) { synchronized (mCallBacks){ for
(BleAdapterCallBack callBack : mCallBacks) { callBack.onDataSendOk(true); } } }
else { synchronized (mCallBacks){ for (BleAdapterCallBack callBack :
mCallBacks) { callBack.onDataSendOk(false); } } } } } //接收到连接设备传送过来的数据的回调
@Override public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic); byte[] data =
characteristic.getValue(); //取出接收到的数据 if
(characteristic.getUuid().equals(UUID_NOTIFY)) { Message message =
Message.obtain(); message.obj=data; message.what=RECEIVE_DATA;
mHandler.sendMessage(message); } } } //连接设备 public void connect(BluetoothDevice
device) if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { mBluetoothGatt =
device.connectGatt(MainActivity.this,false, gattCallback,
BluetoothDevice.TRANSPORT_LE); } else { mBluetoothGatt =
device.connectGatt(MainActivity.this,false, gattCallback); } } /** * 发送数据 *
@param data */ protected void writeData(byte[] data) { if
(mWriteCharacteristic!=null){ mWriteCharacteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(mWriteCharacteristic); } } /** *
从远程设备读取请求的特征(比较少用) * @param data */ private boolean readData() { return
mBluetoothGatt.readCharacteristic(mReadCharacteristic); }
在这里有几个需要注意的问题:

首先,在连接成功之后记得调用gatt.discoverServices(),否则不会回调onServicesDiscovered(),则无法建立连接;其次,在进行连接时系统提供了两个不同的方法:
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) { return (connectGatt(context, autoConnect,
callback, TRANSPORT_AUTO)); } public BluetoothGatt connectGatt(Context context,
boolean autoConnect, BluetoothGattCallback callback, int transport) { return
(connectGatt(context, autoConnect, callback, transport, PHY_LE_1M_MASK)); }
connectGatt(Context context, boolean autoConnect,BluetoothGattCallback
callback, int
transport)这个方法在6.0之前的隐藏的,6.0才开始开放,两个方法只差一个transport参数,这个参数主要用来设置连接模式,对于这个参数系统提供了三个常量:

BluetoothDevice.TRANSPORT_AUTO:对于GATT连接到远程双模设备无物理传输优先。
BluetoothDevice.TRANSPORT_BREDR:GATT连接到远程双模设备优先BR/EDR。
BluetoothDevice.TRANSPORT_LE:GATT连接到远程双模设备优先BLE。

我们这是Ble设备,所以我们选择BluetoothDevice.TRANSPORT_LE模式。

<>容易碰到的一些错误和问题

* 连接不成功,Log提示“BluetoothGatt status 133”
问题:连接总是不成功,onConnectionStateChange()方法中的status 为133。

   
出现这个问题的主要的主要原因之一是在连接失败或者连接断开后没有调用gatt.close()进行资源的释放,所以要注意gatt资源的及时释放;另外在6.0及以上的设备连接时记得使用可以设置连接模式的方法,可以大概率降低这个133问题出现的概率。

* 单条数据大小超出20个byte
   
虽然在制定协议的时候会在保证安全性的前提下尽量简洁,但是有时候可能会有那么几条通信指令的数据量超出20个byte,毕竟包序号、校验值等都要占用几个字节,这种时候这么办呢?
   
碰到这种情况我们就必须对数据进行分包传送接收:可以和制定协议的人约定每条数据的开头标志、分包数量、结束标志等,这样就可以很清楚的知道当前是不是第一条数据以及后面还有没有数据等。

<>总结

我们再来回顾一下主要流程:

   
获取BluetoothAdapter->开启蓝牙->扫描指定类型设备->设备连接->获取gatt和相关Characteristic,基本的流程就是这些,大家可以根据自己的需求将整个流程封装到一个工具类中,方便使用。

文章末尾给大家推荐一个不错的蓝牙调试工具nRF Connect
<https://github.com/NordicSemiconductor/Android-nRF-Connect>
,该工具可以很方便的查看到连接设备的详细信息,包括Service信息、各个Characteristic的信息和属性等等。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信