跳到主要内容

蓝牙 (Bluetooth)

ESP32 系列芯片内置了强大的蓝牙功能,使其成为智能穿戴、无线传感和设备间近场通信的理想选择。蓝牙技术分为两种主要类型:

  • 经典蓝牙 (Bluetooth Classic):为持续性、高吞吐量的数据传输设计,常见于无线音频设备。
  • 低功耗蓝牙 (Bluetooth Low Energy, BLE):专为低功耗、间歇性、小数据包通信而优化,是物联网(IoT)应用的主流选择,如智能手环、无线传感器。

ESP32 产品概览

ESP32 芯片的蓝牙支持情况有所不同:经典的 ESP32 芯片同时支持经典蓝牙和 BLE;而后续的新型号则专注于支持 BLE,以优化成本和功耗(具体支持情况请查看:ESP32 产品概览)。在物联网、可穿戴设备等领域,BLE 因其低功耗和高兼容性成为首选。

本教程将聚焦于低功耗蓝牙(BLE)技术的应用。

BLE 基础概念

BLE 通信基于以下两个核心:

  • GAP(Generic Access Profile) - 通用访问规范:描述设备广播、发现、连接的规则。例如,ESP32 广播自己的存在,手机进行扫描和连接。
  • GATT(Generic Attribute Profile)- 通用属性规范:定义 BLE 设备之间的数据结构和通讯方式。GATT 层由“服务(Service)”和“特征(Characteristic)”组成,每个数据项都有唯一的 UUID。

GAP(Generic Access Profile)

GAP 负责管理设备的连接和广播,并定义了设备在蓝牙通信中的角色。

GAP 定义了两种主要角色:

  • 外围设备 (Peripheral): 通常是拥有数据的设备,如传感器。它通过广播(Advertising)来宣告自身存在,等待被连接。在示例中,ESP32 将主要扮演此角色。
  • 中央设备 (Central): 通常是功能更强大的设备,如智能手机或电脑。它通过扫描(Scanning)来发现外围设备,并发起连接。

GAP 关系

GAP 通过以下过程实现设备间的交互:

  • 广播 (Advertising): 外围设备周期性地发送广播包,其中包含设备名称、服务 UUID 等信息,以便中央设备发现。
  • 扫描 (Scanning): 中央设备监听广播信道,接收并解析来自外围设备的广播包。
  • 建立连接 (Connecting): 中央设备向其选定的外围设备发起连接请求,一旦外围设备接受,两者便建立起一对一的连接。

GATT(Generic Attribute Profile)

GATT (Generic Attribute Profile) 在设备建立连接后生效,它定义了数据交换的框架和格式。GATT 基于一种客户端-服务器(Client-Server)架构。这两个角色通常与 GAP 角色直接对应:

  • GATT 服务器 (Server): 即拥有数据的设备(通常对应 GAP 的外围设备),它存储并提供数据。
  • GATT 客户端 (Client): 即访问数据的设备(通常对应 GAP 的中央设备),它向服务器发送读/写请求。

GATT 中的数据以一种标准化的层级结构组织:

GATT 层次

  • 服务 (Service) 一个服务是多个相关“特征”的逻辑集合,代表设备的一项功能。每个服务由一个唯一的 UUID 标识。例如,“电池服务 (Battery Service)”可能包含一个“电量水平 (Battery Level)”特征。

  • 特征 (Characteristic) 特征是数据交换的基本单元,封装了一个具体的数据值。一个完整的特征包含:

    • 值 (Value): 存储的实际数据。
    • 属性 (Properties): 定义客户端可对“值”执行的操作,常见的有:
      • Read: 允许客户端读取值。
      • Write: 允许客户端写入值。
      • Notify: 允许服务器在值改变时,主动将新值发送给客户端。
      • Indicate: 与 Notify 类似,但需要客户端确认收到。
    • 声明 (Declaration): 包含特征的属性、UUID 和在服务中的位置。
  • 描述符 (Descriptor) 描述符是可选的,它为特征提供附加的元数据(metadata)。例如,它可以用来提供一个人类可读的描述(如 "Temperature Measurement")、指明值的单位(如 "Celsius")或定义一个有效的数值范围。

  • UUID (Universally Unique Identifier) UUID 是用于唯一标识服务、特征和描述符的 128 位数字。为了方便,蓝牙技术联盟 (SIG) 为通用功能预定义了一套官方的短 UUID(通常为 16 位),例如 0x180F 代表电池服务。当开发自定义应用时,应使用随机生成的完整 128 位 UUID,以确保全局唯一性。所有已分配的标准 UUID 可在 SIG 官网 查询。

