title: 蓝牙通信
author: wucm
tags:
- Android
categories: - Android
Android手机间通过蓝牙方式进行通信,有两种常见的方式,一种是socket方式,另一种是通过Gatt Server(Android 5.0以后)通信,socket方式最为简单,但是很多低功耗的蓝牙设备,如单片机上的蓝牙模块可能不支持;而Gatt方式相对比较复杂。其实无论是socket方式还是Gatt,Android设备间蓝牙通信都是一种C/S(client-server)模式。
本文基于两种通信方式,进行详细展开,并推荐了开源项目,建议配合学习。
前置知识
特征
蓝牙4.0是以参数来进行数据传输的,即服务端定好一个参数,客户端可以对这个参数进行读,写,通知等操作,这个东西我们称之为特征值(characteristic)。比如我们这个特征值是电量的值,另一个特征值是设备读取的温度值。BLE 主从机的通信均是通过 Characteristic 来实现。每个特征有唯一UUID。
属性
包含 特征的声明(Characteristic Declaration)条目、特征的值(Characteristic Value)条目。一个特性至少包含2个属性条目(也即属性值必须要有,而描述符根据需要可选):一个属性条目用于声明(Characteristic Declaration),一个属性条目用于存放特性的值(Characteristic Value)。
- Read: 读属性。具有这个属性的特征是可读的,也就是说这个属性允许手机来读取一些信息,手机可以发送指令来读取某个具有读属性UUID的信息。
- Notify: 通知属性。具有这个属性的特征是可以发送通知的,也就是说具有这个属性的特征可以主动发送信息给手机。
- Write: 写属性。具有这个属性的特征是可以接收写入数据的,通常手机发送数据给蓝模块就是通过这个属性完成的。这个属性在Write 完成后,会发送写入完成结果的反馈给手机,然后手机再写入下一包或处理后续业务。这个属性在写入一包数据后,需要等待应用层返回写入结果,速度比较慢。
- WriteWithout Response:写属性。从字面意思上看,只是写,不需要返回写的结果,这个属性的特点是不需要应用层返回,完全依靠协议层完成,速度快,但是写入速度超过协议处理速度的时候会丢包。
描述符
包含 特征的客户配置(Client Characteristic Configuration)条目、特征的用户描述(Characteristic User Description)条目。描述符是一个额外的属性以提供更多特性的信息,它提供一个人类可识别的特性描述的实例。每个描述符有唯一UUID。
服务
特征值有很多,我们需要对特征值进行分类,分出来的类我们称之为服务(service)。每个服务有唯一UUID。
GATT
GATT(Generic Attribute Profile):中文名叫通用属性协议,它定义了services和characteristic两种东西来完成低功耗蓝牙设备之间的数据传输。它是建立在通用数据协议Attribute Protocol (ATT)之上的,ATT把services和characteristic以及相关的数据保存在一张简单的查找表中,该表使用16-bit的id作为索引。
profile
profile可以理解为一种规范,一个标准的通信协议,它存在于从机中。蓝牙组织规定了一些标准的profile,例如 HID OVER GATT ,防丢器 ,心率计等。每个profile中会包含多个service,每个service代表从机的一种能力。
开发流程
下面说一下GATT的实现,也就是我们常说的BLE蓝牙开发,这种方式也可以运行在传统蓝牙上。
1.申请权限
这里不讲如何动态申请权限,如果你想轻装上阵,声明权限后,自己手动去授权即可
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!--目标设备版本是 Android9 及其以上的系统上,需要申请此权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--目标设备版本是 Android12 及其以上的系统上,扫描蓝牙设备需要申请此权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!--目标设备版本是 Android12 及其以上的系统上,使当前设备可被其他蓝牙设备检测到需要申请此权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!--目标设备版本是 Android12 及其以上的系统上,与已配对的蓝牙设备通信需要申请此权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
2.判断是否支持蓝牙功能
我们可以通过 getDefaultAdapter 来获取该实例。
private BluetoothAdapter bleAdapter = BluetoothAdapter.getDefaultAdapter();
假如 bleAdapter 为 null 就意味着设备不支持蓝牙。
3.打开蓝牙
private void openBle() {
//通过isEnable() 判断蓝牙是否打开
if (!bleAdapter.isEnabled()) {
//如果没有打开,打开蓝牙
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
return;
}
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
}
在上面的内容都准备好了,那就可以开始进入核心流程了。
4.开始扫描
private BluetoothLeScanner bleScanner = bleAdapter.getBluetoothLeScanner();
private void startScan() {
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_SCAN) != PERMISSION_GRANTED) {
return;
}
bleScanner.startScan(new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//连接目标设备
connectDevice(result);
}
});
}
5.连接目标设备
扫描到设备后,找到目标设备就可以开始连接了。
private BluetoothGatt bluetoothGatt;
private void connectDevice(ScanResult result) {
BluetoothDevice device = result.getDevice();
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED
&& !device.getName().matches("蓝牙通配符")) {
return;
}
bluetoothGatt = device.connectGatt(this, true, new BluetoothGattCallback() {
//Gatt连接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
//连接目标设备成功
if (newState == BluetoothGatt.STATE_CONNECTED) {
//搜索服务
discoverServices(gatt);
}
}
});
}
BluetoothGatt可以理解为与其他设备沟通交流的对象。
6.搜索服务
private void discoverServices(BluetoothGatt gatt) {
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
return;
}
//搜索服务
gatt.discoverServices();
}
好了,现在我们看看执行完 bluetoothGatt.discoverServices() 又发生了什么变化吧。
//BluetoothGattCallback.java
//发现服务回调
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//服务发现结束
if (status == BluetoothGatt.GATT_SUCCESS) {
findNotifyCharacteristics(gatt);
}
}
7.打开通知通道
假设已经跟硬件端确认好了所需信息(服务 uuid,特征uuid*,描述符 uuid ),这时,确实可以开始写入消息。但是,我们却得不到回应,因为我们没有打开通知通道。
开启监听,即建立与设备的通信的数据通道,BLE开发中只有当上位机成功开启监听后才能与下位机收发数据。开启监听的方式如下:
//获取到该设备的【通知通道】的特征值
private void findNotifyCharacteristics(BluetoothGatt gatt) {
List<BluetoothGattCharacteristic> notifyCharacteristics = new ArrayList<>();
for (BluetoothGattService service : gatt.getServices()) {
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
//两个都是通知的意思,notify和indication的区别在于
//notify只是将你要发的数据发送给手机,没有确认机制,不会保证数据发送是否到达。
//而indication的方式在手机收到数据时会主动回一个ack回来。即有确认机制,只有收到这个ack你才能继续发送下一个数据。这保证了数据的正确到达,也起到了流控的作用。所以在打开通知的时候,需要设置一下。
if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0
|| (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
notifyCharacteristics.add(characteristic);
}
}
}
for (BluetoothGattCharacteristic notifyCharacteristic : notifyCharacteristics) {
//打开通知通道
openNotifyChannel(gatt, notifyCharacteristic, true);
}
}
//打开通知通道
private void openNotifyChannel(BluetoothGatt gatt, BluetoothGattCharacteristic notifyCharacteristic, boolean enabled) {
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
return;
}
//step1:打开通知通道
gatt.setCharacteristicNotification(notifyCharacteristic, enabled);
List<BluetoothGattDescriptor> descriptors = notifyCharacteristic.getDescriptors();
if (descriptors.isEmpty()) {
return;
}
for (BluetoothGattDescriptor descriptor : descriptors) {
if (descriptor == null) {
break;
}
//step2:为描述符设置对应的属性
if ((notifyCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
descriptor.setValue(enabled ? ENABLE_NOTIFICATION_VALUE : DISABLE_NOTIFICATION_VALUE);
} else if ((notifyCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
descriptor.setValue(enabled ? ENABLE_INDICATION_VALUE : DISABLE_NOTIFICATION_VALUE);
}
//step3:向设备写入上述的描述符
gatt.writeDescriptor(descriptor);
}
}
打开通知通道就可以写入数据了👇
8.写入数据
private void write(BluetoothGatt gatt) {
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
return;
}
//具体用哪个服务特征,需要跟硬件端确认
UUID serviceUUID= UUID.fromString(SERVIC_UUID);
UUID writerUUID = UUID.fromString(WRITER_UUID);
//step1:获取到写通道的特征值
BluetoothGattCharacteristic characteristic = gatt.getService(serviceUUID).getCharacteristic(writerUUID);
//step2:把我们要发送的信息存在上述特征值中
characteristic.setValue("QCoder,U are so handsome.".getBytes());
//step3:向设备写入上面的特征值
gatt.writeCharacteristic(characteristic);
}
这样,对方就收到我们的消息了。如果你发的消息,对方想回复(提前协商好的指令)。那么我们将会在下面这个方法中收到
//BluetoothGattCallback.java
//特性改变回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
//拿到传回来的原始数据 byte数组
byte[] content = characteristic.getValue();
}
9.读取数据
private void read(BluetoothGatt gatt) {
//具体用哪个服务特征,需要跟硬件端确认
UUID serviceUUID= UUID.fromString(SERVIC_UUID);
UUID readerUUID = UUID.fromString(READER_UUID);
BluetoothGattCharacteristic characteristic = gatt.getService(serviceUUID).getCharacteristic(readerUUID);
if (ActivityCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED) {
return;
}
gatt.readCharacteristic(characteristic);
}
读取的特征值将通过下面方法收到
//BluetoothGattCallback.java
//特性读取回调
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
//拿到传回来的原始数据 byte数组
byte[] content = characteristic.getValue();
}
10.分包
1.设置MTU(单次数据包的最大字节数),值越大传输效率越高,但相对的延迟也高,如果传输大数据可以设置的大一点。
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.requestMtu(517); // 请求最大MTU值
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// MTU修改成功,获取实际的MTU值。个值可能是请求的值,也可能是设备支持的最大值
actualMtu = mtu;
}
}
2.发送时根据MTU进行分包,并在每个数据包的前几个字节添加总包数和当前包数。
public List<byte[]> splitData(byte[] data, int mtu) {
int headerSize = 4; // 总包数和当前包数占用4个字节
int maxPacketSize = mtu - headerSize;
List<byte[]> packets = new ArrayList<>();
int totalPackets = (data.length + maxPacketSize - 1) / maxPacketSize;
for (int i = 0; i < totalPackets; i++) {
int start = i * maxPacketSize;
int length = Math.min(maxPacketSize, data.length - start);
byte[] packet = new byte[headerSize + length];
// 添加总包数
packet[0] = (byte) ((totalPackets >> 8) & 0xFF);
packet[1] = (byte) (totalPackets & 0xFF);
// 添加当前包数
packet[2] = (byte) ((i >> 8) & 0xFF);
packet[3] = (byte) (i & 0xFF);
// 添加数据
System.arraycopy(data, start, packet, headerSize, length);
packets.add(packet);
}
return packets;
}
3.接收时进行合包
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
int totalPackets = (value[0] << 8) | (value[1] & 0xFF);
int currentPacket = (value[2] << 8) | (value[3] & 0xFF);
byte[] data = new byte[value.length - 4];
System.arraycopy(value, 4, data, 0, data.length);
// 判断重组数据是否完成
// ...
}
补充
BluetoothGattCallback方法介绍:
| 方法 | 描述 |
|---|---|
| onPhyUpdate | 物理层改变回调 |
| onPhyRead | 设备物理层读取回调 |
| onConnectionStateChange | Gatt连接状态回调 |
| onServicesDiscovered | 发现服务回调 |
| onCharacteristicRead | 特性读取回调 |
| onCharacteristicWrite | 特性写入回调 |
| onCharacteristicChanged | 特性改变回调 |
| onDescriptorRead | 描述读取回调 |
| onDescriptorWrite | 描述写入回调 |
| onReliableWriteCompleted | 可靠写入完成回调 |
| onReadRemoteRssi | 读取远程设备信号值回调 |
| onMtuChanged | MtuSize改变回调 |
| onConnectionUpdated | 连接更新回调 |
415

被折叠的 条评论
为什么被折叠?



