首页 / 学习路径 / 第 06 章 · 蓝牙 BLE 手机控制
06
进阶 · 无线

蓝牙 BLE 手机控制

GATT 服务与特征值、读写与通知回调、手机调试工具实操。不需要路由器,手机就是你的遥控器。

阅读时长 约 50 分钟硬件 3 件实战案例 1 个前置要求 第 5 章
1

本章要点

BLE 采用 GATT 客户端-服务器模型:手机是中心设备(客户端),ESP32 是外设(服务器)。它不用配对,靠广播让手机发现自己。

特征值可以理解成「一根数据通道」,上面挂着三个动作:读(Read)、写(Write)、通知(Notify)。setValue() 更新数据、notify() 主动推送,两者要配合使用。

断开连接后必须重新 startAdvertising(),否则手机再也扫不到设备。调试优先用 nRF Connect 这类通用工具,能排除 App 自身的问题。

2

硬件清单与原理

名称数量作用与原理
ESP32 DevKit V11 块内置 BLE 5.0 射频。与 WiFi 可共存,但同时全速工作会互相影响。
手机1 台需安装 nRF Connect(安卓/iOS 均可)或 LightBlue。
LED + 220Ω 电阻1 套被控对象。板载 LED 已接 GPIO2,可直接复用。
3

接线说明

接线示意 · BLE 仅需供电,手机即为遥控器 ESP32 DevKit V1 BLE 5.0 GPIO2 板载 LED ON / OFF 手机 · nRF Connect BLE 广播 · ESP32_LED GATT 模型:手机是客户端(中心),ESP32 是服务器(外设)
BLE 无需外接接线。LED 可用板载 GPIO2。

USB 数据线 → ESP32 → 电脑(供电与烧录)

板载 LED 已接 GPIO2,用作被控对象

① 烧录后用 nRF Connect 扫描,找到设备名 ESP32_LED

② 连接后找到特征值,写入 ON 或 OFF 并发送

提示:BLE 与 WiFi 可以共存,但同时全速工作会互相影响。

4

实战案例 06-1 · BLE 手机控 LED

功能用途

用手机连接 ESP32,写入 ON / OFF 控制 LED,并订阅通知查看返回状态。

预期现象

手机扫描到 ESP32_LED 设备,连接后写入字符串即可控制 LED。

ch06_ble.ino编译通过
/*
 * ============================================================================
 * 第 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 任务自行处理,
  // 应用层不需要写任何轮询代码,这里留空即可。
}
完整源码 157 行 · ch06_ble/ch06_ble.inoarduino-cli 编译通过
5

本章小结

① BLE 采用 GATT 客户端-服务器模型:手机是中心设备,ESP32 是外设。

② 特征值像一根数据通道,挂着读、写、通知三个动作。

③ setValue() 更新数据、notify() 主动推送,两者要配合使用。

④ 断开连接后必须重新 startAdvertising(),否则手机再也扫不到。

⑤ 调试时优先用 nRF Connect 这类通用工具,能排除 App 自身问题。

6

本章练习

练习 1

增加第二个特征值,用来读取 ESP32 的运行时间。

练习 2

往特征值里写 SPEED 500,实现手机端调节闪烁速度。

练习 3

把 setPower 改成 ESP_PWR_LVL_P3,观察广播距离变化。

练习 4

故意不调 notify(),观察手机端能否感知状态变化。

7

下一章

07
NEXT · 第 07 章

综合项目:温湿度监测站

DHT11 采集 + OLED 显示 + WiFi 上报 + 超温告警。把前六章的能力全部拧成一个能真正用的桌面设备。

开始学习