示例 1:向蓝牙发送数据(外围设备)

本示例将 ESP32 配置为外围设备,读取电位器的模拟值,并通过一个 BLE 特征 (Characteristic) 将其发布。可以使用手机 App (如 LightBlue) 作为中央设备连接 ESP32,并读取该特征的值。

搭建电路

需要使用的器件有:

  • 电位器 * 1
  • 面包板 * 1
  • 导线
  • ESP32 开发板

按照下面接线图连接电路:

ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图

代码

提示

为了确保 BLE 设备的唯一性,最佳实践是使用您自己生成的 UUID,而不是直接复制教程中的。您可以使用在线工具(如 Online UUID Generator)来生成新的 UUID。

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// 全局变量
BLEServer *pServer = NULL;
BLECharacteristic *pPotentiometerCharacteristic = NULL;
bool deviceConnected = false;

// 电位器相关的定义
const int POTENTIOMETER_PIN = 7; // 电位器连接到 GPIO 7
uint16_t lastPotValue; // 用于存储上一次的电位器读数 (0-4095)

// 为服务和特征定义 UUID
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define POTENTIOMETER_CHARACTERISTIC_UUID "1b9a473a-4493-4536-8b2b-9d4133488256"

// 服务器回调类,用于处理连接和断开事件
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) {
deviceConnected = true;
Serial.println("Device Connected");
}

void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
Serial.println("Device Disconnected, restarting advertising...");
// 当设备断开连接时,立即重新开始广播
pServer->getAdvertising()->start();
}
};

void setup() {
Serial.begin(115200);
Serial.println("Starting BLE Potentiometer example...");

// 初始化电位器输入
lastPotValue = analogRead(POTENTIOMETER_PIN);

// 1. 初始化 BLE 设备
BLEDevice::init("ESP32_Potentiometer");

// 2. 创建 BLE 服务器并设置回调
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

// 3. 创建 BLE 服务
BLEService *pService = pServer->createService(SERVICE_UUID);

// 4. 创建 BLE 特征
pPotentiometerCharacteristic = pService->createCharacteristic(
POTENTIOMETER_CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | // 可读
BLECharacteristic::PROPERTY_NOTIFY // 可订阅通知
);
pPotentiometerCharacteristic->addDescriptor(new BLE2902()); // 添加 2902 描述符,用于接收启用订阅的指令

// 设置初始值
pPotentiometerCharacteristic->setValue(lastPotValue);

// 5. 启动服务
pService->start();

// 6. 启动广播
pServer->getAdvertising()->start();
Serial.println("BLE Server started, waiting for a client connection...");
}

void loop() {
// 只有当设备连接时,才需要检查电位器状态
if (deviceConnected) {
uint16_t currentPotValue = analogRead(POTENTIOMETER_PIN);

// 为了防止模拟信号抖动导致的数据泛滥,只有当值变化超过一个阈值时才发送
if (abs(currentPotValue - lastPotValue) > 30) {
lastPotValue = currentPotValue;

// 更新特征的值并通过 notify 发送给客户端
pPotentiometerCharacteristic->setValue(currentPotValue);
pPotentiometerCharacteristic->notify();

Serial.print("Potentiometer value changed to: ");
Serial.println(currentPotValue);
}

delay(50);
}
}

