1:经典蓝牙开发
1.1:经典蓝牙简介
Android 平台包含蓝牙网络堆栈支持,凭借此支持,设备能以无线方式与其他蓝牙设备交换数据。应用框架提供了通过 Android Bluetooth API 访问蓝牙功能的途径。使用 Bluetooth API Android 应用可以执行下面的操作:
- 扫描其他蓝牙设备
- 查询本地蓝牙适配器的配对蓝牙设备
- 建立 RFCOMM 通道
- 通过服务发现连接到其他设备
- 与其他设备进行双向数据传输
- 管理多个连接
传统蓝牙适用于电池使用强度较大的操作,例如 Android 设备之间的流传输和通信等。针对具有低功耗要求的蓝牙设备,Android 4.3(API 18)中引入了面向低功耗蓝牙的 API 支持。
1.2:基础知识
使用 Android Bluetooth API 来完成使用蓝牙进行通信的四项主要任务:
设置蓝牙、查找局部区域内的配对设备或可用设备、连接设备、以及在设备之间传输数据。
关于蓝牙的 API 在 android.bluetooth
包中,下面介绍一下和蓝牙相关的主要类
BluetoothAdapter
本地蓝牙适配器,是所有蓝牙交互的入口点,表示蓝牙设备自身的一个蓝牙设备适配器,整个系统只有一个蓝牙适配器。通过它可以发现其他蓝牙设备,查询绑定(配对)设备的列表,使用已知的 Mac 地址实例化 BluetoothDevice
以及创建 BluetoothServerSocket
用来侦听来自其他设备的通信。
BluetoothDevice
表示远程的蓝牙设备。利用它可以通过 BluetoothSocket
请求与某个远程设备建立连接,或查询有关该设备的信息,例如设备的名称、地址、类和绑定状态等。
BluetoothSocket
表示蓝牙套接字接口(与 TCP Socket 相似)。这是允许应用通过 InputStream
和 OutputStream
与其他蓝牙设备交换数据的节点。正是利用这个对象来完成蓝牙设备间的数据交换,
BluetoothServerSocket
表示用于侦听传入请求的开发服务器套接字(类似于 TCP ServerSocket)要连接两台 Android 设备,其中一台设备必须使用此类开发的一个服务器套接字。当一台远程蓝牙设备向此设备发出连接请求时,BluetoothServerSocket
将会在接受连接后返回已连接的 BluethoothSocket
。
BluetoothClass
描述蓝牙设备的一般特性和功能。这是一组只读属性,用于定义设备的主要和次要设备类及其服务。不过,它不能可靠地描述设备支持的所有蓝牙配置文件和服务,而是适合作为设备类型提示。
BluetoothProfile
表示蓝牙配置文件的接口。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。免提配置文件便是一个示例。如需了解关于配置文件的详细讨论,参考下面配置文件的讲解
BluetoothHeadset
提供蓝牙耳机支持,以便与手机配合使用。其中包括蓝牙耳机和免提(1.5版)配置文件。BluetoothProfile 的实现类
BlutoothA2dp
定义高质量音频如何通过蓝牙连接和流式传输,从一台设备传输到另一台设备。“A2DP”代表高级音频分发配置文件。是 BluetoothProfile 的实现类
BluetoothHealth
表示用于控制蓝牙服务的健康设备配置文件代理。 BluetoothProfile 的实现类。
BluetoothGatt
BluetoothProfile 的实现类。与低功耗蓝牙通信有关的配置文件代理
BluetoothHealthCallback
用于实现 BluetoothHealth 回调的抽象类。必须扩展此类并实现回调方法,以接收关于应用注册状态和蓝牙通道状态变化的更新内容。
BluetoothHealthAppConfiguration
表示第三方蓝牙健康应用注册的应用配置,以便与远程蓝牙健康设备通信
BluetoothProfile.ServiceListener
在 BluetoothProfile IPC 客户端连接到服务(即,运行特定配置文件的内部服务)或断开服务连接时向其发送通知的接口。
1.3:经典蓝牙模块
1.3.1:蓝牙权限
使用蓝牙必须声明权限 BLUETOOTH 才可以执行蓝牙通信。:
<mainifest>
<uses-permission android:name = "android.permission.BLUETOOTH"/>
<!--启用应用启动设备发现或者操作蓝牙设备的超级管理员-->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
</mainifest>
1.3.2:获取蓝牙适配器
所有的蓝牙 Activity 都是需要 BluetoothAdapter 的。获取 BluetoothAdapter 调用 BluetoothAdapter 的静态方法 getDefaultAdapter()
方法。会返回一个表示设备自身的蓝牙适配器(蓝牙无线装置)的 BluetoothAdapter。
整个系统有一个蓝牙适配器,我们的应用可以通过 BluetoothAdapter 这个对象与之交互。如果 getDefaultAdapter()
返回 null 则说明 该设备不支持蓝牙。
1.3.3:启用蓝牙
需要确认是否已经开启蓝牙isEnabled()
。返回 false 则说明蓝牙处于关闭状态。请求启用蓝牙。使用 ACTION_REQUEST_ENABLE
操作 Intent 调用 startActivityForResult()
将通过系统设置发出启用蓝牙的请求。也可以通过 mBluetoothAdapter.enable()
直接打开蓝牙。
// 没有开始蓝牙
if(!mBluetoothAdapter.isEnabled()){
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent,REQUEST_ENBLE_BT);
}
我们的应用也可以选择侦听 ACTION_STATE_CHANGED
广播 Intent。每当蓝牙状态发生变化时,系统会广播此 Intent。此广播包含额外字段 EXTRA_STATE
和 EXTRA_PREVIOUS_STATE
分别表示新的和旧的蓝牙状态。
1.3.4:查找设备
使用 BluetoothAdapter 可以通过设备发现或通过查询配对设备的列表来查找远程蓝牙设备。
设备发现是一个扫描过程,它会搜索局部区域内已启用蓝牙功能的设备,然后请求一些关于各台设备的信息。这个过程也称为发现、查询、扫描。局部区域内的蓝牙设备仅在其当前已启用可检测性时才会响应发现请求。如果设备可以检测到,它将通过共享一些信息(例如设备名称、类及其唯一MAC地址)来响应发现请求。利用此信息,执行发现的设备可以选择发起到被发现设备的连接。
在首次与远程设备建立连接后,将会自动向用户显示配对请求。设备完成配对后,将会保存关于该设备的基本信息(如 设备名称、MAC 地址)。并且可以使用 Bluetooth API 读取这些信息。利用远程设备的已知 Mac 地址可以随时向其发起连接,而不需执行发现操作(假定该设备处于有效范围内)。
被配对和被连接之间存在差别。被配对意味着两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。被连接意味着设备当前共享一个 RFCOMM 通道,并且能够向彼此传输数据。当前的 Android Bluetooth API 要求对设备进行配对,然后才能建立 RFCOMM 连接(在使用 Bluetooth API 发起加密连接时,会自动执行配对)。Android 设备是默认处于不可检测状态的。
查询配对的设备,在执行设备发现之前,有必要查询已配对的设备集合。用来了解设备是否处于已知状态。通过 getBondedDevices()
来实现,这将返回表示已配对设备的一组 BluetoothDevice
。
例如:我们可以查询所有已配对的设备,然后使用 ArrayAdapter 向用户显示每台设备的名称:
Set<BluetoothDevice> pairedDevices = mBlutooothAdapter.getBondedDevices();
if(pairedDevices.size() > 0){
for(BluetoothDevice device:pairedDevices){
// 把名字和地址取出来添加到适配器中
mArrayAdapter.add(device.getName()+"\n"+ device.getAddress());
}
}
要发起连接仅需要知道目标蓝牙设备的 Mac 地址就可以了。
:
1.3.5:发现设备
发现设备使用 startDiscovery()
该进程为异步进程。该方法会立刻返回一个布尔值,指示是否已成功启动发现操作。发现进程通常包含约 12 秒的查询扫描,之后对发现的设备进行扫描,以检索其蓝牙设备的名字。
我们的应用必须针对 ACTION_FOUND
Intent 注册一个 BroadcastReceiver,以便接受每台发现的设备的信息。针对每台设备,系统会广播 ACTION_FOUND Intent。这个 Intent 会携带额外的字段 EXTRA_DEVICE 和 EXTRA_CLASS。这两者分别包含 BluetoothDevice 和 BluetoothClass。
// 创建一个接受 ACTION_FOUND 的 BroadcastReceiver
private final BroadcastReceiver mReceiver = new BroadcastReceiver(){
public void onReceive(Context context,Intent intent){
String action = intent.getAction();
// 当 Discovery 发现了一个设备
if(BluetoothDevice.ACTION_FOUND.equals(action)){
// 从 Intent 中获取发现的 BluetoothDevice
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 将名字和地址放入要显示的适配器中
mArrayAdapter.add(device.getName + "\n" + device.getAddress());
}
}
};
// 注册这个 BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReiver,filter);
// 在 onDestroy 中 unRegister
注意 执行 discovery 对于蓝牙适配器来说是一个非常繁重的过程,并且会消耗大量资源。在找到要连接的设备后,要确保使用 cancelDiscovery()
来停止发现,然后尝试连接。如果您已经和某台设备进行连接,那么这个时候执行发现操作会大幅度的减少此连接可用的带宽!因此不应该在处于连接状态的时候执行发现操作!
1.3.6:启用可检测性(如果是连接设备,可以忽略该步骤)
如果我们希望我们的设备是可以被其他设备检测到的,可以使用 ACTION_REQUEST_DISCOVERABLE
来操作 Intent 调用 startActivityForResult(Intent,int)
。这样会通过系统设置发出启用可检测到模式的请求(无需停止我们的应用)。默认情况下,设备会变为可检测状态并且持续 120 秒钟。我们还可以通过 EXTRA_DISCOVERABLE_DURATION
Intent Extra
来定义不同的持续时间。可以设置的最大持续时间为 3600 秒。值为 0 表示始终可以被检测到。任何小于 0 或者大于 3600 的值都会自动设置为 120 秒钟。
例如:
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,300);
startActivityForResult(discoverableIntent);
将显示对话框,请求用户允许将设备设为可检测到。如果用户响应为 YES,则设备将变为可检测到并持续指定的时间量。然后您的 Activity 将会收到对 onActivityResult() 回调的调用,其结果代码等于设备可检测到的持续时间。如果用户响应 NO 或者出现错误,结果代码为 RESULT_CANCELED
如果设备没有打开蓝牙,则启用设备可检测性的时候会自动启用蓝牙。
设备将在分配的时间内以静默方式保持可检测到模式。如果我们希望在可检测到模式发生变化时收到通知,可以利用 ACTION_SCAN_MODE_CHANGED
Intent 注册 BroadcastReceiver。它将包含额外字段 EXTRA_SCAN_MODE 和 EXTRA_PREVIOUS_SCAN_MODE,两者分别告诉我们新的和旧的扫描模式。每个字段可能包括SCAN_MODE_CONNECTABLE_DISCOVERABLE(可检测到模式)、SCAN_MODE_CONNECTABLE(未处于可检测模式但可以接受连接)、SCAN_MODE_NOE(未处于可检测到模式并且无法连接)
1.3.7:连接设备
要在两台设备上的应用之间创建连接,必须同时实现服务端和客户端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须发起连接(使用服务器设备的 MAC 地址发起连接)。当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,二者将被视为彼此连接。在这种情况下每台设备都能获得输入和输出流式传输,并且可以开始传输数据。
服务端和客户端分别以不同的方式来获得 BluetoothSocket 。服务器将在传入连接被接受时收到套接字。客户端将在其打开到服务器的 RFCOMM 通道时收到该套接字。
一种实现方式是自动将每台设备准备为一个服务器,从而使每台设备开发一个服务器套接字并侦听连接。然后任一设备可以发起与另一台设备的连接,并成为客户端。或者其中一台设备可显示“托管”连接并按需开放一个服务器套接字,从而另一台设备则直接发起连接。
在连接之前如果两个设备没有配对,则系统会自动发出配对请求
连接为服务器
当连接两台设备时,其中一台必须保持开发的 BluetoothServerSocket
来充当服务器,用于监听传入的连接请求,在接受了请求后提供一个已经连接的 BluetoothSocket
。从 BluetoothServerSocket
连接获取 BluetoothSocket
后就可以调用 close 来关闭这个等待了。
关于 UUID
通用唯一标识符(UUID),用于表示唯一标识信息的字符串ID,128位。可以使用网络上众多的随机 UUID 生成器,然后通过 formString(String)
来初始化一个 UUID。
服务器套接字接受连接的基本过程
- 通过
listenUsingRfcommWithServiceRecord(String,UUID)获取 BluetoothServerSocket
字符串是我们自己定义的服务的可识别名称,可以直接使用包名。系统会自定将其写入到设备上的新服务发现协议(SDP)数据库条目中。UUID 也在 SDP 中,作为与客户端设备连接协议的匹配规则。只有客户端和这里的UUID 一样了才可以会被连接 accept()
侦听连接请求
阻塞调用,将在连接被接受或者发生异常的时候返回,操作成功后,会返回BluetoothSocket
。- 除非要接受更多的连接,否则调用
close()
来关闭这个监听
这样会释放服务器套接字及其所有资源,但不会关闭已经连接的 BluetoothSocket。与 TCP/IP 不同的是,RFCOMM 一次只允许每个通道有一个已经连接的客户端。
放在子线程中去执行。
例子:
private class AcceptThread extend Thread{
private final BluetoothServerSocket mServerSocket;
public AcceptThread(){
mServerSocket = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME,MY_UUID);
}
public void run(){
BluetoothSocket socket = null;
while(true){
socket = mServerSocket.accept();
if(socket!=null){
// 自定义方法
manageConnectedSocket(socket);
mServerSocket.close();
break;
}
}
}
public void cancle(){
mServerSocket.close();
}
}
连接为客户端
要想和保持开发服务器套接字的设备建立连接,必须首先要获取该设备的 BluetoothDevice 对象。然后使用这个对象来获取 BluetoothSocket 并发起连接。
客户端连接的基本过程
- 通过 BluetoothDevice 的 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket 对象
这里的 UUID 要和服务器端的一致 - 通过 connect() 发起连接
执行此方法后,系统将会在远程设备上执行 SDP 查找,来匹配 UUID。如果查找成功了并且远程设备接受了该连接,它将共享 RFCOMM 通道在连接期间使用。这个时候 connect() 就会返回。这个方法也是阻塞的,如果失败或者超时(12秒之后),将引发异常。
调用 connect() 的时候要确保客户端没有执行发现操作。如果执行了会大幅度降低连接的速度,增加失败的可能性
例子
private class ConnectThread extend Thread{
private BluetoothDevice mDevice;
private BluetoothSocket mSocket;
public ConnectThread(BluetoothSocket device){
mDevice = device;
// 这里的 UUID 需要和服务器的一致
mSocket = device.createRfcommSocketToServiceRecord(My_UUID);
}
public void run(){
// 关闭发现设备
mBluetoothAdapter.cancelDiscovery();
try{
mSocket.connect();
}catch(IOException connectException){
try{
mSocket.close();
}catch(IOException closeException){
return;
}
}
// 自定义方法
manageConnectedSocket(mmSocket);
}
public void cancle(){
try{
mSocket.close();
}cathc(IOException closeException){
}
}
}
在连接之前调用 cancleDiscovery()
在进行连接之前应该始终调用这个方法,而且调用的时候无需检测是否正在扫描。
1.3.8:管理连接
建立连接后的两个设备都有一个 BluetoothSocket
通过这个 Socket 就可以在这两个设备间传输数据了。
过程:
- 获取 InputStream 和 OutputStream
- 使用 read(byte[])和 write(byte [])读取或者写入流式传输
1.3.9:使用配置文件
从 Android 3.0 开始, Bluetooth API 便支持使用蓝牙配置文件。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。
蓝牙配置文件就是设备间通信(蓝牙设备)的一种规范
免提配置文件便是一个示例,对于连接到无线耳机的手机,两台设备都必须支持免提配置文件。我们也可以通过实现接口 BluetoothProfile
来写入自己的类来支持特定的蓝牙配置文件。Android API 提供了以下的几种蓝牙配置文件的实现:
- 耳机:耳机配置文件提供了蓝牙耳机的支持。也就是这个配置文件提供了手机和蓝牙耳机进行通信的一种规范。使用 BluetoothHeadset 类,用于进程间通信来控制蓝牙耳机服务的代理。这个类包含 AT 命令支持。
- A2DP: 高级音频分发配置文件(A2DP)。定义了高质量音频如何通过蓝牙连接和流式传输,从一个设备传输到另一个设备。BluetoothAdp 类,是用于通过进程间通信(IPC)来控制蓝牙 A2DP 服务的代理。
- 健康设备: Android 4.0(API 14)引入了对蓝牙健康设备配置文件(HDP)的支持。这样就允许我们创建的应用可以使用蓝牙与支持蓝牙功能的健康设备进行通信。(心率检测仪、血糖仪、温度计等等)。详解的配置要查看健康设备配置文件。
使用配置文件的基本步骤
- 获取默认适配器 BluetoothAdapter
- 使用
getProfileProxy()
,建立到配置文件所关联的配置文件代理对象的连接。 - 设置监听
BluetoothProfile.ServiceListener
。这个监听会在客户端连接到服务或者断开服务连接的时候发送通知。 - 在
onServiceConnected()
中获取配置文件代理对象的句柄。 - 获取配置文件代理对象后,可以里脊将其用于监听连接状态和执行其他与该配置文件相关的操作。
例子: 如何连接到 BluetoothHeadset 代理对象,以便能够控制耳机配置文件:
BluetoothHeadset mBluetoothHeadset;
// 获取默认蓝牙适配器
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// 设置监听(监听连接状态)
private BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener(){
public void onServiceConnected(int profile,BluetoothProfile proxy){
if(profile == BluetoothProfile.HEADSET){
mBluetoothHeadset = (BluetoothHeadset)
}
}
public void onServiceDisconnected(int profile){
if(profile == BluetoothProfile.HEADSET){
mBluetoothHeadset = null;
}
}
}
// 建立与配置文件代理的连接
mBluetoothAdapter.getProfileProxy(contenxt,mProfileListener,BluetoothProfile.HEADSET);
// 使用 mBluetoothHeadset 代理内部的方法
// 使用完毕后关闭
mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);
供应商特定的 AT 命令
从 Android 3.0 开始。应用可以注册接受耳机所发送的预定义的供应商特定 AT 命令的系统广播(例如 Plantronics +XEVENT命令)(也就是说我们的应用可以接受耳机蓝牙商预定义的命令)。如:应用可以接受指示所连接设备的电池电量的广播,并根据需要通知用户或采取其他操作。使用 ACTION_VENDOR_SPECIFIC_HEADSET_EVENT intent 创建广播接收器,用来处理耳机供应商特定的 AT 命令。
健康设备配置文件
Android 4.0 引入了对蓝牙健康设备配置文件(HDP)的支持。这可以使用我们的应用使用蓝牙与支持蓝牙功能的健康设备进行通信(心率检测仪、血糖仪、温度计、台秤)
创建 HDP 应用:
- 获取 BluetoothHealth 代理对象
与常规耳机和 A2DP 类似。使用 ServiceListener 和 HEALTH 配置文件类型参数来调用 getProfileProxy(),来和配置文件代理对象建立连接。 - 创建 BluetoothHealthCallback 并注册充当健康汇集设备的应用配置(BluetoothHealthAPPConfiguration)
- 建立到健康设备的连接
- 成功连接到健康设备后,使用文件描述符对健康设备执行读写操作
- 完成后,关闭健康通道并取消注册该应用,该通道长时间闲置也会关闭。
1.3.10:总结
关于普通蓝牙设备和普通蓝牙设备之间的连接通信
- 通过 BluetoothAdapter 的 getDefaultAdapter 方法获取系统唯一的蓝牙适配器(如果返回为 null 则说明此设备不支持蓝牙)
- 通过 BluetoothAdapter 的 isEnable 方法判断是否已经打开蓝牙
- 可以通过 BluetoothAdapter.ACTION_REQUEST_ENABLE intent 来开启蓝牙,也可以直接 .enable 开启蓝牙
- 通过 调用 startDiscovery 开启发现周边设备(持续 12 秒),这个时候需要注册广播接收器来接受发现的蓝牙设备(及时关闭这个操作)
- 通过 BluetoothDevice 就可以进行建立连接,(分为客户端和服务端类似于 Socket 连接)
- 然后建立连接后就可以通过 BluetoothSocket获取里面的流来进行读写通信
关于蓝牙设备和蓝牙仪器(蓝牙耳机、电子秤等等类似产品)
这种之间的通信是通过配置文件代理来实现的。
都有一个对应的配置文件代理类。具体的操作是通过这个对象来完成。
2:蓝牙4.0标准(Ble开发)
蓝牙4.0标准是2012年推出的蓝牙标准,是蓝牙3.0的升级版本,相比3.0版本它更省电、成本更低,3毫秒低 延迟、具有超长有效连接距离和 AES-128加密等。
蓝牙4.0标准包括传统蓝牙部分和低功耗蓝牙模块部分。低功耗蓝牙是建立在传统蓝牙基础之上继而发展的。
那么,将蓝牙4.0标准按照应用和支持协议划分主要分为BT 和BLE两种:
经典蓝牙模块(BT):
泛指支持蓝牙协议在4.0以下的模块,一般用于数据量比较大的传输。
经典蓝牙模块可再细分为:传统蓝牙模块和高速蓝牙模块。
传统蓝牙模块在2004年推出,主要代表是支持蓝牙2.1协议的模块,在智能手机爆发的
时期得到广泛支持。
高速蓝牙模块在2009年推出,速率提高到约24Mbps,是传统蓝牙模块的八倍。
低功耗蓝牙模块(BLE):
指支持蓝牙协议4.0或更高的模块,也称为BLE模块(Bluetooth Low Energy Module),最大的特点是成功和功耗的降低。
蓝牙低功耗技术采用可变连接时间间隔,这个间隔根据具体应用可以设置为几毫秒到几秒不等。
另外,因为BLE技术采用非常快速的连接方式,因此可以处于“非连接”状态(节省能源),此时链路两端相互间仅能知晓对方,必要时可以才开启链路,然后在尽可能短的时间内关闭链路。
蓝牙BT/BLE只是蓝牙模块的一种分类方法,其实蓝牙模块还有很多分类。
举个栗子:
按照用用途来分,蓝牙模块有数据蓝牙模块,语音蓝牙模块,串口蓝牙模块和车载蓝牙模块;
按照芯片设计来分,蓝牙模块有flash版本和ROM版本。前者一般是BGA封装(求栅阵列封装),外置flash的,后者一般是LCC封装(表面贴装型封装),外接EEPROM。
2.1:低功耗BLE开发详解
1:申请权限和经典蓝牙一样
2:打开蓝牙和经典蓝牙一样
3:搜索设备(BLE蓝牙)
mBluetoothAdapter.startLeScan(callback);
private LeScanCallback callback = new LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int arg1, byte[] arg2) {
//device为扫描到的BLE设备
if(device.getName() == "目标设备名称"){
//获取目标设备
targetDevice = device;
}
}
};
4:连接设备
通过扫描BLE设备,根据设备名称区分出目标设备targetDevice,下一步实现与目标设备的连接。在连接设备之前要停止搜索蓝牙。
mBluetoothAdapter.stopLeScan(callback);
注 :停止搜索一般需要一定的时间来完成,最好调用停止搜索函数之后加以100ms的延时,保证系统能够完全停止搜索蓝牙设备。停止搜索之后启动连接过程。
BLE蓝牙的连接方法相对简单只需调用connectGatt
方法,函数原型如下:
public BluetoothGatt connectGatt (Context context, boolean autoConnect, BluetoothGattCallback callback);
参数说明
返回值 BluetoothGatt: BLE蓝牙连接管理类,主要负责与设备进行通信。后续会进一步介绍该类。
boolean autoConnect:建议置为false,能够提升连接速度。
BluetoothGattCallback callback 连接回调,重要参数,BLE通信的核心部分。
5:设备通信
与设备建立连接之后与设备通信,整个通信过程都是在BluetoothGattCallback的异步回调函数中完成。
BluetoothGattCallback中主要回调函数如下
private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt,
BluetoothGattDescriptor descriptor, int status) {
};
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
}
};
上述几个回调函数是BLE开发中不可缺少的,每个函数的意义以及被调用的时机会在后续步骤中一一说明
(1)等待设备连接成功
当调用targetdDevice.connectGatt(context, false, gattCallback)
后系统会主动发起与BLE蓝牙设备的连接,若成功连接到设备将回调onConnectionStateChange
方法,其处理过程如下:
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status,
int newState) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
Log.e(TAG, "设备连接上 开始扫描服务");
// 开始扫描服务,安卓蓝牙开发重要步骤之一
mBluetoothGatt.discoverServices();
}
if (newState == BluetoothGatt.STATE_DISCONNECTED) {
// 连接断开
/*连接断开后的相应处理*/
}
};
判断newState == BluetoothGatt.STATE_CONNECTED
表明此时已经成功连接到设备
(2)开启扫描服务
mBluetoothGatt.discoverServices();
扫描BLE设备服务是安卓系统中关于BLE蓝牙开发的重要一步,一般在设备连接成功后调用,扫描到设备服务后回调onServicesDiscovered()
函数,函数原型如下:
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
private List<BluetoothGattService> servicesList;
//获取服务列表
servicesList = mBluetoothGatt.getServices();
}
BLE蓝牙协议下数据的通信方式采用BluetoothGattService、BluetoothGattCharacteristic和BluetoothGattDescriptor三个主要的类实现通信。
BluetoothGattService 简称服务,是构成BLE设备协议栈的组成单位,一个蓝牙设备协议栈一般由一个或者多个BluetoothGattService组成。
BluetoothGattCharacteristic 简称特征,一个服务包含一个或者多个特征,特征作为数据的基本单元。
一个BluetoothGattCharacteristic特征包含一个数据值和附加的关于特征的描述BluetoothGattDescriptor。
BluetoothGattDescriptor:用于描述特征的类,其同样包含一个value值。
(3)获取负责通信的BluetoothGattCharacteristic
BLE蓝牙开发主要有负责通信的BluetoothGattService完成的。当且称为通信服务。通信服务通过硬件工程师提供的UUID获取。获取方式如下:
BluetoothGattService service = mBluetoothGatt.getService(UUID.fromString("蓝牙模块提供的负责通信UUID字符串"));
通信服务中包含负责读写的BluetoothGattCharacteristic,且分别称为notifyCharacteristic和writeCharacteristic。其中notifyCharacteristic负责开启监听,也就是启动收数据的通道,writeCharacteristic负责写入数据。
具体操作方式如下:
BluetoothGattService service = mBluetoothGatt.getService(UUID.fromString("蓝牙模块提供的负责通信服务UUID字符串"));
// 例如形式如:49535343-fe7d-4ae5-8fa9-9fafd205e455
notifyCharacteristic = service.getCharacteristic(UUID.fromString("notify uuid"));
writeCharacteristic = service.getCharacteristic(UUID.fromString("write uuid"));
(4)开启监听
开启监听,即建立与设备的通信的首发数据通道,BLE开发中只有当上位机成功开启监听后才能与下位机收发数据。开启监听的方式如下:
mBluetoothGatt.setCharacteristicNotification(notifyCharacteristic, true)
BluetoothGattDescriptor descriptor = characteristic
.getDescriptor(UUID
.fromString
("00002902-0000-1000-8000-00805f9b34fb"));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
若开启监听成功则会回调BluetoothGattCallback中的onDescriptorWrite()
方法,处理方式如下:
@Override
public void onDescriptorWrite(BluetoothGatt gatt,
BluetoothGattDescriptor descriptor, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
//开启监听成功,可以像设备写入命令了
Log.e(TAG, "开启监听成功");
}
};
5)写入数据
监听成功后通过向 writeCharacteristic写入数据实现与下位机的通信。写入方式如下:
//value为上位机向下位机发送的指令
writeCharacteristic.setValue(value);
mBluetoothGatt.writeCharacteristic(writeCharacteristic)
其中:value一般为Hex格式指令,其内容由设备通信的蓝牙通信协议规定。
(6)接收数据
若写入指令成功则回调BluetoothGattCallback中的onCharacteristicWrite()
方法,说明将数据已经发送给下位机。
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "发送成功");
}
super.onCharacteristicWrite(gatt, characteristic, status);
}
若发送的数据符合通信协议,则下位机会向上位机回复相应的数据。发送的数据通过回调onCharacteristicChanged()
方法获取,其处理方式如下:
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
// value为设备发送的数据,根据数据协议进行解析
byte[] value = characteristic.getValue();
}
通过向下位机发送指令获取下位机的回复数据,即可完成与设备的通信过程。
6:断开连接
当与设备完成通信之后之后一定要断开与设备的连接。调用以下方法断开与设备的连接:
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
通过以上六个主要步骤即可实现与设备的通信流程,BLE蓝牙开发流程相对固定,只需按照固定步骤执行即可。若按照上述流程扔不能完成整个通信过程
3:蓝牙开发中遇到的坑
(一)传递对象而不传递地址
开发之初实现主界面搜索设备,点击目标设备进入连接、通信界面的功能,因此需要传递目标设备BluetoothDevice的相关信息。起初采用Intent传递Bluetooth.getAddress()方式传递蓝牙的Mac地址,在ManagerActivity调用BluetoothDevice device = mBluetoothAdapter.getRemoteDevice()获取设备。实际使用中发现此函数比较耗时,影响效率。
解决办法:使用Intent传递BluetoothDevice对象。BluetoothDevice实现了Parcelable接口,可以使用Intent直接传递。
(二)加上延时,保证执行完毕
手机端打开蓝牙、关闭蓝牙、开启搜索和停止搜索都需要一定的时间来完成,因此在执行以上操作后最好加上一段延时,等待对应操作完成后再去执行下一步。
(三) BLE蓝牙连接参数
经典蓝牙建立连接后类似于Socket通信,出现问题的情况不多。遇到问题的情况大多数出现在BLE蓝牙中。
device.connectGatt(context, autoConnect, callback);第二个参数最好设置为false,实际测试发现设置为false连接速度更快。
(四)找准UUID,成功开启监听
BLE蓝牙的服务和特征都是通过UUID来查找的,开发时一定要区分好哪个BluetoothGattCharacteristic是用于setCharacteristicNotification的,哪个BluetoothGattCharacteristic是用于writeCharacteristic的。
setCharacteristicNotification函数用于开启监听,执行成功后相当于与设备建立通信通道,之后可以通过写入命令指令来获取设备中的数据。
setCharacteristicNotification的返回值基本上一直都是true,但是不代表开启监听成功。通过查看谷歌官方BLEdemo发现安卓系统下还需要设置用于监听下BluetoothGattCharacteristic的UUID为"00002902-0000-1000-8000-00805f9b34fb"的BluetoothGattDescriptor的Value为BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE。
当设置成功后回调onDescriptorWrite,当status == BluetoothGatt.GATT_SUCCESS基本上才能实现开启监听。而且开启监听以后最好还是要等待200ms左右再向设备发命令,等待设备和手机建立稳定通信通道。
(五)最多20字节
成功开启监听后,终于可以与设备通信了。我使用的设备是一个便携式心电计。上位机发送命令指令,下位机回复的数据格式均为64字节,然后将64字节解析出心率和心电图数据。理想很丰满,然而实际使用中才发现每次回调onCharacteristicChanged方法获取的数据最多20字节,64字节的数据分成了4包发送上来。
解决办法:只能用缓冲区接收了,判断缓冲区大小、判断缓冲区第一个字节,根据数据头读取缓冲区,并进一步解析数据。
不过值得庆幸的是上位机命令指令均小于20字节,起码写入数据不需要分包发送,如果真有遇到这种特殊情况的请看BLE分包发送。
既然最多20字节一包数据,大数据通信就不要考虑BLE了。
(六)一定要关闭连接
我就有一段程序在通信完毕后忘记了断开连接,发现之后再次连接设备以后每写入一次命令onCharacteristicChanged回调多次相同数据,使得缓冲区处理出现问题。感觉就是建立了多个连接通道。在通信出错或者完成同步之后一定要调用mBluetoothGatt.disconnect();
mBluetoothGatt.close();
断开当前连接。
(七)加密BLE设备
大多数BLE设备是非加密的,也就是不需要与手机配对即可通信。大部分软件都是同步BLE设备的中数据到手机端,整个流程也是自动执行的。大体流程就是:扫描目标设备、开启连接、连接成功扫描服务,扫到服务开启监听、收发数据、断开连接。
BLE设备加密后在连接设备时会弹出系统蓝牙配对对话框,等待用户配对。按常理说此时设备是没有建立连接的,因为还没有配对成功,但是安卓手机端不仅可以完成建立连接,而且还能扫描到服务。如果按照自动流程就去开启监听是无法成功的,因为设备还没有配对成功。
解决办法:监听系统关于蓝牙配对的广播,在完成配对之后在去开启监听。如果是已经配对的设备不会由蓝牙配对广播,只需按照不加密设备流程执行即可。
(八)6.0以上权限问题
最初测试用的设备比较老旧,有天用新手机测试发现根本不能扫描到蓝牙设备,查询之后发现是权限问题。安卓6.0以后扫描蓝牙需要模糊定位权限
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
安卓随着版本提升对于权限限制更加严格,还是学着用动态申请权限吧。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 1);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
(九)碎片化带来各种问题
安卓系统碎片化导致遇到了各种奇葩问题:
华为mate8 mate9 荣耀6a无法主动断开与加密BLE设备连接,除非取消配对或者关闭某一方的蓝牙。
魅族Flyme系统第一次配对成功后不能断开连接,以后使用可以正常断开。
部分手机在BLE设备连接上之后会主动断开一次连接,然后马上又重新连接上设备。