前言
前面我们已经了解了蓝牙ble的基本流程,主要分为:扫描发现蓝牙设备、解析蓝牙广播包、建立GATT连接、mtu协商 发现服务、打开服务、进行通信、断开连接。
上篇已经重点讲解了蓝牙ble的扫描和广播解析,本次我们介绍一下蓝牙的连接过程,对于通信过程我们后面再详细讲解。
蓝牙ble连接概念
当目标设备已经被我们用app发现的时候,我们就需要和蓝牙设备建立连接才能通信,建立连接我们通常会先打开设备的GATT连接,那么什么是GATT?为了说明GATT的本质,我们先了解下ATT。
ATT
ATT:Attribute Protocol。它是一种属性协议,它定义了设备如何被发现,以及设备的读写规则。因此,ATT是一种协议规范。(我们上篇提到的蓝牙广播和扫描也是遵循了ATT的协议的,之所以不在上一篇文章提主要是为了突出扫描和广播的的重点)。
ATT定义设备作为服务端(终端设备)提供拥有关联值的属性集(属性集组成了一个集合服务)让作为客户端的设备(手机/平板)来发现、读、写这些属性;同时服务端能主动通知客户端。
ATT定义了两种角色: 服务端(Server)和客户端(Client)
ATT中的属性包含下面三个内容:
diff
- Attribute Type : 由UUID(Universally Unique IDentifier)来定义
- Attribute Handle : 用来访问Attribute Value
- A set of Permissions : 控制该Attribute是否可读、可写、属性值是否通过加密链路发送
GATT

GATT
GATT(Generic Attribute Profile)是Bluetooth Low Energy(BLE)协议的一部分,它是基于ATT的。它定义了设备如何传输和接收数据。他是一种通讯协议的通用数据格式,在设备端,存储了一份名为gatt profile的文件来存储数据。app和设备的进行通信,本质上就是进行GATT的数据交换。
GATT使用的是客户端(APP)-服务器(Device)架构。在这种架构中,服务器设备拥有可供其他设备访问的数据(称为特性,Characteristics)。客户端设备可以读取、写入或者订阅这些数据的变化。
一个GATT服务包含一个或多个特性,每个特性都有一个唯一的UUID(Universally Unique Identifier)用于标识。每个特性也可以包含一个或多个描述符(Descriptors),用于描述特性的属性。
例如,一个心率监测设备可能提供一个心率服务,这个服务包含一个特性,用于报告当前的心率。手机(作为客户端设备)可以连接到这个设备,读取心率特性的值,或者订阅心率的变化。
MTU
MTU,全称为Maximum Transmission Unit,是数据通信中的一个重要概念,指的是一次可以传输的最大数据单元(单位通常是用:Byte)。在蓝牙通信中,MTU大小决定了一次可以发送或接收的最大字节数。
蓝牙的MTU大小可以在连接建立后进行协商。默认的MTU大小为23字节,但是在许多现代设备中,MTU可以被协商到更大的值,从而提高数据传输效率。我们通常所说的最大payload指的就是mtu。在蓝牙4.0的时候,默认的mtu是23个字节,但是具体的mtu为多少,很大程度取决于设备端的芯片。
Service
顾名思义,指代的是服务。例如:
- 健康服务(Health Service):提供心率、血压等特征。
- 天气服务(Weather Service):提供温度、湿度、天气等特征。
- 基础服务(Basic Service):电量、开关等服务。
- 状态服务(Status Service):设备处于待配网、设备处于已配网等等
以下是通过nRF展示的已经连接的设备的提供的services