代码解析

  • #include <BLE...>: 引入 ESP32 BLE 功能所需的核心库。

    • BLEDevice.h: 核心设备库,用于初始化 BLE 栈。
    • BLEServer.h: 用于创建 BLE 服务器(外围设备)。
    • BLEUtils.h: 提供 BLE 相关的辅助工具。
    • BLE2902.h: 提供对标准 0x2902 描述符的支持,这是客户端启用 NotifyIndicate 所必需的。
  • SERVICE_UUIDPOTENTIOMETER_CHARACTERISTIC_UUID

    128 位自定义 UUID,用于唯一标识服务和特征。实际项目中建议使用在线 UUID 生成器创建。

  • MyServerCallbacks 类: 继承自 BLEServerCallbacks。通过重写 onConnectonDisconnect 方法,可以定义当有客户端连接或断开时需要执行的动作。这里我们用它来更新 deviceConnected 标志位,并打印日志。当设备断开时自动重新开始广播。

  • setup() 函数:

    • BLEDevice::init("ESP32_Potentiometer");: 初始化 BLE 设备并设置设备名称,该名称在蓝牙扫描时显示。
    • pServer = BLEDevice::createServer();: 创建一个 GATT 服务器实例。
    • pServer->setCallbacks(...): 将服务器事件(连接/断开)与我们自定义的回调类关联起来。
    • pService = pServer->createService(...): 在服务器上创建一个服务,并指定其 UUID。
    • pPotentiometerCharacteristic = pService->createCharacteristic(...): 在服务中创建一个特征。第二个参数是特征的属性,这里 PROPERTY_READ 表示可读,PROPERTY_NOTIFY 表示支持通知。
    • pPotentiometerCharacteristic->addDescriptor(new BLE2902());: 为特征添加一个标准的 CCCD (Client Characteristic Configuration Descriptor)。客户端特征配置描述符,这是启用通知功能的必要组件。
    • pPotentiometerCharacteristic->setValue(...): 设置特征的初始值。
    • pService->start()pServer->getAdvertising()->start(): 依次启动服务和广播,使设备变得可见并可连接。
  • loop() 函数:

    • if (deviceConnected): 仅在有客户端连接时才执行逻辑,以节省资源。
    • if (abs(currentPotValue - lastPotValue) > 30): 这是一个简单的去抖和数据过滤逻辑。只有当电位器读数变化超过一个阈值时,才更新并发送数据,避免因模拟信号的微小抖动而频繁发送无效数据。
    • pPotentiometerCharacteristic->setValue(...): 更新特征的值。
    • pPotentiometerCharacteristic->notify(): 将更新后的值主动推送给所有已订阅此特征的客户端。

运行结果

提示

本示例需要使用蓝牙调试工具,如 LightBlue。iOS 用户可在 Apple Store 下载,安卓用户可在应用商店搜索 LightBlue 下载。

打开 LightBlue,按照下面步骤操作:

首先,搜索“ESP32”,找到“ESP32_Potentiometer”设备并点击“Connect”连接。在设备详情页中,找到特征,可以看到已开启可读和可订阅功能,然后点击进入。点击右上角的“HEX”设置数据类型,以便后续查看数据。

设置“Byte Limit”为 2,并选择“2 Byte Unsigned Integer”,然后保存。保存后返回特征详情页,点击“Read”读取数据。转动电位器,再次读取即可看到变化。也可以点击“Subscribe”订阅数据,当转动电位器时数值会自动刷新。

示例 2:从蓝牙读取数据(外围设备)

本示例将 ESP32 配置为外围设备,创建一个可写的 BLE 特征。手机 App (如 LightBlue) 可向该特征写入特定值(如 0 或 1),以控制连接在 ESP32 上的 LED 的亮灭。

搭建电路

需要使用的器件有:

  • LED * 1
  • 330Ω 电阻 * 1
  • 面包板 * 1
  • 导线
  • ESP32 开发板

按照下面接线图连接电路:

ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图

代码

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// 全局变量
BLEServer *pServer = NULL;
BLECharacteristic *pLedCharacteristic = NULL;
bool deviceConnected = false;

