您的位置 首页 java

Android 蓝牙 与 BLE 低功耗蓝牙的最佳实践

鉴于很多同学比较期望此类文章,本文我们好好的来聊聊低功耗蓝牙,相信有小部分同学正在从事的就是此行业了,这篇文章算是从零开始吧,带领你从新认识BLE,所以前面会有一些长篇大论,耐心读写去,收获满满。

一.蓝牙的工作原理

蓝牙的设备关系处于C/S架构,也就是说,一个是Client,一个是Server,两端连接成功之后将建立数据链,建立成功之后就能进行双向的数据或语音通讯,并且两端都是可以发起断链操作,也就是说只有在设备连接的时候才会有身份关系,之后双向的可操作性都是一样的,而搜索配对这个过程,我们可以理解为呼叫:主端发起呼叫首先是查找,然后根据蓝牙地址进行配对,配对成功后进行建链,蓝牙在手机上的应用,一般都是无线传输,但是应用最广泛的,还是基于蓝牙模块的串口通信。

二.蓝牙的基本概念

先来了解一下UUID,UUID被用于唯一标识的一个服务,主设备通过UUID访问从设备提供相同UUID的服务,从而建立C/S模式,再来了解一下基础的概念知识,一般我们使用的无外乎蓝牙3.0(传统蓝牙) 和 蓝牙4.0(低功耗蓝牙),他们之前的使用有一些共性也有一些区别的,所以我们虽然是以BLE为主,但是我们还是要先从传统蓝牙了解,这样也能更好的平移过去,我们先来了解传统蓝牙

三.蓝牙3.0的实践

我们就来基础的使用一下传统蓝牙,先了解传统蓝牙的两个关键类

  • BluetoothAdapter
  • BluetoothDevice

BluetoothAdapter位于源码中frameworks/base/core/java/android/bluetooth/目录下,作为本地适配器的存在,他有一系列的可操作本地功能的方法,我们再来看下BluetoothDevice,本地蓝牙设备,记录了机身相关蓝牙信息和同步蓝牙状态信息的一个类,通过实例,我们就可以使用了。

1.初始化

首先是添加权限,在传统蓝牙中,我们的权限需要添加如下两个:

< uses-permission android:name=”android.permission.BLUETOOTH” />

< uses-permission android:name=”android.permission.BLUETOOTH_ADMIN” />

然后就可以操作我们的蓝牙设备获取本地适配器来打开蓝牙了:

这里我们可以看到,我通过getDefaultAdapter获取到BluetoothAdapter对象,这里要判断是否为空,因为模拟器是获取不到对象的,我们看下他的实现就知道了:

这是一个单例的写法,我们通过ServiceManager.getService去获取与framework通讯的IBinder,如果能获取到,说明系统蓝牙层是运行状态的,我们就可以初始化BluetoothAdapter

并且返回了,否则会打印: Bluetooth binder is null

这里还要看下,我们可以通过两种方式来打开蓝牙,实际上是一种方式,另外一种知识Setting对外的接口而已,我们可以调用enable来打开,也可以通过disable来关闭蓝牙,这些方法包括适配器中的方法无外乎都是外包装了一层,最终的实现还是BluetoothManager通过IBluetoothManager接口实现的,这里还可以看到我们的第二种方法就是Intent跳转,这里可以传入一个requestCode来接收返回结果是否开启蓝牙。

2.已配对列表

到这里蓝牙是打开了,我们紧接着就可以实现搜索等功能并且展示蓝牙列表了,首先我们可以通过适配器去获取已配对的列表:

这里通过getBondedDevices获得一个Set集合,只要遍历就能得到已配对的列表,我们来看下这个方法的实现:

他首先判断了当前的状态是否是开启的,如果不是则返回一个空的集合,然后再判断mService是否为空,我们在getDefaultAdapter的源码中看到,mService实际上就是绑BluetoothManager的服务,所以如果调用getDefaultAdapter或者设备不支持蓝牙,则调用getBondedDevices获取为空。

这里用到了一个mServiceLock,也就是ReentrantReadWriteLock,这是一个 线程 锁,同一时间只能允许一个线程访问,确保binder的通信,后面会有很多地方用到这个锁的。

3.未配对列表

然后我们就可以开启搜索了:

同样的我们调用适配器的startDiscovery就可以开始搜索了,如果想停止则调用cancelDiscovery即可,他的源码没什么好看的,主要是他的注释,注视中说,当发现远程的设备的时候会通过广播ACTION_FOUND来通知,搜索完成后会通过ACTION_DISCOVERY_FINISHED来通知,所以我们监听这两个广播,当然,你现在还想知道ACTION_FOUND如何获取蓝牙名称和地址,那你可以看下这个广播的注释,上面写道:EXTRA_NAME 和 EXTRA_DEVICE。当然,我们看一下EXTRA_DEVICE的注释你就知道