Services是由
Character:
特征值。他不会独立存在,肯定是依托于某一个Services。他往往代表一个设备特征值,例如电量、开关、亮度、心率等等都是设备等特征,也可以理解为属性。
Descriptor:
是对特征值(Character)的一种描述。例如,我们有一个特征值是心率。那么心率的描述就是:心率是指心脏在一定时间内跳动的次数,通常以每分钟的心跳次数(BPM,即每分钟的跳动次数)来计算。心率是衡量心脏健康和身体活动水平的重要指标。
小结
蓝牙通信主要是通过gatt制定的数据通用格式进行的,这些数据的通用格式是由一个或多个services组成的,其中一个services又由一个或多个character组成的。因此,蓝牙的物理连接主要就是为了打开设备的gatt通道。 这才具备了后续进行通信的充分必要条件。
Android 连接实战
首先,经过上一篇文章的了解,我们已经通过扫描,拿到了蓝牙的扫描结果ScanResult
,接下来,我们使用android API来打开GATT通道。
连接API
java
package android.bluetooth;
public final class BluetoothDevice implements Parcelable, Attributable {
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback, int transport,
boolean opportunistic, int phy, Handler handler){...}
}
蓝牙ble连接的connectGatt是存在多个重载的。我们这里直接介绍参数最多的,因为其余的函数最终还是走这里的。好了。废话不多说,我们来直接看下连接的参数分别有什么作用;
-
context(Context)
:android赖以生存的上下文。 -
autoConnect(Boolean)
:如果此参数为 true,系统会在蓝牙设备可用时自动尝试连接。如果为 false,系统会立即尝试连接。 -
callback (BluetoothGattCallback)
: 用于接收异步操作的结果,如连接状态改变和服务发现等。 -
transport
参数用于指定用于此连接的传输类型。取值可以是以下几种:-
TRANSPORT_AUTO (int): 这是默认的传输层模式。系统会自动根据设备和可用的硬件选择最佳的传输模式。
-
TRANSPORT_BREDR (int): 该模式指定使用基本速率/增强数据速率 (BR/EDR) 传输,这是传统的蓝牙连接方式,通常用于较远的距离和较高的数据传输速率。
-
TRANSPORT_LE (int): 该模式指定使用低功耗 (LE) 传输,这是一种针对短距离、低数据传输速率和低功耗的蓝牙连接方式。
选择哪种传输模式取决于你的应用需要和远程设备的支持。例如,如果你正在开发一个需要长时间运行且电池寿命重要的应用,那么可能会选择
TRANSPORT_LE
。相反,如果你需要在较远的距离传输大量数据,那么可能会选择TRANSPORT_BREDR
。 -
-
BluetoothGattCallback
是一个抽象类,用于接收来自BluetoothGatt
对象的异步操作的回调,这个回调十分重要,后续所有和设备相关的回调都会通过该回调返回结果。以下是一些主要的回调方法及其含义:-
onConnectionStateChange(BluetoothGatt gatt, int status, int newState): 当远程设备的连接状态发生更改时,会回调此方法。
-
onServicesDiscovered(BluetoothGatt gatt, int status): 当远程设备的服务被发现时,会回调此方法。
-
onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status): 当读取特性操作完成时,会回调此方法。
-
onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status): 当写入特性操作完成时,会回调此方法。
-
onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic): 当启用特性通知并且特性的值发生更改时,会回调此方法。
-
onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status): 当读取描述符操作完成时,会回调此方法。
-
onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status): 当写入描述符操作完成时,会回调此方法。
-
onReliableWriteCompleted(BluetoothGatt gatt, int status): 当可靠写入操作完成时,会回调此方法。
-
onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status): 当读取远程设备的 RSSI 完成时,会回调此方法。
-
onMtuChanged(BluetoothGatt gatt, int mtu, int status): 当 MTU 更改时,会回调此方法。
-
以上每个方法的 status
参数都是一个表示操作成功或失败的状态代码。如果操作成功, status
将是 BluetoothGatt.GATT_SUCCESS
。否则,它将是一个错误代码。
调用示例
kotlin
fun openGatt(scanResult: ScanResult){
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
// TODO: No permission
return
}
// 打开gatt连接
val bleGatt = scanResult.device.connectGatt(context, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt?, status: Int,
newState: Int
) {
// 设备连接状态回调
// 具体参数含义见下文
}
})
}
这个回调方法 onConnectionStateChange
在 BluetoothGattCallback
中用于处理与远程设备的连接状态改变相关的事件。以下是每个参数的详细解释:
-
gatt: BluetoothGatt? - 这是当前连接的 GATT 客户端。你可以使用这个对象来读取、写入、和接收特性的通知。
-
status: Int - 这是连接或操作的状态。如果连接或操作成功,状态将是
BluetoothGatt.GATT_SUCCESS
。否则,它将是一个错误代码,表示连接或操作失败的原因。 -
newState: Int - 这是设备的新连接状态。可能的值有:
-
BluetoothProfile.STATE_CONNECTED
(值为 2)表示设备已连接。 -
BluetoothProfile.STATE_DISCONNECTED
(值为 0)表示设备已断开连接。 -
BluetoothProfile.STATE_CONNECTING
(值为 1)表示设备正在连接。 -
BluetoothProfile.STATE_DISCONNECTING
(值为 3)表示设备正在断开连接。
-
当设备的连接状态发生更改时(例如,从未连接状态变为已连接状态),将调用此回调方法。
Bluetooth Gatt的状态
java
public final class BluetoothGatt implements BluetoothProfile {
private static final int CONN_STATE_IDLE = 0; // 初始化
private static final int CONN_STATE_CONNECTING = 1; // 正在连接
private static final int CONN_STATE_CONNECTED = 2; // 已经连接
private static final int CONN_STATE_DISCONNECTING = 3;// 正在断连
private static final int CONN_STATE_CLOSED = 4;// 蓝牙GATT已关闭
断开连接和关闭资源
android的断开链接和关闭资源是两个不同的API,他们分别是
csharp
class BluetoothDevice{
public void disconnect();
}
和
csharp
class BluetoothGatt{
public void close();
}
那这两个方法有什么作用呢?前者仅仅是断开和蓝牙设备的连接。后者是关闭的整个蓝牙在系统内的资源占用。因此,如果有些场景,需要暂时断开蓝牙,后续你是需要重新连接的,那么我们仅调用BluetoothDevice#disconnect
即可,如果我们不再使用该蓝牙资源了,那么在调用disconnect之后,必要调用调用BluetoothGatt#close
来关闭资源。
打开Gatt通道之后
当我们调用了gatt连接之后,并且设备的newState
为BluetoothProfile.STATE_CONNECTED
,按理来说我们应该已经可以通信了。
我们前面说过,连接的目的是为了通信,通信的数据是为了交换service-character的数据。所以在正式通信之前,我们还得保证我们要通信的service是存在的。
因此在真正进行通信之前,mtu协商、发现服务、打开服务也成了通信之前的必要准备。
mtu 协商
后面我们来看看mtu协商的API
java
package android.bluetooth;
public final class BluetoothGatt implements BluetoothProfile {
// 和设备进行mtu的协商
public boolean requestMtu(int mtu){...}
...
}
API很简单,就是通过上述连接成功拿到的BluetoothGatt
对象直接调用requestMtu(int mtu)
即可。mtu协商的结果是通过BluetoothGattCallback#onMtuChanged(BluetoothGatt gatt, int mtu, int status)
返回的,这个比较简单,就不再多做叙述了。
发现服务
发现服务就是查看,当前设备提供的服务,有没有符合我们要求的service。
发现服务的service API如下:
java
package android.bluetooth;
public final class BluetoothGatt implements BluetoothProfile {
// 发现设备里面所有的服务
public boolean discoverServices();
// 发现制定UUID的服务
public boolean discoverServiceByUuid(UUID uuid);
...
}
结果是通过BluetoothGattCallback#onServicesDiscovered(BluetoothGatt gatt, int status)
返回的,这个比较简单,就不再多做叙述了。
打开服务
我们在发现服务之后,就需要打开特定服务的character的通道,来确保我们的数据通道是正常可用的。
less
package android.bluetooth;
public final class BluetoothGatt implements BluetoothProfile {
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
public boolean setCharacteristicNotification(BluetoothGattCharacteristic characteristic,boolean enable) {}
结果是通过BluetoothGattCallback#onCharacteristicChanged(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value)
返回的。
BluetoothGattCharacteristic
是根据Service和Character确定下来的。 在 Bluetooth GATT 中,每个服务、特性和描述符都由一个 UUID 来唯一标识。这些 UUID 可以是标准的,也可以是自定义的。
以下是一些标准的 UUID 示例:
- 心率服务:
0000180d-0000-1000-8000-00805f9b34fb
- 心率测量特性:
00002a37-0000-1000-8000-00805f9b34fb
- 设备名称特性:
00002a00-0000-1000-8000-00805f9b34fb
这些 UUID 都是按照 Bluetooth SIG 提供的规范定义的。
如果你正在开发自己的设备和服务,你可能需要自定义 UUID。自定义 UUID 通常是随机生成的,例如:
- 自定义服务:
12345678-1234-5678-1234-567812345678
- 自定义特性:
9abcdef0-1234-5678-1234-567812345678
请注意,自定义 UUID 应该避免与标准 UUID 冲突,并且在同一设备或服务中应该是唯一的。
有了Service和Character,我们就可以通过以下方式得到BluetoothGattCharacteristic
kotlin
val characteristic : BluetoothGattCharacteristic? = gatt?.services?.filter {
it.uuid == myService // 比较uuid的内容
}?.get(0)?.getCharacteristic(myCharacteristic)
使用注意点
我们发现,上面的mtu协商、发现服务、打开服务的函数返回值都是boolean,包括后面通信要说的writeXXX()的方法。那么这个boolean的值代表什么含义呢?这个比较好理解,首先,蓝牙的通信其实本质上和HTTP是一样的,是一种异步IO。我们看下从我们调用以上API到设备收到数据再回复消息的过程是怎么样的。

上述的过程有点像寄快递一样,只不过这个快递需要一个回执。
- 首先我们调用系统的API将数据给到系统。(将我们的快递给到快递公司)
- 系统通过手机上的蓝牙驱动将数据发送到外界。(快递公司将快递的打包之后装车开始运输),然后告诉我们包裹已经顺利寄出去了
- 外界的蓝牙目标设备发现这个是发送给他的消息开始处理(收件人收到快递)
- 最后处理的结果返回给手机
上述的boolean变量就代表了我们发送的数据是否正确发出,但是不代表设备是否有收到(丢数据就跟丢包裹一样)。
对于蓝牙连接的使用建议
蓝牙的连接是比较耗费资源的。因此,我们在实际开发当中势必要对蓝牙的连接进行一个管理。如果管理不当,内存泄漏、资源浪费、耗费电量等等都是会影响我们app的性能和用户体验的。
使用单例模式统一管理所有的连接资源
kotlin
object BleConnectManager {
// 定义连接任务对象
data class ConnectModule(
val gatt : BluetoothGatt,
val gattCallback: BluetoothGattCallback,
val bluetoothDevice: BluetoothDevice
)
// 用于管理所有已经连接的gatt,K = mac, V = BluetoothGatt
private val gattMap : MutableMap<String, ConnectModule> = ConcurrentHashMap()
// 根据mac的物理地址获取已存在的gatt,若无则返回空
fun obtainBluetoothGatt(mac : String) : ConnectModule? = gattMap[mac]
// 获取一个Gatt
fun getBluetoothGatt(context : Context, autoConnect: Boolean,device: BluetoothDevice) : ConnectModule{
var connectModule = obtainBluetoothGatt(device.address)
if (connectModule != null) {
return connectModule
}
val gattCallback = object : BluetoothGattCallback() {
// ... 省略
}
val connectGatt = device.connectGatt(context, autoConnect, gattCallback)
// 存储gatt
connectModule = ConnectModule(
gatt = connectGatt,
gattCallback = gattCallback,
bluetoothDevice = device
)
gattMap[device.address] = connectModule
return connectModule
}
// 断开连接
fun disconnect(mac : String){
gattMap[mac]?.disconnect()
}
// 断开并且释放gatt资源
fun disconnectAndCloseGatt(mac : String){
gattMap.remove(mac)?.let {
it.disconnect()
it.close()
}
}
}
以上的代码仅仅用来表达对于连接资源管理的重要性,实际代码当中,需要根据业务需求进行改造。
通信
发送数据(Write)
蓝牙写数据的API:
java
public final class BluetoothGatt implements BluetoothProfile{
public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic)
}
使用示例:
kotlin
// 获取要通信的characteristic
val characteristic : BluetoothGattCharacteristic?
= gatt?.services?.filter {
it.uuid == myService // 比较uuid的内容 }?
.get(0)?.getCharacteristic(myCharacteristic)
// 准备好下发的数据
val data : ByteArray? = assemableData()
// 设置数据
characteristic?.setValue(data)
// 发送数据
val sendResult = bluetoothGatt.writeCharacteristic(characteristic)
结果是通过BluetoothGattCallback#onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
返回的,这个比较简单,就不再多做叙述了。
读取数据(Read)
蓝牙读数据的API:
java
public final class BluetoothGatt implements BluetoothProfile{
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic)
}
使用示例:
kotlin
// 获取要通信的characteristic
val characteristic : BluetoothGattCharacteristic?
= gatt?.services?.filter {
it.uuid == myService // 比较uuid的内容 }?
.get(0)?.getCharacteristic(myCharacteristic)
// 发送数据
val sendResult = bluetoothGatt.readCharacteristic(characteristic)
结果是通过BluetoothGattCallback#onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value, int status)
返回的,这个比较简单,就不再多做叙述了。
数据的吞吐量
我们知道,数据的发送和接收都伴随着处理的。手机的数据处理目前的性能是比较过剩的,我们这里讨论的主要是设备接收的。通常情况下,设备的处理是FIFO的队列形式,如果我们发送的数据超过数据的吞吐量,那么会导致数据处理不过来。因此,最好在正式通信之前,先和设备进行吞吐量的协商,通常是决定1s内,设备能处理多少的数据包。
总结
以上就是蓝牙的连接和通信相关的分享了。下一篇我们来聊聊,app发送蓝牙广播包要如何使用?