// LED 相关的定义
const int LED_PIN = 7; // LED 连接到 GPIO 7
uint8_t ledState = 0; // 用于存储 LED 的状态 (0: OFF, 1: ON)

// 为服务和特性定义 UUID (请使用您自己生成的 UUID 以避免冲突)
#define SERVICE_UUID "48407a44-6e13-4d28-a559-210de862bc29"
#define LED_CHARACTERISTIC_UUID "539ca2ac-09e5-49be-90da-3b157549eac3"

// 服务器回调类,用于处理连接和断开事件
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) {
deviceConnected = true;
Serial.println("Device Connected");
}

void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
Serial.println("Device Disconnected, restarting advertising...");
// 当设备断开连接时,立即重新开始广播
pServer->getAdvertising()->start();
}
};

// 特性回调类,用于处理客户端的写入请求
class MyLedCallbacks : public BLECharacteristicCallbacks {
// 当客户端向该特性写入数据时,此函数被调用
void onWrite(BLECharacteristic *pCharacteristic) {
// 获取客户端发送过来的数据
String value_str = pCharacteristic->getValue();

if (value_str.length() > 0) {
// 我们只关心第一个字节
uint8_t command = value_str[0];

Serial.print("Received command: ");
Serial.println(command);

// 根据收到的命令控制 LED
if (command == 1) {
Serial.println("Turning LED ON");
digitalWrite(LED_PIN, HIGH);
ledState = 1;
} else if (command == 0) {
Serial.println("Turning LED OFF");
digitalWrite(LED_PIN, LOW);
ledState = 0;
} else {
Serial.print("Unknown command: ");
Serial.println(command);
}

// 更新特征值以反映当前 LED 状态,确保客户端读取时获得正确信息。
pLedCharacteristic->setValue(&ledState, 1);
}
}
};

void setup() {
Serial.begin(115200);
Serial.println("Starting BLE LED Control example...");

// 初始化 LED 引脚
pinMode(LED_PIN, OUTPUT);
ledState = 0;

// 1. 初始化 BLE 设备
BLEDevice::init("ESP32_LED_Control");

// 2. 创建 BLE 服务器并设置回调
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

// 3. 创建 BLE 服务
BLEService *pService = pServer->createService(SERVICE_UUID);

// 4. 创建 BLE 特性
pLedCharacteristic = pService->createCharacteristic(
LED_CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ | // 可读
BLECharacteristic::PROPERTY_WRITE // 可写
);

// 为特性设置写入回调
pLedCharacteristic->setCallbacks(new MyLedCallbacks());

// 设置特性的初始值 (LED 初始状态为 OFF)
pLedCharacteristic->setValue(&ledState, 1);

// 5. 启动服务
pService->start();

// 6. 启动广播
pServer->getAdvertising()->start();
Serial.println("BLE Server started, waiting for a client connection...");
}

void loop() {
// 主循环可以保持为空,因为所有的 BLE 事件都是通过回调函数异步处理的。
}

代码解析

  • MyLedCallbacks 类: 继承自 BLECharacteristicCallbacks。它专门用于处理与特定特征相关的事件。通过重写 onWrite 方法,可以定义当客户端向这个特征写入数据时需要执行的动作。

  • onWrite(BLECharacteristic *pCharacteristic):

    • String value_str = pCharacteristic->getValue();: 获取客户端写入的数据。数据以 std::string 的形式返回。
    • uint8_t command = value_str[0];: 从接收到的字符串中提取第一个字节作为命令。
    • digitalWrite(LED_PIN, ...): 根据命令控制 LED 的开关。
    • pLedCharacteristic->setValue(&ledState, 1);: 在处理完写入命令后,更新特征自身的值。这确保了当客户端执行读取操作时,能够获取到与 LED 物理状态一致的最新值。
  • setup() 函数:

    • pLedCharacteristic = pService->createCharacteristic(...): 创建特征时,属性设置为 PROPERTY_READ | PROPERTY_WRITE,表示该特征既可被客户端读取,也可被客户端写入。
    • pLedCharacteristic->setCallbacks(new MyLedCallbacks());: 将我们自定义的 MyLedCallbacks 实例与 LED 特征绑定。这样,任何对该特征的写入操作都会触发 onWrite 方法。