这里实际上是传递了一个Devices的对象的,所以我们的代码应该这样写:

这段代码相当好理解,我们按照源码的注释一步步实现了如上的逻辑,这就是我们搜索的逻辑了,这里我们运行一下,记得要真机运行哦,看下效果如何:

4.可见性

当然,我们还需要设置一个可见性和配对,环顾BluetoothAdapter,找到了一个EXTRA_DISCOVERABLE_DURATION的Action,这就是我们要操作的可见性:

5.配对

传入的showTime是几秒就是可见几秒,接着还有我们的配对,配对比较麻烦,我们阅读了适配器的源码之后可以看到有一个createBond的方法是进行配对的方法通过此方法是可以进行配对的,我们来看下代码:

其实这段代码很简单,就是反射得到这个方法,这里传入一个蓝牙地址,就是我们需要配对或者取消配对的对象,我们通过getRemoteDevice是可以拿到的,执行就好了,源码中对于createBond的注释中有提到我们可以通过ACTION_BOND_STATE_CHANGED这个广播来监听配对状态,所以,来看看代码吧:

这是状态的回调,我们后面还需要细化,现在可以配对试试效果:

6.连接

可以看到系统的配对列表已经显示了,配对成功之后我们还需要进行连接,才可以进行数据交互,这也是比较重要的环节,在这里我先单独的配置各个线程的参数,到后面在用线程池去优化性能,好的,我们接着往下看,现在既然已经配对了,那么我们就需要连接了:

在这里可以看到,我通过反射拿到createRfcommSocket方法来建立连接,这里注意的就是有几个坑,首先是getBluetoothService() called with no BluetoothManagerCallback 这个错误,这是因为你要是根据网上的Demo来操作就会出现,因为是很老的版本根据UUID创建的连接,这是不可靠的,也就是createRfcommSocketToServiceRecord创建的Socket。第二个错误就是:read failed, socket might closed or timeout, read ret: -1,这里你肯定很奇怪,为什么我要创建两个子线程来连接,这就是为了解决这个问题,如果你在同一个子线程中连接,就会报这个错误。

到这里我们基本的搜索,配对,连接都已经OK了,那么接下来我们就要建立连接产生数据传输了。

7.数据传输

数据传输的话,我们需要用到createRfcommSocketToServiceRecord和listenUsingRfcommWithServiceRecord这两个方法,主要是为了建立服务端和客户端的数据传输,我们来看下怎么传输:

这一段代码你要仔细看清楚了,这里分别是客户端和服务端线程的创建,你可以会有疑问,你连接刚才不是用反射吗?是的,如果你只是连接可以反射,如果你想数据通信的话,还是通过createRfcommSocketToServiceRecord来创建socket吧,然后就是服务端了,获取流,并且通过Handler发送消息,到这里基本上就可以测试了,教大家测试的方法:

手机A和B分别安装此Apk,点击B界面上的Server按钮,然后点击A界面上列表中B手机的蓝牙名称,等待配对连接,连接成功后B手机会有Toast弹出。

当然,这样写其实是非常的繁琐的,我们可以优化一下代码,也就是我们的日常可扩展代码,你可以直接拿来用:

首先是我们的客户端线程:

在这段代码中,我们可以看到,他的构造方法传入了一个BluetoothDevice和Handler,这是用于建立通信以及返回数据所用的,并且在run中,我们实际上是再次开启了一个线程DataThread来处理他们之间的数据交互,也就是Input和Output,同时我传入了一个线程名称用来区分工作线程好对应的显示数据。

我们接着往下看,这里是服务端的线程,因为是服务端,所以我们只要基于本地适配器去建立socket即可,同样的我们用DataThread来处理数据,所以关键的核心部分还是在DataThread中处理的:

这里我们把传入的线程名称设置后在run中一直循环读取并且根据线程名的标识来通过Handler发送数据出去,最终得到的结果如下:

这里截的是客户端收到的Log,双方通信算是建立完整,通过此实例,你就可以完成一个蓝牙聊天室的项目了,到此,我们的传统蓝牙算是授课完成了,了解传统蓝牙有助于我们快速熟悉低功耗蓝牙,好的,我们现在来看低功耗蓝牙的知识和案例。

四.蓝牙4.0的实践

蓝牙4.0又称BLE,以下我就用BLE代替,我们在学习BLE的过程中需要明白一些基本的概念,这跟我们的传统蓝牙是有所区别的,所以我们先来了解一些概念吧。

