最近更新于 2026-08-09 23:12
测试环境
- ESP32-WROOM-32
- PlatformIO IDE 3.3.4
- PlatformIO Core 6.1.19
- pioarduino platform-espressif32 55.03.311(Arduino Release v3.3.11 based on ESP-IDF v5.5.5)
platformio.ini
[env:esp32dev]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip
board = esp32dev
framework = arduino
monitor_speed = 115200
服务端
工具见:https://blog.iyatt.com/?p=25020
我这里是 Android 手机,使用 nRF Connect 开蓝牙服务器来供 ESP32 连接。
点开左上角的三杠,点开Configure GATT server

下拉展开,点击加号新建 GATT 配置

点击 ADD SERVICE 添加服务,
名字这里设置的my service,可以自定义
UUID必须填(和代码里写死的连接目标一致):4fafc201-1fb5-459e-8fcc-c5c9c331914b

然后点击 ADD CHARACTERISTIC 添加特征
名字这里设置的my chara,可以自定义
UUID必须填(和代码里写死的特征目标一致):beb5483e-36e1-4688-b7f5-ea07361b26a8
属性要勾读Read、写Write、通知Notufy,权限要勾Read、Write
下面的初始值可以选Empty value(空值),也可以填文本或写十六进制

返回主页,切换到 ADVERTISER 选项卡,点击加号创建广播
显示名字我设置为:server test,可以自定义
点击ADD RECORD,选Service UUDI,然后填上面配置 GATT 服务的 UUID:4fafc201-1fb5-459e-8fcc-c5c9c331914b
勾选可连接Connectable和可被发现Discoverable

创建好广播点击开关打开,弹出的页面直接点 OK,现在就开启广播了