运行结果

提示

本示例需要使用蓝牙调试工具,如 LightBlue。iOS 用户可在 Apple Store 下载,安卓用户可在应用商店搜索 LightBlue 下载。

打开 LightBlue,按照下面步骤操作:

首先,搜索“ESP32”,找到“ESP32_LED_Control”设备并点击“Connect”连接。在设备详情页中,找到特征,可以看到已开启可读和可写功能,然后点击进入。点击右上角的“HEX”设置数据类型,以便后续查看数据。

设置“Byte Limit”为 1,并选择“1 Byte Unsigned Integer”,然后保存。保存后返回特征详情页,点击“Read”读取数据。默认值为 0,此时 LED 为熄灭状态。点击“Write new value”, 写入数值 1,LED 随即亮起。

示例 3:ESP32 间蓝牙通信

通过 BLE,用一个 ESP32 连接的电位器,控制另一个 ESP32 连接的 LED。

搭建电路

需要使用的器件有:

  • LED * 1
  • 330Ω 电阻 * 1
  • 电位器 * 1
  • 面包板 * 2
  • 导线
  • ESP32 开发板 * 2

按照下面接线图连接电路:

ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图

代码

ESP32 开发板 A 代码 (外围设备)

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>

// 全局变量
BLEServer *pServer = NULL;
BLECharacteristic *pBrightnessCharacteristic = NULL;

bool deviceConnected = false;
bool newDataAvailable = false;
uint8_t brightness = 0;

// LED 相关定义
const int LED_PIN = 7; // LED 连接到 GPIO 7

// 为服务和特征定义唯一的 UUID
#define SERVICE_UUID "458063a1-02bf-4664-857e-16c1030be066"
#define BRIGHTNESS_CHARACTERISTIC_UUID "a5209632-66a9-411d-9353-9be5507790fa"

// 服务器回调类,用于处理连接和断开事件
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) {
deviceConnected = true;
Serial.println("Client connected successfully");
}

void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
Serial.println("Client disconnected, restarting advertisement");
// 立即重新开始广播,以便客户端可以重新连接
pServer->getAdvertising()->start();
}
};

// 特征回调类,用于处理客户端的写入请求
class MyBrightnessCallbacks : public BLECharacteristicCallbacks {
// 当客户端向该特征写入数据时,此函数被调用
void onWrite(BLECharacteristic *pCharacteristic) {
String value_str = pCharacteristic->getValue();

if (value_str.length() > 0) {
// 从接收的数据中获取亮度值
brightness = value_str[0];
newDataAvailable = true;
}
}
};

void setup() {
Serial.begin(115200);
Serial.println("Starting ESP32 BLE LED Controller");

// 设置引脚为输出
pinMode(LED_PIN, OUTPUT);

// 1. 初始化 BLE 设备
BLEDevice::init("ESP32_LED");

// 2. 创建 BLE 服务器并设置回调
pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());

// 3. 创建 BLE 服务
BLEService *pService = pServer->createService(SERVICE_UUID);

// 4. 创建 BLE 特征
pBrightnessCharacteristic = pService->createCharacteristic(
BRIGHTNESS_CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_WRITE // 只允许写入
);

// 为特征设置写入回调
pBrightnessCharacteristic->setCallbacks(new MyBrightnessCallbacks());

// 5. 启动服务
pService->start();
Serial.println("BLE service started");

// 6. 启动广播
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pServer->getAdvertising()->start();

Serial.println("Advertisement started, ready for connections");
}

void loop() {
if (newDataAvailable) {
// 重置标志位,防止重复处理
newDataAvailable = false;

Serial.print("Brightness received: ");
Serial.println(brightness);

// 设置 LED 亮度
analogWrite(LED_PIN, brightness);
}
}

ESP32 开发板 B 代码 (中心设备)

#include <BLEDevice.h>