1.GATT

通过BLE连接,读写属性类小数据的一种Profile通用规范,所有的BLE的Profile规范都是基于GATT。

2.ATT

ATT(Attribute Protocol)属性层是GATT和GAP的基础,它定义了BLE协议栈上层的数据结构和组织方式。具体就是在传输过程 中使用尽量少的数据。每个属性都有一个唯一的 UUID,属性将以 Characteristic s and services 的形式传输。

3.Characteristic

可以理解为一个数据模型,可以理解为一个实体。它包括一个 value 和 0 至多个对次 value 的描述 (Descriptor)。

4.Descriptor

对 Characteristic 的描述,例如范围、计量单位等

5.Service

Characteristic 的集合。例如一个 service 叫做“Heart Rate Monitor”,它可能包含多个 Characteristics,其中可能包含一个叫做“heart rate measurement”的 Characteristic。

6.角色

在我们传统的蓝牙中,我们有客户端和服务端,并且通过socket去通信,这有一点点类似于http,而在我们BLE当中实际上也是存在一定的关系的,那就是中心角色和外设角色,中心角色扫描外设角色,而外设角色给中心角色提供数据服务。

7.初始化

我们在使用BLE之前,我们需要先了解一下他的关键类的作用和含义:

  • BluetoothManager:蓝牙管理器,获取蓝牙适配器和其他操作方法
  • BluetoothGatt:通用协议
  • BluetoothGattCallback:通讯回调,重要
  • BluetoothGattCharacteristic:GATT最小单元数据
  • BluetoothGattService:BLE设备服务
  • BluetoothGattDescriptor:描述类
  • BluetoothLeScanner:扫描类

相比于传统蓝牙,BLE增加了很多类,我们在实践中来学习这些类的使用方法

对比传统的蓝牙,BLE还需要在清单文件中增加一条uses-feature属性来表明此为BLE应用,也就是说此应用只在支持BLE的设备上运行,这和我们等一下初始化蓝牙的时候有一条属性是对应的:

< uses-feature

android:name=”android.hardware.bluetooth_le”

android:required=”true” />

并且权限也需要增加定位:

< uses-permission android:name=”android.permission.ACCESS_COARSE_LOCATION” />

< uses-permission android:name=”android.permission.ACCESS_FINE_LOCATION” />

这样我们就可以使用BLE了,首先是初始化,和传统的蓝牙相比,初始化有了一定改变:

到此,我们就已经获取到了适配器和管理器,接着就开启我们的搜索之路。

8.搜索列表

我们直接看代码,这里代码多的原因主要还是版本判断吧,不过也有点多此一举了,现在的市场占有率来看,低于5.0的版本可以直接丢弃了,我们来看,在老版本上,我们可以使用mBluetoothAdapter.startLeScan的方式来搜索,并且可以传UUID数组来过滤搜索,而在新版本中我们就需要通过BluetoothLeScanner这个类来搜索了,并且过滤的话就要传入一个过滤器,启动之后有几个回调,这里要说一下,一个是onScanResult一个是onScanFailed,代表搜索成功和失败,而在onScanResult中有一个ScanResult就是我们获得的参数了,我们可以通过他获取我们想要的数据,运行可以看到:

这里获取到的蓝牙设备名称为空,这是因为有些厂商把它设置为空或者加密,需要配对后才能读取,这也是比较蛋疼的,然后就是rssi这个概念了,这是信号强度,在传统蓝牙也有这个概念的,只是我没有讲,Rssi和接收功率有关,单位是dBm,一般为负值,反应的是信号的衰减程度,理想状态下(无衰减),Rssi = 0dBm,实际情况是,即使蓝牙设备挨得非常近,Rssi也只有-50dBm的强度,在传输过程中,不可避免要损耗。

一般情况下,经典蓝牙强度

  • -50 ~ 0dBm 信号强
  • -70 ~-50dBm 信号中
  • <-70dBm 信号弱

低功耗蓝牙分四级

  • -60 ~ 0 4格
  • -70 ~ -60 3格
  • -80 ~ -70 2格
  • <-80 1格

9.连接

连接的话我们使用的是connectGatt这个方法:

可以看到在这里需要传入三个参数,这三个参数分别是上下文,是否重连,和BluetoothGattCallback回调,这个回调代表着我们的数据都会从这里回调,我们来看下,BluetoothGattCallback有很多歌回调方法,这里例举了几个,首先是onConnectionStateChange,我们判断连接上设备后应该主动开启discoverServices来搜索从设备的服务,服务在第五节有说,实际上还有很多的服务类型没有列举,我们再来看onCharacteristicChanged方法,并且现在我们从device中获取到对应的设备名,现在再来看下我们要发送的数据了;

