前段时间,项目要接入一个ble硬件,以前也没接触过ble开发,在查阅不少资料和踩了不少坑才完成任务,因此打算写一个简单的ble开发步骤,希望能帮助到初次接触ble开发的同学。
GATT:GATT 的全名是 Generic Attribute Profile(姑且翻译成:普通属性协议),它定义两个 BLE 设备通过叫做 Service 和 Characteristic 的东西进行通信。
Profile:Profile 并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。
Service:Service 是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。 UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,这个就可以自己随便设置。
Characteristic:在 GATT 事务中的最低界别的是 Characteristic,Characteristic 是最小的逻辑数据单元,与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。在 Android 开发中,建立蓝牙连接后,通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。
下面是一张来自官网的结构图
更多关于BLE GATT介绍可查看以下链接
BLE GATT介绍
GATT PROFILE 介绍
BluetoothAdapter
BluetoothAdapter 拥有基本的蓝牙操作,例如开启蓝牙扫描,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)实例化一个 BluetoothDevice 用于连接蓝牙设备的操作等等。
BluetoothDevice
代表一个远程蓝牙设备。这个类可以连接所代表的蓝牙设备或者获取一些有关它的信息,例如它的名字,地址和绑定状态等等。
BluetoothGatt
这个类提供了 Bluetooth GATT 的基本功能。例如重新连接蓝牙设备,发现蓝牙设备的 Service 、读写ble设备等等。
BluetoothGattService
对应前文所介绍的Service,通过 BluetoothGatt.getService 获得,通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现蓝牙数据的双向传输。
BluetoothGattCharacteristic
对应前文提到的 Characteristic。对ble设备的读写主要通过这个类来完成,也是我们主要打交道的类。
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
//先去获取BluetoothAdapter
private BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//检查手机蓝牙开关、是否支持ble
private void checkBluetooth() {
//是否支持蓝牙功能
if (mBluetoothAdapter == null) {
return;
}
//是否支持BLE
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(mContext, "不支持BLE功能", Toast.LENGTH_SHORT).show();
return;
}
//是否打开蓝牙开关
if (!mBluetoothAdapter.isEnabled()) {
Toast.makeText(mContext, "请打开蓝牙开关", Toast.LENGTH_SHORT).show();
return;
}
//搜索设备
scanBleDevice(true);
}
/**
* 搜索ble设备
* @param enable 开始搜索或停止搜索
*/
private void scanBleDevice(final boolean enable) {
if (enable) {
//开始搜索设备,搜索到设备会执行回调接口mLeScanCallback
mBluetoothAdapter.startLeScan(mLeScanCallback);
isScanning = true;
//搜索设备十分耗电,应该避免长时间搜索,这里设置10s超时停止搜索
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
scanBleDevice(false);
Toast.makeText(mContext, "搜索超时,请重试", Toast.LENGTH_SHORT).show();
}
}, 10 * 1000);
} else {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
isScanning = false;
}
}
/**
* 设备搜索回调
*/
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
//回调不是在ui线程中执行的,但是ble设备的连接、断开最好在ui线程中执行,否则可能会出现些奇奇怪怪的问题
((Activity)mContext).runOnUiThread(new Runnable() {
@Override
public void run() {
//判断设备是否是我们要找的
if (!TextUtils.isEmpty(device.getName()) && device.getName().equals(
"AP-002")) {
//找到设备后停止搜索,并取消开始搜索时设置的超时取消搜索
mHandler.removeCallbacksAndMessages(null);
mBluetoothAdapter.stopLeScan(mLeScanCallback);
if (isScanning) {
//开始连接设备
connect(device.getAddress());
isScanning = false;
}
}
}
});
}
};
//连接设备
private boolean connect(final String address) {
if (mBluetoothAdapter == null || TextUtils.isEmpty(address)) {
Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
return false;
}
//获取设备
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
if (device == null) {
Log.d(TAG, "Device not found. Unable to connect.");
return false;
}
//连接设备,连接成功或失败都会执行回调mGattCallback的
//onConnectionStateChange(BluetoothGatt gatt, int status, int newState)方法
//mGattCallback是mBluetoothGatt操作的回调
//包括读、写、连接、断开等操作,是与ble设备通信十分重要的一部分
mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
Log.d(TAG, "Trying to create a new connection.");
return true;
}
BluetoothGattCallback 有很多回调方法,我们只重写几个常用的方法
// **回调接口是在非ui线程回调**
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//设备发送数据回调,由于ble设备可能有有多个characteristic
//用于发送数据,这里进行比较,确认是否是我们想要的
if (characteristic.equals(mReceiveCharac)) {
//提取设备发送的数据
Log.d(TAG, "接收到设备发送的数据:"+characteristic.getValue());
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//mBluetoothGatt.DiscoverServices()的回调
Log.d(TAG, "discovery service successfully");
//以列表形式返回List mGattServices;
mGattServices = gatt.getServices();
//筛选保存我们需要的characteristic和service
displayGattServices(mGattServices);
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
//手机往ble设备写数据回调
if (status == BluetoothGatt.GATT_SUCCESS && characteristic.equals(mWriteCharac)) {
//写入的数据可以通过characteristic.getValue()获取,用于确认是否是之前写入的数据
//这里以一个关机命令为例
if (characteristic.getValue().equals(shutDownCommand)) {
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
Log.d(TAG, "写入关机命令成功");
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
super.onCharacteristicRead(gatt, characteristic, status);
//读设备回调,这里以读取设备电池状态为例
if (status == BluetoothGatt.GATT_SUCCESS && characteristic.equals(mBatteryCharac)) {
characteristic.getValue();
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
//连接状态回调
//连接或断开连接操作是否成功
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
//连接操作成功,获取service列表
gatt.discoverServices();
} else {
//断开连接操作成功
Log.d(TAG, "断开连接成功");
Toast.makeText(mContext, "设备已断开", Toast.LENGTH_SHORT).show();
mBluetoothGatt.close();
mBluetoothGatt = null;
return;
}
} else {
if (newState == BluetoothProfile.STATE_DISCONNECTED ) {
//连接失败,重连
//这里之所以连接失败还要先断开再重连
//是因为不知为何,有时连接成功后,系统又回调到这里
//如果不断开连接再重连的话会出问题
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
mHandler.post(new Runnable() {
@Override
public void run() {
reconnectDevice();
}
});
Log.d(TAG, "连接失败重连");
}
}
}
};
//重连设备
public void reconnectDevice() {
if (mBluetoothGatt == null) {
checkBluetooth();
Toast.makeText(mContext, "开始搜索连接设备", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mContext, "设备已连接", Toast.LENGTH_SHORT).show();
}
}
/**
* 遍历设备所有service和characteristic
* 一般我们再开发接入ble设备时,硬件那边会给出ble设备文档
* 根据文档所说的service、characteristic的uuid进行读写操作
* 保存所需characteristic
* 设置用于接收设备数据的characteristic notification属性
* @param gattServices
*/
private void displayGattServices(List gattServices) {
Log.d(TAG, Thread.currentThread()+""+"display service and characteristics");
if (gattServices == null) {
return;
}
for (BluetoothGattService gattService : gattServices) { // 遍历出gattServices里面的所有服务
//比较service的uuid,只取所需的service
if (gattService.getUuid().equals(UUID.fromString(BATTERY_SERVICE_UUID))
|| gattService.getUuid().equals(UUID.fromString(PRIVATE_SERVICE_UUID))) {
List gattCharacteristics =
gattService.getCharacteristics();
Log.d(TAG, "service uuid: " + gattService.getUuid());
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { //
// 遍历每条服务里的所有Characteristic,保存所需Characteristics
Log.d(TAG, "characteristics uuid: " + gattCharacteristic.getUuid());
if (gattCharacteristic.getUuid().toString().equalsIgnoreCase(
BATTERY_CHARACTERISTICS_UUID)) {
//读数据characteristic,这里以电池characteristic为例
mBatteryCharac = gattCharacteristic;
} else if (gattCharacteristic.getUuid().toString().equalsIgnoreCase(
RECEIVE_CHARACTERISTICS_UUID)) {
//接收数据characteristic
mReceiveCharac = gattCharacteristic;
//设置notification通知,按键回调在onCharacteristicChanged
//以下代码一定不能少,否则将无法接收到设备发送的数据
boolean isEnableNotification = mBluetoothGatt.setCharacteristicNotification(
mReceiveCharac, true);
if (isEnableNotification) {
List descriptorList =
mReadKeypressCharac.getDescriptors();
if (descriptorList != null && descriptorList.size() > 0) {
for (BluetoothGattDescriptor descriptor : descriptorList) {
descriptor.setValue(
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
}
} else if (gattCharacteristic.getUuid().toString().equalsIgnoreCase(
WRITE_DATA_UUID)) {
//写数据characteristic
mWriteCharac = gattCharacteristic;
}
}
}
}
}
/**
* 断开连接,回调mGattCallback
*
*/
public void disconnectDevice() {
if (mBluetoothGatt != null) {
mBluetoothGatt.disconnect();
}
}
对设备的读、写、接收都会在mGattCallback中回调
硬件工程师在开发ble设备时,会设置不同的characteristic,并设置其读、写、通知等属性,只有具有这些属性,才可以对characteristic进行相应的读写等操作。
这里说明下,读数据和接收数据是不一样的,读数据需要我们主动操作,通过BluetoothGatt#readCharacteristic(),发起读操作,然后在回调方法里接收数据;而接收数据是ble设备主动发送数据,系统的BLE框架会回调onCharacteristicChanged(),我们只需在这个方法里提取数据即可。
当我们要接入ble设备时,要根据ble设备的文档,对指定characteristic进行指定读、写、接收等操作,才能实现正确的通信。
**注意:characteristic的读、写操作都是串行的,也就是说,只有前一个读写操作回调成功后才会执行下一个操作,
若是在上一个读写操作还没回调便进行下一个操作,那么这个操作会被ble设备抛弃掉。**
/**这里以本人手头上ble设备读取电池状态为例
*不同的characteristic作用不同,要根据硬件接入文档,
*选择正确的characteristic进行操作
* 获取电池状态
*/
private void getBatteryInfo() {
if (mBluetoothGatt != null && mBatteryCharac != null) {
mBluetoothGatt.readCharacteristic(mBatteryCharac);
Log.d(TAG, "读取电量");
}
}
接着在回调方法里取出数据
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
super.onCharacteristicRead(gatt, characteristic, status);
//读设备回调,这里以读取设备电池状态为例
if (status == BluetoothGatt.GATT_SUCCESS && characteristic.equals(mBatteryCharac)) {
//取出数据
characteristic.getValue();
}
}
在通过指定characteristic向ble设备写入数据时,一次性只能写入20字节的数据,当需要写入的数据超过20字节时就要分次写入。
一般,ble设备会要求我们写入一些固定的数据,来作为ble识别的命令
这里,以一个让ble关机的命令为例
//关闭设备命令,20字节
private byte[] shutDownCommand = new byte[]{0x21, 0x10, 0x26, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, (byte) 0xFF};
/**
* 写入关闭设备命令
*/
public void shutdownDevice() {
if (mBluetoothGatt != null && mWriteDataCharac != null) {
Log.d(TAG, "关闭设备");
mWriteCharac.setValue(shutDownCommand);
mWriteCharac.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
mBluetoothGatt.writeCharacteristic(mWriteDataCharac);
}
}
接着回调
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
//写入命令成功,且characteristic是我们所写入的characteristic
if (status == BluetoothGatt.GATT_SUCCESS && characteristic.equals(mWriteCharac)) {
//写入的数据可以通过characteristic.getValue()获取,用于确认是否是之前写入的数据
if (characteristic.getValue().equals(shutDownCommand)) {
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
Log.d(TAG, "写入关机命令成功");
}
}
}
对于接收ble设备数据,手机是被动接收的。也就是说,当设备通过指定characteristic发送数据时,系统会回调BlutoothGatt#onCharacteristicChanged(),在这个回调方法里通过characteristic.getValue()来获取设备发送的数据。但是,能够接收到数据的前提是对characteristic设置通知属性,前面在筛选保存service和characteristic时候,我们进行这么一个操作
f (gattCharacteristic.getUuid().toString().equalsIgnoreCase(RECEIVE_CHARACTERISTICS_UUID)) {
//接收数据的characteristic
mReceiveCharac = gattCharacteristic;
//设置notification通知属性,按键回调在onCharacteristicChanged
//以下代码一定不能少,否则将无法接收到设备发送的数据
boolean isEnableNotification = mBluetoothGatt.setCharacteristicNotification(
mReceiveCharac, true);
if (isEnableNotification) {
List descriptorList =
mReadKeypressCharac.getDescriptors();
if (descriptorList != null && descriptorList.size() > 0) {
for (BluetoothGattDescriptor descriptor : descriptorList) {
descriptor.setValue(
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
}
}
对设备要发送数据的characteristic设置notification属性,并对该characteristic下的descriptor设置BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE。这样,才能够成功回调onCharacteristicChanged()方法并接收数据
下面以接收ble设备按键按下时发送数据为例
//对于接收数据,我们无需做什么操作,只需要这回调方法里获取数据即可
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//设备发送数据回调,由于ble设备可能有有多个characteristic
//用于发送数据,这里进行比较,确认是否是我们想要的
if (characteristic.equals(mReceiveCharac)) {
//提取设备发送的数据
Log.d(TAG, "接收到设备发送的数据:"+characteristic.getValue());
}
}
以上便是ble设备接入的基本流程和读写接收等操作,一些容易踩到的坑和重点在注释中已经写地很罗嗦,有不明白的可以在文章下留言,一起探讨,希望这篇文章能对大家有所帮助。