目录
本文前序
Android API选择
Android 4.3(API 级别 18)引入了内置平台支持 蓝牙低功耗(BLE)的核心角色,并提供应用程序可用于发现设备,查询服务和传输信息的 API。
而在 Bluetooth 的官方资料中
BLE 平台支持
从以下版本开始,大多数主要平台都支持蓝牙 4.0 和蓝牙低功耗(这是 BT 4.0 的一个子集):
- iOS5 +(iOS7 + 首选)
- Android 4.3+(4.4 + 中的大量错误修复)
- Apple OS X 10.6+
- Windows 8(XP,Vista 和 7 仅支持蓝牙 2.1)
- GNU / Linux Vanilla BlueZ 4.93+
所以使用 Android 做 BLE 开发还是建议使用 Android 4.4 以上版本,Android5.0 支持手机作为外设,个人推荐 Android 5.0+
Android BLE开发流程
获取权限 -->
打开蓝牙 -->
扫描 -->
连接 -->
获取服务,特征 -->
打开通知 -->
通讯(读写特征) -->
断开连接
获取权限
若想在应用程序中使用蓝牙功能,必须声明权限 BLUETOOTH,才能进行请求连接,接受连接、传输数据等功能。
鉴于 LE 信标 通常与位置相关联,您必须声明获取位置得权限。 如果没有此权限,扫描将不会返回任何结果。如果我们的应用程序针对 的是 Android 9(API 级别 28)或更低版本,则可以声明
ACCESS_COARSE_LOCATION
权限而不是ACCESS_FINE_LOCATION
权限。
实际使用时,我们通常希望应用启动设备发现或操作蓝牙设置,因此还必须声明 BLUETOOTH_ADMIN
权限。
ps:如果您使用该 BLUETOOTH_ADMIN
权限,则您还必须拥有该 BLUETOOTH
权限。
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!-- If your app targets Android 9 or lower, you can declare
ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
此外,我们仅使用 BLE 的话还应该加上
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
需要特别注意:
Android6.0+ 的版本,想要扫描低功率蓝牙设备,应用需要拥有访问设备位置的权限。这是因为 Bluetooth beacons 蓝牙信标,可用于确定手机和用户的位置。有些设备在申请位置权限后,还需要打开定位服务(GPS等)才能扫描到 BLE 设备。
动态添加位置权限的方法可以参考 博客,否则扫描会发生以下 错误:
另外在看到有网友提醒说:
Android7.0+ 不能在 30s 内扫描和停止超过 5 次。
官网没特意说明,可自行测试,设置扫描时长为 3s ,连续扫描 10 次,稳定复现 5 次后不能扫描到任何设备。
Android 蓝牙模块会打印当前应用扫描太频繁的 log日志 ,并在 Android 5.0 的 ScanCallback回调 中触发onScanFailed(int),返回错误码:ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED, 表示 app无法注册,无法开始扫描。
ps:目前我还没有测试
打开蓝牙
检查 Android 设备 是否支持 BLE
在应用程序可以通过 BLE 进行通信之前,我们需要验证设备是否支持 BLE,如果是,请确保它已启用。
官方ps:仅当 <uses-feature...android:required="flase"/> 时才需要进行此检查。
如果不支持 BLE,则应禁用任何 BLE 功能。 如果支持 BLE 但已被禁用,则我们应该请求用户启用蓝牙。
开启蓝牙
BluetoothAdapter 是所以蓝牙活动所必须的。 该 BluetoothAdapter 代表了移动端本地的蓝牙适配器,可以通过它对蓝牙进行基本的操作, 一个 Android系统 只有一个蓝牙适配器,我们通过
getSystemService()
返回一个实例BluetoothManager
,然后再获取适配器。
private BluetoothAdapter bluetoothAdapter;
//...
// Initializes Bluetooth adapter.
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
启动蓝牙
接下来,我们需要确保启用蓝牙。 使用 isEnabled()
检查当前是否启用了蓝牙。如果未开启,则会请求启用蓝牙:
private static final int REQUEST_ENABLE_BT = 1;
//...
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
REQUEST_ENABLE_BT
是一个本地定义的整数(必须大于 0),传入函数startActivityForResult(android.content.Intent, int);
系统会在onActivityResult(int, int, android.content.Intent)
中将其作为requestCode
参数返回。ps:不例程未对
REQUEST_ENABLE_BT
进行使用,可在Google BLE例程 -- Scan 中查找到使用范例
查找BLE设备
简单扫描
public BluetoothManager mBluetoothManager;
public BluetoothAdapter mBluetoothAdapter;
//扫描状态
public boolean mScanning = false;
//BluetoothManager 初始化
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
//BluetoothAdapter 初始化
mBluetoothAdapter = mBluetoothManager.getAdapter();
//BLE扫描实例初始化
//mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
private void scanNew()
{
mScanning = true;
//基本的扫描方法
mBluetoothManager
.getAdapter()
.getBluetoothLeScanner()
.startScan(mScanCallback);
}
/**
* 停止扫描
*/
private void stopNewScan()
{
mScanning = false;
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
}
扫描回调
例程显示的是将不重复的设备放入 ble_Devices 中保存
//存放扫描结果
private List<BluetoothDevice> ble_Devices;
//...
//设备列表初始化
ble_Devices = new ArrayList<BluetoothDevice>();
/**
* BLE 扫描结果回调
*/
ScanCallback mScanCallback = new ScanCallback()
{
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType,result);
BluetoothDevice device = result.getDevice();
//判断是否已经添加
if (!ble_Devices.contains(device))
{
//添加BLE到ble_Devices列表
ble_Devices.add(device);
}
//callbackType:扫描模式
//Log.i("TAG","callbackType:"+callbackType);
//无名设备
if(result.getDevice().getName() == null)
Log.i("TAG","BLE_name:N/A");
else
Log.i("TAG","BLE_name"+result.getDevice().getName());
//result:扫描到的设备数据,包含蓝牙设备对象,解析完成的广播数据等
}
};
我们在回调中获取 扫描到的 BluetoothDevice 设备信息
主要有以下方法:
其中 getAddress() 获取的 MAC地址;可用于 BLE设备 建立 GATT连接 。
进阶
设置扫描参数及过滤条件
public BluetoothManager mBluetoothManager;
public BluetoothAdapter mBluetoothAdapter;
//扫描状态
public boolean mScanning = false;
//BluetoothManager 初始化
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
//BluetoothAdapter 初始化
mBluetoothAdapter = mBluetoothManager.getAdapter();
//BLE扫描实例 初始化
mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
/**
* 启动扫描
*/
private void scanNew()
{
mScanning = true;
//设置扫描参数
ScanSettings settings = new ScanSettings
.Builder()
//这里设置的低延迟模式,也就是更快的扫描到周围设备,相应耗电也更厉害
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
//设置过滤条件,不只可以像旧API中的按服务UUID过滤;还可以按设备名称,MAC地址等条件过滤
List<ScanFilter> scanFilters = new ArrayList<>();
//需要过滤扫描到的设备可以用下面的这种构造方法
mBLEScanner
.startScan(scanFilters,settings,mScanCallback);
}
/**
* 停止扫描
*/
private void stopNewScan()
{
mScanning = false;
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
}
定时器中断扫描
扫描是很占用资源的,这里添加定时器 10s 后停止搜索
//蓝牙扫描时间
private static final long SCAN_TIME = 10000;
//判断扫描状态
if (false == mScanning)
{
//扫描设备
scanNew();
//定时关闭蓝牙扫描,定时10s
handler.postDelayed(runnable, SCAN_TIME);
}
//...
/**
* BLE 扫描按键 - 点击触发
*/
mButton_Ble_Scan.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//消息提醒
Toast.makeText(MainActivity.this,"请等待"+SCAN_TIME/1000+"s",Toast.LENGTH_LONG).show();
//判断扫描状态
if (false == mScanning)
{
//扫描设备
scanNew();
//定时关闭蓝牙扫描,定时10s
handler.postDelayed(runnable, SCAN_TIME);
}
}
});
建立连接
与一个 BLE 设备 交互的第一步就是连接 BLE 设备 上的 GATT 服务端。
connectGatt( )
为了连接到 BLE 设备 上的 GATT 服务端,需要使用 connectGatt ( ) 方法。
这个方法需要三个参数:一个 Context 对象,自动连接(boolean 值,表示只要 BLE 设备 可用是否自动连接到它),和 BluetoothGattCallback 调用。
mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mBluetoothGattCallback);
public BluetoothDevice mBluetoothDevice;
public BluetoothGatt mBluetoothGatt;
//GATT连接回调函数
public BluetoothGattCallback mBluetoothGattCallback;
//...
//存在设备列表,可进行点击链接
if (null != ble_Devices && ble_Devices.isEmpty())
{
ble_Device_lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//Toast.makeText(MainActivity.this,"BLE_Devices:"+ ble_Devices.get(position).getName(),Toast.LENGTH_SHORT).show();
/**
*
* 预留的BLE连接接口
* 所选中的ble设备为 mBluetoothDevice = ble_Devices.get(position)
*/
mBluetoothDevice = ble_Devices.get(position);
//发起连接:设置为true时可用时尽快连接到BLE设备
mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mBluetoothGattCallback);
}
});
BluetoothGattCallback( )
mBluetoothGattCallback = new BluetoothGattCallback() {
/**
* 连接状态发生改变时触发此回调,返回连接状态
* @param gatt GATT客户端
* @param status 链接或者断开连接是否成功
* @param newState 返回一个新的状态
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
//连接成功,异步调用,所以刷新UI的操作最好放到主线程
if (BluetoothProfile.STATE_CONNECTED == newState)
{
//提示连接成功
Log.i("TAG","连接成功");
//Toast.makeText(MainActivity.this,"连接成功",Toast.LENGTH_SHORT).show();
//连接成功后调用发现服务
//用来发现远程设备提供的服务,以及它们包含的特征特性和描述符
gatt.discoverServices();
}
else if (BluetoothProfile.STATE_DISCONNECTING == newState)
{
//断开连接
// Toast.makeText(MainActivity.this,"断开连接",Toast.LENGTH_SHORT).show();
Log.i("TAG","断开连接");
}
}
/**
* 当服务检索完成后会回调该方法,检索完成后我们就可以拿到需要的服务和特征
* 成功获取服务时触发此回调
* @param gatt
* @param status
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//...
}
/**
* 对特征值读操作完成时触发此回调
* @param gatt GATT客户端
* @param characteristic 被读的特征
* @param status 操作的状态码,返回0表示操作成功
*/
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
/**
* 对特征值写操作完成时触发此回调
* @param gatt GATT客户端
* @param characteristic 被写的特征
* @param status 操作的状态码,返回0表示操作成功
*/
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
/**
* 当特征值改变时触发此处回调,*******打开通知********
* @param gatt GATT客户端
* @param characteristic 操作的状态码,返回0表示操作成功
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
}
};
此外还有很多方法可供重写,建议直接进入 BluetoothGattCallback类 中查看 或点击 这里 查看官方API说明
获取服务和特征值
前面用到了gatt.discoverServices()
它的回调方法即onServicesDiscovered(),我们在它的回调方法中获取到服务和特征,并保存起来。
/**
* 当服务检索完成后会回调该方法,检索完成后我们就可以拿到需要的服务和特征
* 成功获取服务时触发此回调
* @param gatt
* @param status
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (BluetoothGatt.GATT_SUCCESS == status)
{
//获取特定的UUID服务
mBluetoothGattService = gatt.getService(UUID.fromString(Service_uuid));
Log.i("TAG","BluetoothGattService:"+mBluetoothGattService.getUuid().toString());
}
if (null != mBluetoothGattService)
{
//获取该服务下指定的UUID特征值
mBluetoothGattCharacteristic = mBluetoothGattService.getCharacteristic(UUID.fromString(Characteristic_uuid));
Log.i("TAG","BluetoothGattCharacteristic:"+mBluetoothGattCharacteristic.getUuid().toString());
}
//...
}
打开通知
找到这个特定的 BluetoothGattCharacteristic 后,我们希望该特征值发生改变时可以得到通知,这里使用setCharacteristicNotification()为特性设置通知
/**
* 当服务检索完成后会回调该方法,检索完成后我们就可以拿到需要的服务和特征
* 成功获取服务时触发此回调
* @param gatt
* @param status
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (BluetoothGatt.GATT_SUCCESS == status)
{
//获取特定的UUID服务
mBluetoothGattService = gatt.getService(UUID.fromString(Service_uuid));
Log.i("TAG","BluetoothGattService:"+mBluetoothGattService.getUuid().toString());
}
if (null != mBluetoothGattService)
{
//获取该服务下指定的UUID特征值
mBluetoothGattCharacteristic = mBluetoothGattService.getCharacteristic(UUID.fromString(Characteristic_uuid));
Log.i("TAG","BluetoothGattCharacteristic:"+mBluetoothGattCharacteristic.getUuid().toString());
/**
* 打开通知
* 注意:读、写、通知等这些 GATT 的操作都只能串行的使用
* 并且在执行下一个任务前必须保证上一个任务已经完成并且成功回调,否则可能出现后面的任务都阻塞无法进行的情况
* 对于开启通知这个操作触发 onDescriptorWrite 时代表任务完成,可以进行下一个 GATT 操作
*/
// 法一:
// 打开对该特征的通知
mBluetoothGatt.setCharacteristicNotification(mBluetoothGattCharacteristic,true);
/*
// 法二:
// 通过对手机B(远程)中需要开启通知的那个特征的CCCD写入开启通知命令,来打开通知
BluetoothGattDescriptor descriptor = mBluetoothGattCharacteristic.getDescriptor(
UUID.fromString(Characteristic_uuid));
// 获取特征的写入类型,用于后面还原
int parentWriteType = mBluetoothGattCharacteristic.getWriteType();
// 设置特征的写入类型为默认类型
mBluetoothGattCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
// 还原特征的写入类型
mBluetoothGattCharacteristic.setWriteType(parentWriteType);
*/
}
//...
}
通讯
BluetoothGattCallback()中可以设置读写特征值的回调函数
/**
* 对特征值读操作完成时触发此回调
* @param gatt GATT客户端
* @param characteristic 被读的特征
* @param status 操作的状态码,返回0表示操作成功
*/
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
/**
* 对特征值写操作完成时触发此回调
* @param gatt GATT客户端
* @param characteristic 被写的特征
* @param status 操作的状态码,返回0表示操作成功
*/
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
以及特征值改变时触发的回调方法,注意此方法要 开启监听
我们可以在此对接收到的BLE数据进行解析
/**
* 当特征值改变时触发此处回调
* @param gatt GATT客户端
* @param characteristic 操作的状态码,返回0表示操作成功
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//解析数据
//BLE设备发送给APP的数据
}
读特征值Demo
//接收demo
byte[] read_bytes = characteristic.getValue();
//转化为text
String read_str= new String (read_bytes);
Log.i("BLE_Read_DATA",read_str);
写特征值Demo
//发送demo
if (null != mBluetoothGattCharacteristic)
{
String write_str = "APP BLE发送成功~~";
byte[] wtite_bytes= write_str.getBytes();
mBluetoothGattCharacteristic.setValue(wtite_bytes);
mBluetoothGatt.writeCharacteristic(mBluetoothGattCharacteristic);
}
关闭连接
/**
* 断开连接
* 最好在主进程中实现
* 如果调用 disConnect () 方法后立即调用 close () 方法
* 蓝牙能正常断开,只是在 onConnectionStateChange 中我们就收不到 newState 为 BluetoothProfile.STATE_DISCONNECTED 的状态回调
* 因此,可以在收到断开连接的回调后在关闭 GATT 客户端。
*/
public void disConnect()
{
mBluetoothGatt.disconnect();
//gatt.close();
}
为此我们在onConnectionStateChange()中增加判断 BluetoothProfile.STATE_DISCONNECTED状态
/**
* 连接状态发生改变时触发此回调,返回连接状态
* @param gatt GATT客户端
* @param status 链接或者断开连接是否成功
* @param newState 返回一个新的状态
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
//连接成功,异步调用,所以刷新UI的操作最好放到主线程
if (BluetoothProfile.STATE_CONNECTED == newState)
{
//提示连接成功
Log.i("TAG","连接成功");
//Toast.makeText(MainActivity.this,"连接成功",Toast.LENGTH_SHORT).show();
//连接成功后调用发现服务
//用来发现远程设备提供的服务,以及它们包含的特征特性和描述符
gatt.discoverServices();
}
else if (BluetoothProfile.STATE_DISCONNECTING == newState)
{
//断开连接
// Toast.makeText(MainActivity.this,"断开连接",Toast.LENGTH_SHORT).show();
Log.i("TAG","断开连接");
}
else if (BluetoothProfile.STATE_DISCONNECTED == newState)
{
//关闭GATT客户端
gatt.close();
}
}
自此,基础的Android BLE通讯结束。
由于才疏学浅,文章难免有疏漏和理解不到位的地方,如有发现还望指正。
下一步将梳理该 DEMO,实现封装,目前的计划是参考 Goodle 官方例程实现。
Google BLE例程资源
Google BLE Samples 或 GitHub android-BluetoothLeGatt
参考鸣谢