// 要连接的服务器的服务和特征 UUID (必须与服务器代码一致)
#define SERVICE_UUID "458063a1-02bf-4664-857e-16c1030be066"
#define BRIGHTNESS_CHARACTERISTIC_UUID "a5209632-66a9-411d-9353-9be5507790fa"

// 全局变量
static boolean doConnect = false;
static boolean connected = false;
static BLEAddress *pServerAddress;
static BLERemoteCharacteristic *pRemoteCharacteristic;

// 电位器相关定义
const int POTENTIOMETER_PIN = 7; // 电位器连接到 GPIO 7
uint8_t lastBrightness = 0; // 存储上一次发送的亮度值 (0-255)

class MyClientCallbacks : public BLEClientCallbacks {
void onConnect(BLEClient *pclient) {}

void onDisconnect(BLEClient *pclient) {
connected = false;
Serial.println("onDisconnect: Client Disconnected");
}
};

// 连接服务器的函数
bool connectToServer(BLEAddress pAddress) {
Serial.print("Connecting to ");
Serial.println(pAddress.toString().c_str());

BLEClient *pClient = BLEDevice::createClient();
Serial.println(" - Client created");

pClient->setClientCallbacks(new MyClientCallbacks());

// 连接到远程 BLE 服务器
if (!pClient->connect(pAddress)) {
Serial.println(" - Connection failed");
return false;
}
Serial.println(" - Connected to server");

// 获取服务器上的服务
BLERemoteService *pRemoteService = pClient->getService(SERVICE_UUID);
if (pRemoteService == nullptr) {
Serial.print("Failed to find service UUID: ");
Serial.println(SERVICE_UUID);
pClient->disconnect();
return false;
}
Serial.println(" - Service found");

// 获取服务中的特征
pRemoteCharacteristic = pRemoteService->getCharacteristic(BRIGHTNESS_CHARACTERISTIC_UUID);
if (pRemoteCharacteristic == nullptr) {
Serial.print("Failed to find characteristic UUID: ");
Serial.println(BRIGHTNESS_CHARACTERISTIC_UUID);
pClient->disconnect();
return false;
}
Serial.println(" - Characteristic found");

connected = true;
return true;
}

// 扫描回调类,当发现 BLE 设备时被调用
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
void onResult(BLEAdvertisedDevice advertisedDevice) {
// 找到设备,检查它是否包含正在寻找的服务。
if (advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID))) {
Serial.print("Found target server by Service UUID: ");
Serial.println(advertisedDevice.getAddress().toString().c_str());

// 停止扫描
advertisedDevice.getScan()->stop();

// 保存服务器地址,并设置连接标志
pServerAddress = new BLEAddress(advertisedDevice.getAddress());
doConnect = true;
}
}
};

void setup() {
Serial.begin(115200);
Serial.println("Starting BLE LED Brightness Controller (Client)...");

// 初始化 BLE 客户端
BLEDevice::init("");

// 获取扫描对象并设置回调
BLEScan *pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setActiveScan(true); // 主动扫描
pBLEScan->start(30, false); // 开始扫描,持续 30 秒
}

void loop() {
// 如果我们收到了连接指令并且尚未连接,则尝试连接
if (doConnect == true) {
if (connectToServer(*pServerAddress)) {
Serial.println("Successfully connected to the server!");
doConnect = false; // 清除连接指令
} else {
Serial.println("Failed to connect to the server. Rescanning after 3 seconds...");
delay(3000);
BLEDevice::getScan()->start(5, false); // 重新开始扫描 5 秒
}
}

// 如果已连接,则读取电位器并发送数据
if (connected) {
// 读取电位器的模拟值 (ESP32 ADC 是 12 位的,范围 0-4095)
int potValue = analogRead(POTENTIOMETER_PIN);

// 将 0-4095 的值映射到 0-255 的亮度范围
uint8_t brightness = map(potValue, 0, 4095, 0, 255);

// 只有当亮度值发生一定变化,以减少不必要的通信
if (abs(brightness - lastBrightness) > 2) {
Serial.print("Potentiometer value: ");
Serial.print(potValue);
Serial.print(" -> Sending brightness: ");
Serial.println(brightness);

// 将单个字节的亮度值写入服务器的特征
pRemoteCharacteristic->writeValue(&brightness, 1);

lastBrightness = brightness;
}

delay(100); // 每 100 毫秒检查一次
} else {
// 如果断开连接,则重新扫描
if (!doConnect) {
Serial.println("Disconnected. Rescanning...");
BLEDevice::getScan()->start(5, false);
}
}
}

