)
esp-nimble-cpp 快速上手在 ESP32 上打造 BLE 服务端与客户端New User Guide 实战解读【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota本篇指南以仓库内 esp-nimble-cpp 官方新手文档 为核心脉络结合 esp-nimble-cpp 的源码实现完整讲解如何在 ESP32 上使用 NimBLE 协议栈编写 BLE 服务端广播、服务与特征值和客户端扫描、连接、读写。读完本文你将掌握NimBLEDevice::init初始化、NimBLEServer/NimBLEService/NimBLECharacteristic服务端三件套、NimBLEScan/NimBLEClient客户端链路以及特征属性位掩码、返回值检查、资源清理等关键实战细节。该库在本仓库中位于 lib/libesp32_div/esp-nimble-cpp是 Tasmota 为 ESP32 平台引入的 BLE 能力库从源码结构看它服务于 ESP32 相关的蓝牙功能编译目标。一、先认识 esp-nimble-cpp 与 NimBLE在动手写代码之前先了解这个库的定位。根据 README.md 的说明NimBLE 是什么NimBLE 是 Apache 完全开源的 Bluetooth Low Energy 协议栈源自 mynewt-nimble比 Bluedroid 更适合资源受限设备并由 Espressif 官方移植到了 ESP32。esp-nimble-cpp 是什么它是基于 NimBLE 协议栈的 ESP32 C 库目标是在 API 层面尽量兼容原 Bluedroid 版本的 BLE 库但底层使用 NimBLE 栈实现。资源特性README 提到测试显示相比原 Bluedroid 库flash 占用减少近 50%、RAM 节省约 100kB原文同时注明 Your results may vary即实际数据因项目而异。线程安全库本身是线程安全的特征值可以从任意线程设置这一点在 Usage_tips.md 中被再次强调。1.1 如何安装与启用ESP-IDF v4.0按照 README.md 的安装指引将 esp-nimble-cpp 下载或克隆到 ESP-IDF 项目的components目录中运行menuconfig进入Component config - Bluetooth启用 Bluetooth并将Bluetooth host选为NimBLE在NimBLE Options中配置各项协议参数如连接数、绑定数、CCCD 数量等在main.cpp中#include NimBLEDevice.h并在app_main中调用NimBLEDevice::init()。若你是从原 Bluedroid 库迁移的老用户请直接参阅 Migration_guide.md本文面向全新用户。二、库初始化一切 BLE 操作的前提在使用任何 BLE 功能之前必须先初始化 NimBLE 协议栈使其准备好接收命令。只需一行调用NimBLEDevice::init(your device name here);参数是一个字符串即你想要广播的设备名称如果你不创建服务端或不想广播名称直接传空字符串即可该函数可以在任何需要使用 BLE 函数的时刻调用并不强制要求必须放在app_mainIDF或setupArduino中但通常人们都会这么做。从源码看NimBLEDevice.h 中该函数的真实签名为static bool init(const std::string deviceName)它返回布尔值。同时头文件还提供了配套的deinit(bool clearAll false)、setDeviceName(const std::string)、isInitialized()等生命周期与查询接口方便你在需要时切换名称或完全关闭 BLE 栈。注意init传入的设备名会写入 Generic Access 服务中的 GATT 设备名称特征UUID 0x2A00它与广播数据里的 Local name 是两个概念详见后文设备本地名称一节。三、创建 BLE 服务端ServerBLE 服务端承担两项任务广播自己的存在让客户端能够发现它以及提供服务Service——服务内包含供连接方读写的信息特征值 Characteristic。3.1 创建 Server 与 Service初始化协议栈后调用NimBLEDevice::createServer()创建一个服务端实例它会返回指向NimBLEServer的指针。然后调用NimBLEServer::createService(const char* uuid)告诉服务端它要托管哪些服务该方法返回指向NimBLEService实例的指针。uuid参数是一个十六进制字符串可以是 16 位、32 位或 128 位。本示例使用简单的 16 位值ABCD。骨架示例代码#include NimBLEDevice.h extern C void app_main(void) { NimBLEDevice::init(NimBLE); NimBLEServer *pServer NimBLEDevice::createServer(); NimBLEService *pService pServer-createService(ABCD); }此时 NimBLE 已初始化、服务端已创建、服务已挂载但还没有任何实际数据接下来给服务添加特征值。3.2 添加特征值Characteristic与属性位掩码调用NimBLEService::createCharacteristic它返回指向NimBLECharacteristic的指针需要两个参数uuid特征值的 UUID与服务 UUID 一样这里用 16 位值1234properties应用在特征值上的属性位掩码bitmask是多个NIMBLE_PROPERTY::枚举值的按位或组合。NIMBLE_PROPERTY完整选项列表原文档全部保留NIMBLE_PROPERTY::READ— 允许客户端读取NIMBLE_PROPERTY::READ_ENC— 读取需加密链路NIMBLE_PROPERTY::READ_AUTHEN— 读取需认证配对NIMBLE_PROPERTY::READ_AUTHOR— 读取需授权应用层批准NIMBLE_PROPERTY::WRITE— 允许客户端写入NIMBLE_PROPERTY::WRITE_NR— 允许无响应写Write Without ResponseNIMBLE_PROPERTY::WRITE_ENC— 写入需加密链路NIMBLE_PROPERTY::WRITE_AUTHEN— 写入需认证NIMBLE_PROPERTY::WRITE_AUTHOR— 写入需授权NIMBLE_PROPERTY::BROADCAST— 允许在广播包中携带NIMBLE_PROPERTY::NOTIFY— 允许客户端订阅通知NIMBLE_PROPERTY::INDICATE— 允许客户端订阅指示带确认的通知本示例不需要显式指定属性因为默认值就是NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE即允许无加密、无安全要求的读写。这一点在源码中可以得到印证NimBLEService.h 与 NimBLECharacteristic.h 中createCharacteristic的properties参数默认值均为NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE后者还额外提供了maxLen参数默认BLE_ATT_ATTR_MAX_LEN用于指定该特征值最大可承载的字节数。所以这里直接写pService-createCharacteristic(1234);即可。当前示例代码#include NimBLEDevice.h extern C void app_main(void) { NimBLEDevice::init(NimBLE); NimBLEServer *pServer NimBLEDevice::createServer(); NimBLEService *pService pServer-createService(ABCD); NimBLECharacteristic *pCharacteristic pService-createCharacteristic(1234); }3.3 启动服务、写入初始值并开始广播剩下的三步启动服务、给特征值赋值、开始广播等待客户端连接。启动服务调用NimBLEService::start()设置特征值调用NimBLECharacteristic::setValue参数支持多种数据类型字符串、结构体、std::string、容器等本示例用简单字符串pCharacteristic-setValue(Hello BLE);开始广播通过NimBLEDevice::getAdvertising()获取NimBLEAdvertising实例把服务 UUID 加入广播数据可选设置广播名称并启动。广播相关代码NimBLEAdvertising *pAdvertising NimBLEDevice::getAdvertising(); // create advertising instance pAdvertising-addServiceUUID(ABCD); // advertise the UUID of our service pAdvertising-setName(NimBLE); // advertise the device name pAdvertising-start(); // start advertising从 NimBLEAdvertising.h 可以看到addServiceUUID、setName之外还有setAppearance、setManufacturerData、setServiceData、setURI、setMinInterval/setMaxInterval等丰富的广播内容定制接口均返回bool表示操作是否成功start(uint32_t duration 0, ...)的默认持续时间为 0一直广播。完整服务端示例代码#include NimBLEDevice.h extern C void app_main(void) { NimBLEDevice::init(NimBLE); NimBLEServer *pServer NimBLEDevice::createServer(); NimBLEService *pService pServer-createService(ABCD); NimBLECharacteristic *pCharacteristic pService-createCharacteristic(1234); pService-start(); pCharacteristic-setValue(Hello BLE); NimBLEAdvertising *pAdvertising NimBLEDevice::getAdvertising(); pAdvertising-addServiceUUID(ABCD); // advertise the UUID of our service pAdvertising-setName(NimBLE); // advertise the device name pAdvertising-start(); }现在用手机上的 nRFConnect 或任意 BLE 扫描 App 扫描你就能看到一个名为NimBLE、带ABCD服务的设备。3.4 服务端进阶回调机制上面的最小示例只能被动地摆着一个静态值。真正的应用还需要感知连接、断开、读写事件。仓库中的完整示例 examples/NimBLE_Server/main/main.cpp 演示了两类回调NimBLEServerCallbacks继承并重写onConnect客户端连接成功可在此调整连接参数如updateConnParams(handle, 24, 48, 0, 180)间隔单位为 1.25ms、超时为 10ms 的整数倍、onDisconnect客户端断开示例中会调用NimBLEDevice::startAdvertising()重新开始广播、onMTUChangeMTU 更新通知、以及onPassKeyDisplay、onConfirmPassKey、onAuthenticationComplete等安全相关回调示例中演示了配对后校验connInfo.isEncrypted()加密失败则断开客户端。NimBLECharacteristicCallbacks重写onRead/onWrite即可在客户端读取或写入特征值时得到通知并可结合pCharacteristic-getValue()读取当前值。此外NimBLECharacteristic.h 还提供了notify()与indicate()系列方法用于在特征值变化时主动推送数据需在属性中带NOTIFY/INDICATE并提供了针对std::string、容器等类型的模板重载。四、创建 BLE 客户端ClientBLE 客户端同样承担两项任务扫描广播中的服务端以及连接它们去读写特征值/描述符。4.1 初始化扫描初始化协议栈后调用NimBLEDevice::getScan()获取NimBLEScan实例指针。然后调用NimBLEScan::getResults(duration)开始扫描。duration是uint32_t类型单位是毫秒传 0 表示永久扫描。本示例扫描 10 秒。该调用是阻塞式的仓库同时提供非阻塞重载见 4.5 节。扫描完成后返回NimBLEScanResults实例用于解析感兴趣的目标。示例代码#include NimBLEDevice.h extern C void app_main(void) { NimBLEDevice::init(); NimBLEScan *pScan NimBLEDevice::getScan(); NimBLEScanResults results pScan-getResults(10 * 1000); }对照 NimBLEScan.h 源码getResults()有无参与非阻塞两种形式NimBLEScanResults getResults();和NimBLEScanResults getResults(uint32_t duration, bool is_continue false);配合NimBLEScan::start(uint32_t duration, bool isContinue false, bool restart true)即可实现先发起扫描、稍后取结果的非阻塞流程。4.2 遍历扫描结果筛选目标设备拿到结果后遍历NimBLEScanResults中的每个设备检查其是否在广播我们关心的ABCD服务。结果集中的每一项都是const NimBLEAdvertisedDevice*。调用NimBLEAdvertisedDevice::isAdvertisingService判断它接收一个NimBLEUUID参数所以需要先构造一个 UUID 实例。从 NimBLEAdvertisedDevice.h 可以看到该对象还提供了getName()、getAddress()、getRSSI()、getManufacturerData()、haveServiceUUID()等丰富的设备信息访问接口。遍历筛选代码NimBLEUUID serviceUuid(ABCD); for (int i 0; i results.getCount(); i) { const NimBLEAdvertisedDevice *device results.getDevice(i); if (device-isAdvertisingService(serviceUuid)) { // create a client and connect } }4.3 创建客户端并连接调用NimBLEDevice::createClient()创建NimBLEClient实例并返回指针随后调用NimBLEClient::connect连接目标设备它接收指向NimBLEAdvertisedDevice的指针连接成功返回true。连接代码NimBLEUUID serviceUuid(ABCD); for (int i 0; i results.getCount(); i) { const NimBLEAdvertisedDevice *device results.getDevice(i); if (device-isAdvertisingService(serviceUuid)) { NimBLEClient *pClient NimBLEDevice::createClient(); if (pClient-connect(device)) { //success } else { // failed to connect } } }如文档强调NimBLEClient::connect的返回值必须被检查确认成功后再继续取数据。源码层面NimBLEClient.h 中connect还有若干可选参数deleteAttributes连接前是否清理缓存的属性默认 true、asyncConnect是否异步连接默认 false、exchangeMTU连接后是否自动交换 MTU默认 true。4.4 获取远程服务与特征值并读取接下来向服务端索取我们关心的服务和特征值再读取特征值内容NimBLEClient::getService参数为服务 UUID返回指向NimBLERemoteService的指针服务不存在时返回nullptrNimBLERemoteService::getCharacteristic参数为特征值 UUID返回指向NimBLERemoteCharacteristic的指针未找到时返回nullptrNimBLERemoteCharacteristic::readValue()读取特征值。读取数据代码NimBLEUUID serviceUuid(ABCD); for (int i 0; i results.getCount(); i) { const NimBLEAdvertisedDevice *device results.getDevice(i); if (device-isAdvertisingService(serviceUuid)) { NimBLEClient *pClient NimBLEDevice::createClient(); if (!pClient) { // Make sure the client was created break; } if (pClient-connect(device)) { NimBLERemoteService *pService pClient-getService(serviceUuid); if (pService ! nullptr) { NimBLERemoteCharacteristic *pCharacteristic pService-getCharacteristic(1234); if (pCharacteristic ! nullptr) { std::string value pCharacteristic-readValue(); // print or do whatever you need with the value } } } else { // failed to connect } } }务必逐级检查每个返回的指针是否为nullptr——正如 Usage_tips.md 所说对nullptr调用方法必然导致崩溃这是新手最常见的错误来源。4.5 清理客户端实例数据处理完毕后应当清理资源。由于库支持同时创建多个客户端用完的客户端应通过NimBLEDevice::deleteClient删除以节省内存。无需手动调用disconnect——删除客户端实例时库会自动断开连接。加入清理逻辑NimBLEUUID serviceUuid(ABCD); for (int i 0; i results.getCount(); i) { const NimBLEAdvertisedDevice *device results.getDevice(i); if (device-isAdvertisingService(serviceUuid)) { NimBLEClient *pClient NimBLEDevice::createClient(); if (!pClient) { // Make sure the client was created break; } if (pClient-connect(device)) { NimBLERemoteService *pService pClient-getService(serviceUuid); if (pService ! nullptr) { NimBLERemoteCharacteristic *pCharacteristic pService-getCharacteristic(1234); if (pCharacteristic ! nullptr) { std::string value pCharacteristic-readValue(); // print or do whatever you need with the value } } } else { // failed to connect } NimBLEDevice::deleteClient(pClient); } }客户端完整示例代码#include NimBLEDevice.h extern C void app_main(void) { NimBLEDevice::init(); NimBLEScan *pScan NimBLEDevice::getScan(); NimBLEScanResults results pScan-getResults(10 * 1000); NimBLEUUID serviceUuid(ABCD); for (int i 0; i results.getCount(); i) { const NimBLEAdvertisedDevice *device results.getDevice(i); if (device-isAdvertisingService(serviceUuid)) { NimBLEClient *pClient NimBLEDevice::createClient(); if (!pClient) { // Make sure the client was created break; } if (pClient-connect(device)) { NimBLERemoteService *pService pClient-getService(serviceUuid); if (pService ! nullptr) { NimBLERemoteCharacteristic *pCharacteristic pService-getCharacteristic(1234); if (pCharacteristic ! nullptr) { std::string value pCharacteristic-readValue(); // print or do whatever you need with the value } } } else { // failed to connect } NimBLEDevice::deleteClient(pClient); } } }4.6 客户端进阶回调与非阻塞模式扫描与连接并非只能阻塞进行。仓库示例 examples/NimBLE_Async_Client/main/main.cpp 展示了通过setClientCallbacks(NimBLEClientCallbacks*)注册onConnect、onConnectFail、onDisconnect回调配合connect(..., asyncConnect true)实现非阻塞异步连接examples/Continuous_scan/main/main.cpp 则通过NimBLEScanCallbacks::onResult实现持续扫描、每发现一个设备即回调处理配合setScanCallbacks(callbacks, wantDuplicates)控制是否上报重复设备无需等到整轮扫描结束。这些模式更适合需要同时维护多个连接或实时响应的场景。五、实用技巧与避坑指南以下建议来自 Usage_tips.md并结合源码整理能显著提升程序的稳定性和效率1. 线程安全是库的默认保证。特征值、属性可以在任意线程中被修改无需加锁。2. 不要随意删除客户端实例。客户端一旦连接并检索过服务/特征值这些数据会缓存在实例中如果频繁连接同一设备却每次都删除客户端下次连接时将被迫重新从对端检索全部信息既浪费对端设备电量又造成堆碎片、降低连接性能。库的客户端实例内存开销约为原 Bluedroid 库的 20%删除省下的空间有限。官方建议两次连接同一设备间隔小于 5 分钟时保留客户端实例。这与 4.5 节用完即删的场景一次性连接不同设备并不矛盾按实际频率取舍即可。3. 只检索真正需要的服务与特征值。对已知设备应使用NimBLEClient::getService(uuid)、NimBLERemoteService::getCharacteristic(uuid)定点获取getServices(true)/getCharacteristics(true)这类全量拉取只应用于未知设备。这样能减少耗电、堆占用、连接时间提升整体效率。4. 检查一切返回值。对bool返回值如connect判断成功/失败对指针返回值如getService、getCharacteristic、createClient判断是否为nullptr再继续这是避免崩溃和莫名 bug 的最有效手段。5. 设备本地名称有两个来源。其一为广播数据中的 Local name通过NimBLEAdvertising::setName()设置其二是 GATT 设备名称UUID 0x2A00由NimBLEDevice::init()或setDeviceName()设置连接后才被读取。操作系统会缓存 GATT 设备名并在连接后用它覆盖显示名例如广播名设为 ABCD、GATT 名设为 12345则设备连接前显示 ABCD连接后变成 12345若未设置广播名iOS 等系统在连接前可能显示 Unnamed。建议两者都按预期设置。6. 绑定数量不足往往源于 MAX_CCCDS 太小。若CONFIG_BT_NIMBLE_MAX_BONDS设置了 N 但实际持久化的绑定远少于 N可用NimBLEDevice::getNumBonds()观察通常是因为每个绑定都会持久化客户端订阅过的 CCCD 值而CONFIG_BT_NIMBLE_MAX_CCCDS太小导致旧的 CCCD 被覆盖、连带对应绑定丢失。解决办法是调大CONFIG_BT_NIMBLE_MAX_CCCDS每个 CCCD 在 NVS 中约占 40 字节保守取值应不小于MAX_BONDS × 最大可订阅特征值数量。六、继续深入示例与文档地图原文档结尾提示更高级的功能请参考 examples 目录本仓库 examples 目录确实提供了层层递进的完整工程示例路径覆盖内容NimBLE_Server/main/main.cpp服务端完整功能回调、安全配对、连接参数调整NimBLE_Client/main/main.cpp客户端完整功能扫描、连接、发现与读写NimBLE_Async_Client/main/main.cpp客户端非阻塞异步连接Continuous_scan/main/main.cpp持续扫描与逐设备回调L2CAP面向无 GATT 的 L2CAP 通道通信客户端/服务端Bluetooth_5BLE 5.0 扩展扩展广播、扩展扫描、多广播集、扩展客户端配套文档方面除本篇依据的 New_user_guide.md 外还有 Migration_guide.mdBluedroid 老项目迁移、Usage_tips.md实战技巧上文已大量引用、index.md文档入口以及 Bluetooth 5 features 专题文档。七、总结从零开始搭建 ESP32 BLE 应用只需把握一条主线初始化协议栈 → 服务端建服务、加特征、启动广播或客户端扫描、筛选、连接、读取→ 按需使用回调并做好资源清理。本篇完整覆盖了 New_user_guide.md 的全部示例与参数说明并以仓库源码佐证了默认属性、返回值语义、可选参数等关键细节。对照 examples 中的完整工程动手运行一遍再结合 Usage_tips.md 的工程化建议即可稳定可靠地将 BLE 能力接入自己的 ESP32 项目中。【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考