在app
应用的开发过程中,一般和蓝牙接触的不多,但是随着智能穿戴设备的发展,穿戴设备和手机关联的app
越来越多,之前也是没怎么接触过这一块的东西,正好最近需要做一个和蓝牙有关的app
,所以研究学习下,把应用的东西总结一下。项目源码已经上传github。
BLE
是Bluetooth Low Energy
的缩写,又叫蓝牙4.0,区别于蓝牙3.0和之前的技术。BLE
前身是NOKIA
开发的Wibree
技术,主要用于实现移动智能终端与周边配件之间的持续连接,是功耗极低的短距离无线通信技术,并且有效传输距离被提升到了100米以上,同时只需要一颗纽扣电池就可以工作数年之久。
BLE
是在蓝牙技术的基础上发展起来的,既同于蓝牙,又区别于传统蓝牙。BLE
设备分单模和双模两种,双模简称BR
,商标为Bluetooth Smart Ready
,单模简称BLE或者LE,商标为Bluetooth Smart
。Android
是在4.3后才支持BLE
,这可以解释不是所有蓝牙手机都支持BLE
,而且支持BLE
的蓝牙手机一般是双模的。
BLE
和普通蓝牙之间是有区别的:
优点:
功耗更低
连接速度更快
缺点:
每次发送的数据比较小
ATT
ATT
(Attribute Protocol)协议是基础协议,ATT
针对BLE
设备进行了特别的优化,它的基础是属性,使用一个UUID
来定义属性的类型。
GATT
GATT
(Generic Attribute Profile)是所有BLE
顶层协议的基础,它定义了怎么把一堆ATT
属性分组成为有意义的服务。
services
服务,基础是UUID
的值为0x2800的属性。所有跟在这个属性后面的属性都属于这个属性定义的服务,直到另一个0x2800属性出现。一个BLE
设备可以有多个服务。
characteristics
特征,每一个服务都可以包含有多个特征,特征存储了有用的值以及权限。其中蓝牙模块和app
进行通讯主要是通过它来进行。
descriptor
特征的描述,又叫描述符,在一个特征中会有多个描述符。GATT
协议已经定义了大多数的标准描述符,这其中有一个特别重要的描述符是:client characteristic configuration
其UUID
是0x2902,具有一个16bit的可读写值。被用来定义通知和暗示,通过设置可以能够让设备发送通知,并且被主机端接收到。
Notification
通知,BLE
模块向空中发送消息,可以被主机端的蓝牙模块接收到。包含在characteristics
中,但是需要权限打开。
BLE透传
透传模式下,所有的串口数据都被看做用户数据,模块会将这些数据通过蓝牙发送给主机,即是蓝牙模块和手机或者其他控制设备之间的通讯(其实还有另外一种命令模式,但是透传模式更快速和简单。),透传模式即是通过services
和characteristics
进行。一般厂家或者工程师会给出相应的说明,例如下图:
角色和职责
Android
设备和BLE
设备交互有两组角色:
中心设备(Central
)和外围设备(Periphery
)。外围设备是数据提供者,中央是数据使用/处理者。一个中央设备可以同时连接多个外围设备,但是一个外围设备同时只能连接一个中央设备。
准备权限
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
准备BLE
BluetoothAdapter
: BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
//打开蓝牙 方法一
if (!bluetoothAdapter.isEnabled()) {
bluetoothAdapter.enable();
}
//方法二 推荐
Intent enabler = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enabler, REQUEST_ENABLE);
BLE
: private boolean checkBluetooth() {
if (!getPackageManager().hasSystemFeature(
PackageManager.FEATURE_BLUETOOTH_LE)) {
return false;
}
return true;
}
bluetoothAdapter.startLeScan(mLeScanCallback);
BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
runOnUiThread(new Runnable() {
@Override
public void run() {
/*
显示了一个列表,点击进入具体设备页面 BLEDeviceTestActivity
并且将设备的名称和地址传递过去
intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_NAME, device.getName());
intent.putExtra(DeviceControlActivity.EXTRAS_DEVICE_ADDRESS, device.getAddress());
*/
adapter.add("name : " + device.getName() + "\n address : " + device.getAddress());
bluetoothDevices.add(device);
}
});
}
};
连接设备
在BLEDeviceTestActivity
中获取设备,并且连接设备:
//根据地址获取设备
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(addressStr);
//获取链接 这个时候需要实现BluetoothGattCallback
BluetoothGatt bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback);
有关device.connectGatt(this, false, bluetoothGattCallback);
方法,可以看到它是通过设备建立一个GATT
链接,任何有关GATT
链接的操作都将触发回调。而BluetoothGattCallback
会异步处理这些回调结果。
/**
* Connect to GATT Server hosted by this device. Caller acts as GATT client.
* The callback is used to deliver results to Caller, such as connection status as well
* as any further GATT client operations.
* The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
* GATT client operations.
* @param callback GATT callback handler that will receive asynchronous callbacks.
* @param autoConnect Whether to directly connect to the remote device (false)
* or to automatically connect as soon as the remote
* device becomes available (true).
* @throws IllegalArgumentException if callback is null
*/
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback) {
return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
}
所以实现bluetoothGattCallback
:
BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
/**
* 返回链接状态
* @param gatt
* @param status 链接或者断开连接是否成功 {@link BluetoothGatt#GATT_SUCCESS}
* @param newState 返回一个新的状态{@link BluetoothProfile#STATE_DISCONNECTED} or
* {@link BluetoothProfile#STATE_CONNECTED}
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
}
/**
* 获取到链接设备的GATT服务时的回调
* @param gatt
* @param status 成功返回{@link BluetoothGatt#GATT_SUCCESS}
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
}
/**
* 读特征的时候的回调
* @param gatt
* @param characteristic 从相关设备上面读取到的特征值
* @param status
*/
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
/**
* 指定特征写入操作的回调结果
* @param gatt
* @param characteristic
* @param status
*/
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
/**
* 设备发出通知时会调用到该接口
* @param gatt
* @param characteristic
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
}
/**
* 指定描述符的读操作的回调
* @param gatt
* @param descriptor
* @param status
*/
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorRead(gatt, descriptor, status);
}
/**
* 指定描述符的写操作
* @param gatt
* @param descriptor
* @param status
*/
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
}
/**
* 当一个写入事物完成时的回调
* @param gatt
* @param status
*/
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
super.onReliableWriteCompleted(gatt, status);
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
super.onReadRemoteRssi(gatt, rssi, status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
}
};
可以看到BluetoothGattCallback
有很多方法,根据名字不难明白其意思,接着根据蓝牙的使用过程来一个一个的分析就能清晰明了其中功能。
因为我们前面进行了链接,所以首先触发的回调是onConnectionStateChange()
方法:
/**
* 返回链接状态
* @param gatt
* @param status 链接或者断开连接是否成功 {@link BluetoothGatt#GATT_SUCCESS}即表示操作是否成功
* @param newState 返回一个新的状态{@link BluetoothProfile#STATE_DISCONNECTED} or 即表示当前的状态
* {@link BluetoothProfile#STATE_CONNECTED}
因此使用newState参数来做判断
*/
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
//连接成功 因为是异步调用的 所以刷新UI的操作要放在主线程中,当然也可以使用hanlder Eventbus等 随便
runOnUiThread(new Runnable() {
@Override
public void run() {
connectTv.setText("连接成功");
}
});
//链接成功
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
//断开连接
runOnUiThread(new Runnable() {
@Override
public void run() {
connectTv.setText("连接断开");
}
});
}
}
在上面的代码中发现当连接成功后调用了gatt.discoverServices();
方法,这个方法是用来发现远程设备提供的服务,以及它们包含的特征特性和描述符等等。
因为根据前面的介绍可以知道,要想app
和蓝牙模块进行通讯,需要通过这些服务和特征等。这个方法会触发回调onServicesDiscovered()
:
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (BluetoothGatt.GATT_SUCCESS == status) {
gatt.getServices();
/*
一个GATT服务表现为一个 BluetoothGattService 对象,我们需要通过适当的UUID从 BluetoothGatt 实例中获得;
一个GATT特征表示为一个 BluetoothGattCharacteristic 对象,我们可以通过适当的UUID从 BluetoothGattService 中得到;
相当于一个数据类型,它包括一个value和0~n个value的描述(BluetoothGattDescriptor)
一个GATT描述符表现为一个 BluetoothGattDescriptor 对象,我们可以通过适当的UUID从BluetoothGattCharacteristic 对象中获得:
描述符,对Characteristic的描述,包括范围、计量单位等
*/
gattCharacteristicList.clear();
String uuid = null;
ArrayList> gattServiceData = new ArrayList>();
ArrayList>> gattCharacteristicData = new ArrayList>>();
//获取服务
for (BluetoothGattService gattService : gatt.getServices()) {
HashMap currentServiceData = new HashMap();
uuid = gattService.getUuid().toString();
currentServiceData.put("name",
SampleGattAttributes.lookup(uuid, "未知服务"));
currentServiceData.put("uuid", uuid);
gattServiceData.add(currentServiceData);
ArrayList> gattCharacteristicGroupData = new ArrayList>();
List gattCharacteristics = gattService
.getCharacteristics();
ArrayList charas = new ArrayList();
// 获取每个服务中包含的特征
for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
charas.add(gattCharacteristic);
HashMap currentCharaData = new HashMap();
uuid = gattCharacteristic.getUuid().toString();
currentCharaData.put("name",
SampleGattAttributes.lookup(uuid, "未知特征"));
currentCharaData.put("uuid", uuid);
//当某个特征的UUID为0000ff02-0000-1000-8000-00805f9b34fb时 使能BLE透传模块通知功能
if (uuid.equals("0000ff02-0000-1000-8000-00805f9b34fb")) {
setCharacteristicNotification(gattCharacteristic, true);
}
gattCharacteristicGroupData.add(currentCharaData);
}
gattCharacteristicList.add(charas);
gattCharacteristicData.add(gattCharacteristicGroupData);
}
// 用一个可折叠的列表来展示 这些服务和 特征
final SimpleExpandableListAdapter gattServiceAdapter = new SimpleExpandableListAdapter(
BLEDeviceTestActivity.this, gattServiceData,
android.R.layout.simple_expandable_list_item_2, new String[]{
"name", "uuid"}, new int[]{android.R.id.text1,
android.R.id.text2}, gattCharacteristicData,
android.R.layout.simple_expandable_list_item_2, new String[]{
"name", "uuid"}, new int[]{android.R.id.text1,
android.R.id.text2});
runOnUiThread(new Runnable() {
@Override
public void run() {
listView.setAdapter(gattServiceAdapter);
}
});
}
}
上面一大堆代码,就是把获取到的服务和特征用一个列表展示出来并不是关键,因为根据前面的characteristic
说明可以看到0xff02开头的特征值是使能通知,所以有下面代码:
if (uuid.equals("0000ff02-0000-1000-8000-00805f9b34fb")) {
setCharacteristicNotification(gattCharacteristic, true);
}
setCharacteristicNotification()
方法如下是启用一个指定特征的通知权限。这里被小小的坑了一下,本来以为bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
设置true
就够了的,但是调试的时候发现不能够接收到通知,查阅了资料才知道还需要给描述符设置通知权限启用才行,但是具体哪个描述符就不知道了,最后还是根据蓝牙模块厂家发来的demo反编译,查看了其源码才知道相应的UUID
。而且对比发现此描述符的UUID
就是以0x2902开头的,由此猜测这个此描述符应该是GATT
协议中已经写死的一个描述符。
public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) {
//仅仅有这一句是不够的
bluetoothGatt.setCharacteristicNotification(characteristic, enabled);
//需要为指定特征的特定的描述符设置启用才行
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID
.fromString(CLIENT_CHARACTERISTIC_CONFIG));
if (descriptor != null) {
System.out.println("write descriptor");
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
bluetoothGatt.writeDescriptor(descriptor);
}
}
当设置了通知以后,如果BLE
设备通过通知的方式发送数据的话,app
端接到通知会触发onCharacteristicChanged()
方法,此方法就可以用来接收BLE
模块通过广播形式给返回的:
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(BLEDeviceTestActivity.this, "接收到数据", Toast.LENGTH_SHORT).show();
}
});
//以字节码数组的形式接收到数据
final byte[] data = characteristic.getValue();
if (data != null && data.length > 0) {
final StringBuilder stringBuilder = new StringBuilder(
data.length);
StringBuffer test = new StringBuffer();
for (byte byteChar : data) {
test.append(byteChar);
stringBuilder.append(String.format("%02X ", byteChar));//以两位16进制输出 不足的补0
}
runOnUiThread(new Runnable() {
@Override
public void run() {
//数据展示
dataTv.setText(new String(data) + "\n"
+ stringBuilder.toString());
}
});
}
}
根据前面的介绍知道蓝牙的透传是通过characteristics
进行的,所以当然还有读和写的操作,在进行操作之前需要先对服务的UUID进行判断,
可以根据characteristic.getUuid()
来得到UUID
,或者在知道UUID
的情况下,主动获取对应的特征进行操作。
进行读操作如下,触发回调函数onCharacteristicRead():
//对相应的特征进行读操作
bluetoothGatt.readCharacteristic(characteristic);
//读取成功触发回调函数
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
//读取到的数据存在characteristic当中,可以通过characteristic.getValue();函数取出。然后再进行解析操作。
if (BluetoothGatt.GATT_SUCCESS == status) {
final byte[] data = characteristic.getValue();
...
//和通知一样也是通过字节码的形式传递数据 这里省略不写
}
}
同样既然有了读,也可以进行写的操作,写操作如下,会触发回调函数onCharacteristicWrite()
:
//这里写入需要是字节码数组的形式来进行
characteristic.setValue(byte[] value);
bluetoothGatt.writeCharacteristic(characteristic);
//这里没什么好说的,就是判断写入是否成功
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
if (BluetoothGatt.GATT_SUCCESS == status) {
Log.d("BLEDeviceTestActivity", "写入成功");
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(BLEDeviceTestActivity.this, "写入成功", Toast.LENGTH_SHORT).show();
}
});
}
如果写入成功,并且透传模块有应答,一般的应答方式是通过通知的方式进行的。所以一般在项目中在链接成功以后就要开启通知的权限,省的后面写入的时候忘记。至此,手机和BLE
模块之间的通讯基本完成,其他的写入描述符这些回调过程大同小异。
当然,这只是简单的完成了通讯,真正使用的时候由于数据包大小的限制还需要分包传输,接受等等等等很多需要完善的地方。