代码解析

外围设备 (A - LED 端)

这段代码与示例 2 非常相似,但做了以下调整以适应新的场景:

  • 回调处理:在 MyBrightnessCallbacksonWrite 回调中,代码获取接收到的亮度值,并将其存储在全局变量 brightness 中,同时设置 newDataAvailable 标志位。

  • 主循环 loop()

    loop() 函数不再为空。它会检查 newDataAvailable 标志位。如果为真,打印接收到的数据,然后使用 analogWrite() 函数将接收到的亮度值设置给 LED,最后清除标志位。

    将数据接收(在回调函数中)与数据处理(在主循环中)分离,是一种常见的编程模式,有助于避免在回调函数中执行耗时操作,保持系统的响应性。

中央设备 (B - 电位器端)

这段代码展示了 ESP32 作为 BLE 中央设备的编程模型,逻辑与外围设备不同。

  • MyAdvertisedDeviceCallbacks 类: 继承自 BLEAdvertisedDeviceCallbacks。当扫描到任何 BLE 设备时,onResult 方法就会被调用。

  • if (advertisedDevice.isAdvertisingService(BLEUUID(SERVICE_UUID))):

    核心逻辑。通过检查广播包,筛选出包含目标服务 UUID 的设备,精确地找到要连接的设备。

  • advertisedDevice.getScan()->stop(): 找到目标后,立即停止扫描以节省功耗。

  • pServerAddress = new BLEAddress(...): 保存目标设备的地址,并设置 doConnect 标志位,通知主循环去执行连接操作。

  • connectToServer(BLEAddress pAddress) 函数:封装了完整的连接和发现过程。

    • BLEClient *pClient = BLEDevice::createClient();: 创建一个客户端实例。
    • pClient->connect(pAddress): 使用之前保存的地址发起连接。
    • pClient->getService(SERVICE_UUID): 连接成功后,获取远程服务器上的指定服务。
    • pRemoteService->getCharacteristic(...): 从服务中获取想要操作的远程特征。
  • setup() 函数:

    • BLEScan *pBLEScan = BLEDevice::getScan();: 获取全局的扫描对象。
    • pBLEScan->setAdvertisedDeviceCallbacks(...): 将扫描回调与扫描对象关联。
    • pBLEScan->start(30, false);: 启动一个持续 30 秒的扫描。
  • loop() 函数:

    • 连接管理:检查 doConnect 标志位,如果为真,则调用 connectToServer。如果连接失败或后续断开,会重新触发扫描。
    • 数据发送:如果 connected 标志位为真,则:
      • potValue = analogRead(...): 读取本地电位器的值。
      • brightness = map(...): 将 12 位的 ADC 读数 (0-4095) 映射到 8 位的 PWM 亮度值 (0-255)。
      • if (abs(brightness - lastBrightness) > 2): 同样使用了阈值判断,只有当亮度值有明显变化时才发送数据。可以有效减少不必要的蓝牙通信,降低功耗。
      • pRemoteCharacteristic->writeValue(&brightness, 1);: 将计算出的亮度值(1 个字节)通过 BLE 写入到远程外围设备的特征中。

运行结果

  1. 分别上传开发板 A 和 B 的代码。
  2. 客户端会自动扫描并连接到服务器。
  3. 转动连接在开发板 B 的电位器,连接在开发板 A 的 LED 亮度会随之变化。
  4. 也可以在串口监视器,查看连接状态和数据传输过程。

相关链接