ESP32 实战教程:用 BLE GATT 做手机温湿度查看与 LED 控制
本文由 AI 协助整理。示例以 Arduino-ESP32 BLE API 为主,芯片型号、蓝牙栈和库版本可能影响接口。BLE 连接不等于安全认证,涉及门锁、继电器和隐私数据时必须增加配对、加密和权限控制。

先把 BLE 说成人话
经典蓝牙像一条可以持续通话的串口线,BLE 更像一套有目录的公告栏。Service 是目录,Characteristic 是目录下的条目,每个条目可以具备读取、写入或通知属性。
本项目创建一个环境服务:手机可以读取温湿度,也可以订阅温度和湿度通知;另有一个 LED 控制特征值,手机写入 ON 或 OFF 后,ESP32 执行并更新状态。
这个项目的重点不是把 UUID 抄到能编译,而是理解四个边界:服务负责组织功能,特征值负责数据,通知要先订阅,写入命令必须校验。手机工具能发现设备,只能说明广告包出现了,不能证明 GATT 业务已经正确。
硬件与工具
- 支持 BLE 的 ESP32 芯片。
- 一个温湿度传感器,可先使用固定模拟值。
- 板载 LED 或低功率 LED。
- 手机 BLE 调试工具,例如 nRF Connect 一类工具。
ESP32 经典型号支持 BLE,但 ESP32-C3、S3 等芯片的蓝牙能力与经典 ESP32 不完全相同。本文使用 BLE,不依赖蓝牙经典串口;选板时先确认 Arduino-ESP32 的 BLE 支持矩阵。
设计 UUID 和数据格式
学习阶段可以使用自定义 UUID:
#define SERVICE_UUID "7d2a0001-3b7a-4c18-9f42-123456789001"
#define TEMP_UUID "7d2a0002-3b7a-4c18-9f42-123456789001"
#define HUM_UUID "7d2a0003-3b7a-4c18-9f42-123456789001"
#define LED_UUID "7d2a0004-3b7a-4c18-9f42-123456789001"
温湿度通知使用文本,例如 24.6 和 53.1,便于手机工具直接查看。量产设备可以改用定长二进制,减少无线开销,但要在文档中明确字节序、缩放系数和版本。
UUID 稳定后不要频繁改。手机端可能缓存旧服务,调试时遇到特征值不更新,可以关闭蓝牙、重新扫描或清除工具缓存,而不是立刻怀疑 UUID 又被某个小数点改变了。
创建 BLE 服务和特征值
#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
BLECharacteristic* tempChar = nullptr;
BLECharacteristic* humChar = nullptr;
BLECharacteristic* ledChar = nullptr;
bool deviceConnected = false;
bool ledOn = false;
class ServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer*) override {
deviceConnected = true;
Serial.println("BLE connected");
}
void onDisconnect(BLEServer* server) override {
deviceConnected = false;
Serial.println("BLE disconnected");
delay(100);
server->startAdvertising();
}
};
初始化:
void setupBle() {
BLEDevice::init("ESP32-Env-01");
BLEServer* server = BLEDevice::createServer();
server->setCallbacks(new ServerCallbacks());
BLEService* service = server->createService(SERVICE_UUID);
tempChar = service->createCharacteristic(
TEMP_UUID, BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY);
humChar = service->createCharacteristic(
HUM_UUID, BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY);
ledChar = service->createCharacteristic(
LED_UUID, BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE);
tempChar->addDescriptor(new BLE2902());
humChar->addDescriptor(new BLE2902());
tempChar->setValue("--");
humChar->setValue("--");
ledChar->setValue("OFF");
service->start();
BLEAdvertising* advertising = BLEDevice::getAdvertising();
advertising->addServiceUUID(SERVICE_UUID);
advertising->setScanResponse(true);
BLEDevice::startAdvertising();
}
BLE2902 是客户端配置描述符,手机工具通过它打开或关闭通知。只设置 PROPERTY_NOTIFY 而没有描述符,部分工具能看到特征值,却找不到可操作的通知开关。
处理写入命令
class LedCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* characteristic) override {
std::string value = characteristic->getValue();
if (value == "ON") {
ledOn = true;
digitalWrite(LED_BUILTIN, HIGH);
} else if (value == "OFF") {
ledOn = false;
digitalWrite(LED_BUILTIN, LOW);
} else {
Serial.println("Unknown LED command");
return;
}
characteristic->setValue(ledOn ? "ON" : "OFF");
Serial.printf("LED=%s\n", ledOn ? "ON" : "OFF");
}
};
在 setupBle() 中为 LED 特征值注册回调:
ledChar->setCallbacks(new LedCallbacks());
不要直接把任意收到的字符串当成 GPIO 编号或 PWM 数值执行。写入值要有白名单、范围和失败响应。实际控制继电器时,必须在上电时设为安全状态,并在断开连接后决定是否自动关闭。
发送读取值和通知
void publishEnvironment() {
float temperature = 24.6f; // 替换为真实传感器读数
float humidity = 53.2f;
if (!isfinite(temperature) || !isfinite(humidity)) return;
char tempText[16];
char humText[16];
snprintf(tempText, sizeof(tempText), "%.1f", temperature);
snprintf(humText, sizeof(humText), "%.1f", humidity);
tempChar->setValue(tempText);
humChar->setValue(humText);
if (deviceConnected) {
tempChar->notify();
humChar->notify();
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
setupBle();
}
void loop() {
static unsigned long lastPublish = 0;
if (millis() - lastPublish >= 2000) {
lastPublish = millis();
publishEnvironment();
}
delay(10);
}
读取属性让手机主动取值,通知则由设备主动推送。通知频率不宜高于传感器真正更新的频率,否则只是把同一数字反复塞进无线链路。
手机端验证步骤
- 烧录程序,打开串口确认设备开始广播。
- 手机打开 BLE 工具,扫描
ESP32-Env-01。 - 找到自定义 Service UUID。
- 读取温度和湿度特征值,确认文本格式。
- 打开通知,等待数值周期变化。
- 向 LED 特征值写入 UTF-8 文本
ON,观察 LED。 - 写入
OFF,确认状态恢复。 - 关闭手机蓝牙,再打开,确认设备重新广告。
注意 BLE 工具的写入模式:有的工具默认按十六进制发送,输入 ON 可能变成错误字节。先确认是文本模式,再验证设备日志。
连接状态和功耗
BLE 连接时设备会保持更多无线活动,电流通常高于只做周期广播。若项目只需要单向上报,可以让设备周期广播制造商数据,手机扫描后读取,不必一直维持连接。
连接断开后重新开始广播是最基本的恢复动作。若传感器需要长期运行,应把采样周期、通知周期和连接超时分开,不要让手机是否在线决定传感器是否工作。
常见问题
| 现象 | 优先检查 |
|---|---|
| 手机搜不到设备 | BLE 是否初始化、是否开始广播、芯片能力 |
| 能看到设备但看不到服务 | Service UUID、服务是否 start() |
| 特征值能读但无通知 | BLE2902、手机是否开启 notify |
| 写入没有效果 | 写入类型、大小写、回调是否注册 |
| 断开后不再出现 | onDisconnect() 是否重启广告 |
| BLE 初始化崩溃 | 与 Matter 或底层 BLE API 内存配置冲突 |
安全与扩展
BLE 特征值默认不是“只有你能写”。附近的人可能扫描到设备并尝试写控制特征。需要安全控制时,应启用配对和加密,限制可写特征,增加设备身份和命令确认。不要把门锁、加热器等高风险输出接到一个裸写特征上。
后续可以把温湿度特征改成标准 GATT UUID,让通用健康或环境工具识别;也可以增加电池电量、设备版本、采样周期配置,以及 OTA 前的 BLE 配网。功能越多,越要保持 UUID、数据格式和权限表的文档。
总结
BLE GATT 项目的核心是把功能拆成服务与特征值:读用于查询,通知用于推送,写用于命令。手机能发现设备只是第一步,真正可用还要处理通知订阅、重连、数据格式和写入校验。
先用文本值把协议跑通,再接入真实传感器和安全控制。这样既能快速看到结果,也不会把蓝牙协议、传感器错误和执行器风险一次揉成一团。
参考资料
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号