本章要点
BLE 采用 GATT 客户端-服务器模型:手机是中心设备(客户端),ESP32 是外设(服务器)。它不用配对,靠广播让手机发现自己。
特征值可以理解成「一根数据通道」,上面挂着三个动作:读(Read)、写(Write)、通知(Notify)。setValue() 更新数据、notify() 主动推送,两者要配合使用。
断开连接后必须重新 startAdvertising(),否则手机再也扫不到设备。调试优先用 nRF Connect 这类通用工具,能排除 App 自身的问题。
硬件清单与原理
| 名称 | 数量 | 作用与原理 |
|---|---|---|
| ESP32 DevKit V1 | 1 块 | 内置 BLE 5.0 射频。与 WiFi 可共存,但同时全速工作会互相影响。 |
| 手机 | 1 台 | 需安装 nRF Connect(安卓/iOS 均可)或 LightBlue。 |
| LED + 220Ω 电阻 | 1 套 | 被控对象。板载 LED 已接 GPIO2,可直接复用。 |
接线说明
USB 数据线 → ESP32 → 电脑(供电与烧录)
板载 LED 已接 GPIO2,用作被控对象
① 烧录后用 nRF Connect 扫描,找到设备名 ESP32_LED
② 连接后找到特征值,写入 ON 或 OFF 并发送
提示:BLE 与 WiFi 可以共存,但同时全速工作会互相影响。
实战案例 06-1 · BLE 手机控 LED
用手机连接 ESP32,写入 ON / OFF 控制 LED,并订阅通知查看返回状态。
手机扫描到 ESP32_LED 设备,连接后写入字符串即可控制 LED。
/* * ============================================================================ * 第 6 章 · 实战案例 06-1:BLE 蓝牙手机控制 LED * ---------------------------------------------------------------------------- * 硬件清单: * - ESP32 DevKit V1 × 1 * - USB 数据线 × 1 * - LED × 1 + 220Ω 电阻 × 1 * * 接线方式: * GPIO2 → [220Ω] → LED 阳极,LED 阴极 → GND * * 蓝牙原理速览: * ESP32 同时支持「经典蓝牙(BR/EDR)」和「低功耗蓝牙(BLE)」。 * 经典蓝牙像「蓝牙音箱」,设备之间成对通信; * BLE 则采用「GATT 客户端-服务器」模型:一个中心设备(手机)去扫描、 * 连接外设(ESP32),然后读写外设上的「特征值(Characteristic)」。 * 特征值里通常挂了三个动作:读(Read)、写(Write)、通知(Notify)。 * 本案例就是:手机写特征值 → ESP32 收到 → 控制 LED; * ESP32 写特征值 → 手机订阅通知后能实时看到状态。 * * 功能用途: * 用手机(推荐 nRF Connect / LightBlue / ESP32 BLE 调试助手)连接 ESP32, * 写入 class="st">"ON" / class="st">"OFF" 控制 LED,并订阅通知查看 ESP32 返回的状态。 * * 预期现象: * 串口打印 BLE 广播信息;手机能扫描到 class="st">"ESP32_LED" 设备, * 连接后能看到一个特征值,写入字符串即可控制 LED。 * * ⚠ 注意事项: * - BLE 与 WiFi 不能同时全速工作,但可以共存(本案例只开 BLE,不开 WiFi) * - 部分安卓机需要定位权限才能扫描 BLE * ============================================================================ */ #include <Arduino.h> // Arduino 核心库 #include <BLE2902.h> // BLE 规范要求的描述符类 #include <BLEDevice.h> // BLE 设备管理类 #include <BLEServer.h> // BLE 服务端类 #include <BLEUtils.h> // 工具函数 #include <BLEScan.h> // 扫描相关 /* ========================================================================== * 一、引脚与常量 * ======================================================================== */ #define LED_PIN 2 // BLE 的服务 UUID 和特征值 UUID 用 128 位表示。 // 这里使用 ESP32 示例里的标准测试 UUID,作用相当于「服务编号」和「数据通道号」。 #define SERVICE_UUID "6f8f0a1e-3b1a-4f2c-9c8b-2d4e6f8a0b1c" #define CHARACTERISTIC_UUID "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" bool ledState = false; // LED 当前状态 // 全局指针:用来在回调函数里引用服务与特征值对象 BLEServer* bleServer = class="kw">nullptr; BLEService* bleService = class="kw">nullptr; BLECharacteristic* bleChar = class="kw">nullptr; /* ========================================================================== * 二、回调:写入特征值时触发 * 手机往特征值里写什么,这里就执行什么。 * ======================================================================== */ class MyServerCallbacks : class="kw">public BLEServerCallbacks { void onConnect(BLEServer* server) class="kw">override { Serial.println(class="st">"[BLE] 客户端已连接"); } void onDisconnect(BLEServer* server) class="kw">override { Serial.println(class="st">"[BLE] 客户端已断开,重启广播以便再次连接"); bleServer->startAdvertising(); // 断开后重新开始广播,方便手机再次发现 } }; /* ========================================================================== * 三、回调:写入特征值 * ======================================================================== */ class MyCharacteristicCallbacks : class="kw">public BLECharacteristicCallbacks { void onWrite(BLECharacteristic* characteristic, esp_ble_gatts_cb_param_t* param) class="kw">override { // 从写入的参数里取出字符串内容 class="kw">String value = characteristic->getValue().c_str(); Serial.print(class="st">"[BLE] 收到写入数据:"); Serial.println(value); if (value == class="st">"ON" || value == class="st">"on") { ledState = true; } else if (value == class="st">"OFF" || value == class="st">"off") { ledState = false; } else if (value == class="st">"TOGGLE" || value == class="st">"toggle") { ledState = !ledState; } else { Serial.println(class="st">"[BLE] 未识别的指令,仅支持 ON / OFF / TOGGLE"); class="kw">return; // 未知指令直接返回,不改变状态 } // 真正驱动硬件 digitalWrite(LED_PIN, ledState ? LOW : HIGH); Serial.print(class="st">"[BLE] LED 状态已更新为:"); Serial.println(ledState ? class="st">"ON" : class="st">"OFF"); // 主动把新状态通过「通知」推给订阅了通知的手机 class="kw">String stateStr = ledState ? class="st">"ON" : class="st">"OFF"; bleChar->setValue(stateStr.c_str()); // 先更新特征值的内容 bleChar->notify(); // 再发送通知 } }; /* ========================================================================== * 四、初始化 * ======================================================================== */ void setup() { Serial.begin(115200); delay(200); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); // 初始熄灭 Serial.println(); Serial.println(class="st">"[BLE] 正在初始化 BLE ..."); // 创建设备,名字会显示在手机的扫描列表里 BLEDevice::init(class="st">"ESP32_LED"); BLEDevice::setPower(ESP_PWR_LVL_P9); // 设置发射功率(越大覆盖越远,也越费电) // 创建服务端 bleServer = BLEDevice::createServer(); bleServer->setCallbacks(new MyServerCallbacks()); // 注册连接/断开回调 // 创建一个服务,里面会放特征值 bleService = bleServer->createService(SERVICE_UUID); // 创建特征值,属性包括:可读 + 可写 + 可通知 bleChar = bleService->createCharacteristic( CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_NOTIFY); // 为可写特征值注册回调,收到写入时执行 onWrite bleChar->setCallbacks(new MyCharacteristicCallbacks()); // 给特征值一个初始值,手机连上后读到的就是这个 bleChar->setValue(class="st">"OFF"); // 启动服务并开始广播 bleService->start(); BLEDevice::startAdvertising(); Serial.println(class="st">"[BLE] 初始化完成,正在广播设备名:ESP32_LED"); Serial.println(class="st">"[BLE] 请用 nRF Connect / LightBlue 扫描并连接"); } /* ========================================================================== * 五、主循环 * ======================================================================== */ void loop() { // BLE 的协议栈由底层 FreeRTOS 任务自行处理, // 应用层不需要写任何轮询代码,这里留空即可。 }
本章小结
① BLE 采用 GATT 客户端-服务器模型:手机是中心设备,ESP32 是外设。
② 特征值像一根数据通道,挂着读、写、通知三个动作。
③ setValue() 更新数据、notify() 主动推送,两者要配合使用。
④ 断开连接后必须重新 startAdvertising(),否则手机再也扫不到。
⑤ 调试时优先用 nRF Connect 这类通用工具,能排除 App 自身问题。
本章练习
增加第二个特征值,用来读取 ESP32 的运行时间。
往特征值里写 SPEED 500,实现手机端调节闪烁速度。
把 setPower 改成 ESP_PWR_LVL_P3,观察广播距离变化。
故意不调 notify(),观察手机端能否感知状态变化。