10.发送

正如开头所讲,我们都是基于GATT来处理数据,发送的代码如下:

这段代码中,我们首先通过 mBluetoothGatt 根据服务UUID获取到对应的BluetoothGattService,然后再去根据Characteristic的UUID获取到BluetoothGattCharacteristic,之前讲过他包含多个描述,现在我们可以通过setCharacteristicNotification设置通知,这样Gatt的回调就能接收到,也就是说,我们调用writeCharacteristic获取readCharacteristic都是可以调用回调onCharacteristicWrite和onCharacteristicRead的,这里也要注意,BLE的传输数据大小最大为20字节,超过了可能会出现数据不全的现象,所以我们的协议也要做好一定的对应。

如果你并不知道服务的UUID,这里可以通过BluetoothGattCallback回调中的onServicesDiscovered可以获取:

11.字节转换

非验证的代码,因为没有拿到带有协议的板子,所以基本上也是纸上讨论,我们来看下获取到的数据之后的转换:

这里的bytes实际上是我模拟的数据,而真正的数据在getValue()里,不过有时候需要考虑这样一个事情,那就是超过了20个字节所导致的数据丢失了。

12.数据分包

当然,不过接收数据会超过,发送数据也是会超过的,所以一般协定好的数据都是,在发一条完整数据的时候,ble设备首先会把数据长度、类型等信息先放在数据头部,然后连同剩下的详细数据内容按照最大长度分割成一个个包依次广播发送出。

所以我在写入数据的时候进行处理,可以看下如下代码:

在代码中我们判断如果小于或者等于20则直接写入,但是超过我们用了一个temp中间件去裁剪,这里也只是考虑了不超过40的情况,尽量自己控制,所以直接写入前20的数据后直接write后我们回到onCharacteristicWrite回调中看如下代码:

在这里接收到第一次发包的通知后会睡眠50ms,然后发送第二包,如果是第二包,因为我们设置tempCmd2 =null之后,所以不会重复发送。

13.OTA

OTA这个功能有部分人还是需要的,主要还是和服务端确认好升级的流程,一般我们会有如下几个流程:

  • 1.断开蓝牙
  • 2.通知MCU准备升级
  • 3.上传文件
  • 4.等待升级升级
  • 5.等待重启成功

至此,升级完成,至于详细的升级过程,我可以告诉你,当你把BLE的连接都熟悉之后这个功能做起来问题就不大了。

五.BLE Libaray

不过,基本上也只是在模拟一些数据,并不能真正的跑起来,这里分享一个Github上star比较多的库:

  • 支持多设备连接管理;
  • 支持广播包解析;
  • 支持自定义扫描过滤条件;
  • 支持根据设备名称正则表达式过滤扫描设备;
  • 支持根据设备信号最小值过滤扫描设备;
  • 支持根据设备名称或 MAC 地址列表过滤扫描设备;
  • 支持根据设备 UUID 过滤扫描设备;
  • 支持根据指定设备名称或 MAC 地址搜索指定设备;
  • 支持连接设备失败重试;
  • 支持操作设备数据失败重试;
  • 支持绑定数据收发通道,同一种能力可绑定多个通道;
  • 支持注册和取消通知监听;
  • 支持配置最大连接数,超过最大连接数时会依据 Lru 算法自动断开最近最久未使用设备;
  • 支持配置扫描、连接和操作数据超时时间;
  • 支持配置连接和操作数据重试次数以及重试间隔时间。

只需要在app/build.gradle中添加:

implementation ‘com.vise.xiaoyaoyou:baseble:2.0.5’

即可使用,至于其使用的 Api 和接口,可以自行翻阅Github,不过我也相信通过本篇的讲解,你应该可以看明白其中的关系和调用方式

本篇为基础篇,还没有在实战中完全运行,下一篇我将结合BLE的开发板给大家带来更多试用和有趣的功能,好了本篇就到这里了,需要源码的同学请在我的星球【Hi Android】中下载。

有兴趣可以加入我的星球:Hi Android , 里面可都是我手撸的新鲜文章,高质量你值得拥有!

进入星球你可以做什么?

1.我的所有视频可以观看

2.发布提问贴可以得到满意的答案

3.可指定我写你感兴趣的技术文章

4.初学者可配套视频辅导

5.有机会线下交流聚会

欢迎加入Android Developer 交流群:417046685

文章来源:智云一二三科技

文章标题:Android 蓝牙 与 BLE 低功耗蓝牙的最佳实践

文章地址:https://www.zhihuclub.com/198926.shtml

关于作者: 智云科技

热门文章

网站地图