4. 蓝牙连接实战:ESP32 BLE 从机与手机主机开发
这一章,我们直接动手。我会带你从零搭建一个完整的 BLE 通信链路:ESP32 做从机(Server),手机 App 做主机(Client)。数据怎么发、怎么收,Notification 和 Write 到底怎么用,咱们一次说透。
4.1 整体架构与角色分工
先搞清楚谁该干什么。BLE 通信里,角色分工很明确:
| 角色 | 设备 | 职责 | 关键操作 |
|---|---|---|---|
| GATT Server(从机) | ESP32 | 暴露 Service 和 Characteristic,等待连接 | 广播、响应读写、发送 Notification |
| GATT Client(主机) | Android / iOS 手机 | 扫描、连接、读写数据 | 扫描设备、发现服务、Write / 订阅 Notification |
说白了,ESP32 就是那个“摆摊”的,把数据摆出来;手机是“逛街”的,看到感兴趣的就去读写。我早期做项目时,总把两边逻辑搞混,后来养成了一个习惯:先画数据流图,再写代码。下面这张图,就是咱们这章的核心逻辑。
4.2 ESP32 从机(Server)开发
ESP32 这边,我们用 ESP-IDF 的 GATT Server API。核心就三步:初始化、建表、发通知。
4.2.1 创建 GATT 数据库
你需要定义 Service 和 Characteristic。我习惯用一个结构体数组来声明:
// 定义 Service 和 Characteristic 的 UUID
#define SERVICE_UUID 0xFFE0
#define CHARACTERISTIC_TX_UUID 0xFFE1 // 用于 Notification
#define CHARACTERISTIC_RX_UUID 0xFFE2 // 用于 Write
// 属性配置
static const esp_gatts_attr_db_t gatt_db[] = {
// Service Declaration
[IDX_SVC] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
sizeof(uint16_t), sizeof(SERVICE_UUID), (uint8_t *)&SERVICE_UUID}},
// TX Characteristic Declaration
[IDX_CHAR_TX] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
CHAR_DECL_SIZE, CHAR_DECL_SIZE, (uint8_t *)&char_prop_notify}},
// TX Characteristic Value
[IDX_CHAR_VAL_TX] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&CHARACTERISTIC_TX_UUID, ESP_GATT_PERM_READ,
GATTS_CHAR_VAL_LEN_MAX, 0, NULL}},
// RX Characteristic Declaration
[IDX_CHAR_RX] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
CHAR_DECL_SIZE, CHAR_DECL_SIZE, (uint8_t *)&char_prop_write}},
// RX Characteristic Value
[IDX_CHAR_VAL_RX] =
{{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&CHARACTERISTIC_RX_UUID,
ESP_GATT_PERM_WRITE, GATTS_CHAR_VAL_LEN_MAX, 0, NULL}},
};
4.2.2 处理 GATT 事件回调
ESP-IDF 用回调机制驱动。你需要关注这几个事件:
- ESP_GATTS_CONNECT_EVT:客户端连接成功,记录连接句柄
- ESP_GATTS_WRITE_EVT:收到手机发来的数据(Write)
- ESP_GATTS_CONF_EVT:Notification 发送确认
我贴一段核心的 Write 事件处理:
static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_WRITE_EVT: {
// 收到手机发来的数据
uint8_t *data = param->write.value;
uint16_t len = param->write.len;
ESP_LOGI(TAG, "收到数据: %.*s", len, (char *)data);
// 在这里解析指令,比如控制 GPIO
if (data[0] == 0x01) {
gpio_set_level(LED_PIN, 1);
} else if (data[0] == 0x00) {
gpio_set_level(LED_PIN, 0);
}
break;
}
// ... 其他事件
}
}
4.2.3 发送 Notification
ESP32 主动推数据给手机,用 esp_ble_gatts_send_indicate 函数。注意第二个参数传 false 表示 Notification(不需要确认),传 true 表示 Indication(需要确认)。
// 发送 Notification
esp_ble_gatts_send_indicate(gatts_if, conn_id,
char_handle_tx, // TX Characteristic 的句柄
data_len,
data, // 要发送的数据
false); // false = Notification
4.3 手机端主机(Client)开发
手机端,Android 用 BluetoothGatt,iOS 用 CoreBluetooth。流程是一样的:
- 扫描:找到广播中的 ESP32
- 连接:建立 GATT 连接
- 发现服务:获取 Service 和 Characteristic
- 订阅 Notification:使能 TX Characteristic 的 CCCD
- Write:向 RX Characteristic 写数据
4.3.1 Android 端关键代码(Kotlin)
Android 的 BLE API 有点啰嗦,但核心逻辑很清晰。我直接给关键片段:
// 订阅 Notification
fun enableNotification(gatt: BluetoothGatt, char: BluetoothGattCharacteristic) {
val success = gatt.setCharacteristicNotification(char, true)
// 还要写 CCCD 描述符
val descriptor = char.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
// 发送 Write 指令
fun sendCommand(gatt: BluetoothGatt, char: BluetoothGattCharacteristic, cmd: ByteArray) {
char.value = cmd
gatt.writeCharacteristic(char)
}
setCharacteristicNotification 和 writeDescriptor 必须都调用,Notification 才能生效。我见过不少新手只调了第一个,结果死活收不到数据。
4.3.2 iOS 端关键代码(Swift)
iOS 的 CoreBluetooth 相对优雅一些:
// 订阅 Notification
func enableNotification(for characteristic: CBCharacteristic) {
peripheral.setNotifyValue(true, for: characteristic)
}
// 发送 Write
func sendData(_ data: Data, to characteristic: CBCharacteristic) {
peripheral.writeValue(data, for: characteristic, type: .withResponse)
}
iOS 的 writeValue 有 .withResponse 和 .withoutResponse 两种模式。前者会等 ESP32 确认,后者只管发。我建议用 .withResponse,至少你能知道数据有没有发出去。
4.4 数据收发实战:Notification vs Write
这两个操作是 BLE 通信的基石。我整理了一个对比表:
| 特性 | Notification | Write |
|---|---|---|
| 方向 | 从机 → 主机 | 主机 → 从机 |
| 是否需要主机确认 | 否(无应答) | 可选(有/无应答) |
| 典型应用 | 传感器数据推送、状态更新 | 控制指令、参数配置 |
| 最大数据长度 | 20 字节(MTU=23 时) | 20 字节(MTU=23 时) |
| 可靠性 | 低(可能丢包) | 高(有应答时) |
4.5 常见问题与避坑指南
- 手机扫描不到 ESP32:检查广播是否开启,广播间隔是否太长。我一般设 100ms。
- Notification 收不到:确认 CCCD 描述符是否已写。Android 上尤其容易漏这一步。
- Write 没反应:检查 Characteristic 属性是否包含
WRITE权限。ESP32 端要配ESP_GATT_PERM_WRITE。 - 数据乱码:字节序问题。ESP32 是小端,手机也是小端,一般没问题。但如果你传结构体,注意对齐。
ESP_GATT_PERM_READ。嗯,这种低级错误,犯过一次就记住了。