实践
源码参考来源:https://github.com/espressif/arduino-esp32/blob/3.3.11/libraries/BLE/examples/Client/Client.ino
/**
* 一个功能丰富的 ESP32 BLE 客户端示例
* 用于扫描、连接指定的 BLE 服务端,并进行数据读写和通知订阅
* 作者:未知
* 更新:chegewara
*/
#include <Arduino.h>
#include "BLEDevice.h" // ESP32 BLE 核心库
// #include "BLEScan.h"
// 目标 BLE 服务(Service)的 UUID,需与服务端保持一致
static BLEUUID serviceUUID("4fafc201-1fb5-459e-8fcc-c5c9c331914b");
// 目标 BLE 特征(Characteristic)的 UUID
static BLEUUID charUUID("beb5483e-36e1-4688-b7f5-ea07361b26a8");
// 控制逻辑与状态标志位
static boolean doConnect = false; // 标志:是否触发连接操作
static boolean connected = false; // 标志:当前是否已成功连接服务端
static boolean doScan = false; // 标志:断开连接后是否重新开启扫描
// 远程特征指针与扫描到的设备对象指针
static BLERemoteCharacteristic *pRemoteCharacteristic;
static BLEAdvertisedDevice *myDevice;
/**
* 接收服务端 Notify(通知)数据的回调函数
* 当服务端修改特征值并主动推送到客户端时,该函数会被自动触发
*/
static void notifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify)
{
Serial.print("收到特征通知 (UUID: ");
Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
Serial.print("),数据长度: ");
Serial.println(length);
Serial.print("接收到的数据内容: ");
Serial.write(pData, length); // 按照字节流打印接收到的数据
Serial.println();
}
/**
* 客户端连接状态改变时的回调类
*/
class MyClientCallback : public BLEClientCallbacks
{
// 当与服务端建立连接成功时触发
void onConnect(BLEClient *pclient) {}
// 当与服务端断开连接时触发
void onDisconnect(BLEClient *pclient)
{
connected = false;
Serial.println("与 BLE 服务端断开连接 (onDisconnect)");
}
};
/**
* 连接至远程 BLE 服务端的主逻辑函数
*/
bool connectToServer()
{
Serial.print("正在尝试连接设备,MAC 地址: ");
Serial.println(myDevice->getAddress().toString().c_str());
// 1. 创建 BLE 客户端实例
BLEClient *pClient = BLEDevice::createClient();
Serial.println(" - 客户端创建成功");
// 设置客户端状态回调(监听连接/断开事件)
pClient->setClientCallbacks(new MyClientCallback());
// 2. 发起连接
// 直接传入 BLEAdvertisedDevice 对象能自动识别目标设备地址类型(Public 或 Private)
pClient->connect(myDevice);
Serial.println(" - 成功连接至服务端");
// 3. 请求协商最大传输单元 (MTU),默认是 23 字节,此处请求设为 517 字节
pClient->setMTU(517);
// 4. 获取远程服务端对应的 Service(服务)
BLERemoteService *pRemoteService = pClient->getService(serviceUUID);
if (pRemoteService == nullptr)
{
Serial.print("未能在目标设备上找到匹配的服务 UUID: ");
Serial.println(serviceUUID.toString().c_str());
pClient->disconnect();
return false;
}
Serial.println(" - 找到目标服务");
// 5. 从服务中获取对应的 Characteristic(特征)
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (pRemoteCharacteristic == nullptr)
{
Serial.print("未能在目标服务中找到匹配的特征 UUID: ");
Serial.println(charUUID.toString().c_str());
pClient->disconnect();
return false;
}
Serial.println(" - 找到目标特征");
// 6. 检查特征属性并进行【读取】操作
if (pRemoteCharacteristic->canRead())
{
String value = pRemoteCharacteristic->readValue();
Serial.print("成功读取特征当前值: ");
Serial.println(value.c_str());
}
// 7. 检查特征属性并【订阅通知】
if (pRemoteCharacteristic->canNotify())
{
// 注册回调函数以接收实时数据推送
pRemoteCharacteristic->registerForNotify(notifyCallback);
}
connected = true; // 标记已成功连接并准备就绪
return true;
}
/**
* 扫描回调类:在后台扫描周边广播设备,寻找符合目标服务 UUID 的设备
*/
class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks
{
/**
* 每次扫描到周围有 BLE 设备广播时,系统都会调用此函数
*/
void onResult(BLEAdvertisedDevice advertisedDevice)
{
Serial.print("发现 BLE 广播设备: ");
Serial.println(advertisedDevice.toString().c_str());
// 检查设备广播的数据中是否包含我们正在寻找的目标服务 UUID
if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID))
{
// 找到目标设备后,立即停止后台扫描以节省资源
BLEDevice::getScan()->stop();
// 保存目标设备对象的副本
myDevice = new BLEAdvertisedDevice(advertisedDevice);
doConnect = true; // 触发 loop 中的连接逻辑
doScan = true; // 标记断开后需要重新扫描
} // 找到目标服务端
} // onResult
}; // MyAdvertisedDeviceCallbacks
void setup()
{
Serial.begin(115200);
Serial.println("启动 Arduino BLE 客户端程序...");
// 初始化 ESP32 的 BLE 设备,参数可留空
BLEDevice::init("");
// 获取扫描器单例,并配置参数
BLEScan *pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); // 设置广播回调
pBLEScan->setInterval(1349); // 扫描间隔 (ms)
pBLEScan->setWindow(449); // 扫描窗口大小 (ms),需小于或等于间隔
pBLEScan->setActiveScan(true); // 开启主动扫描(可索要更多广播数据)
// 启动第一次扫描,持续 5 秒,第 2 个参数为 false 表示重新扫描
pBLEScan->start(5, false);
}
void loop()
{
// 1. 如果扫描回调找到了目标设备,执行连接逻辑
if (doConnect == true)
{
if (connectToServer())
{
Serial.println("我们现在已成功连接到 BLE 服务端。");
}
else
{
Serial.println("连接服务端失败,未采取进一步操作。");
}
doConnect = false; // 消费连接请求标志
}
// 2. 连接建立后的持续交互逻辑:写入数据 ➔ 重新读取验证
if (connected)
{
// 构造写入数据(开机时长秒数)
String newValue = "Time since boot: " + String(millis() / 1000);
Serial.println("==========================================");
Serial.println("【发送数据】: \"" + newValue + "\"");
// 写入数据
pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
// 延迟 100ms 留给服务端处理和更新缓存
delay(100);
// 重新读取特征值并打印
if (pRemoteCharacteristic->canRead())
{
String actualValue = pRemoteCharacteristic->readValue();
Serial.print("【回读验证】: ");
Serial.println(actualValue.c_str());
}
else
{
Serial.println("【警告】该特征不支持读取 (canRead == false)");
}
Serial.println("==========================================");
}
// 3. 如果连接断开且需要恢复扫描,重新启动扫描
else if (doScan)
{
BLEDevice::getScan()->start(0);
}
delay(2000); // 调整为每 2 秒循环一次
}
ESP32 连接到手机蓝牙,读取到初始值,并周期性修改特征值并读取修改后结果

手机端查看
可以看到当前特征值为:Time since boot: 113

