☰
ESP32-C3与微信小程序BLE通信实战:GATT协议与源码解析
2026/10/22 16:17:17 网站建设 项目流程

简介:这份源码资源面向物联网开发者与嵌入式爱好者,聚焦乐鑫ESP32-C3在Arduino框架下的BLE无线通信能力,并配套微信小程序端实现软硬件联动。项目可用于智能家居、健康监测、智能穿戴等需要近场无线交互的场景,帮助开发者打通从硬件控制到用户界面的完整链路。压缩包共713个文件,约32.22MB,包含22个ino主程序、10个cpp与10个h源文件、21个js脚本、9个wxml与13个wxss小程序页面文件,以及json配置、png与jpg图片、mp4演示视频、bin固件和md说明文档等,覆盖固件工程与小程序工程两大部分。目前已有250人学习下载。对于希望快速上手ESP32-C3 BLE开发、理解Arduino框架与微信小程序通信协议的读者,这份较新的源码提供了可参考的工程结构与实现思路,便于在此基础上二次开发与功能扩展。

1. 从一份 2025 年 4 月的源码包说起:ESP32-C3 与微信小程序的 BLE 通道到底怎么搭

如果你手上正好有一块 ESP32-C3 开发板,又想让微信小程序直接跟它通信,大概率绕不开 BLE 这条路。这份 2025 年 4 月 1 日打包的源码,核心就是两件事:Arduino 框架下把 ESP32-C3 的 BLE 外设跑起来,以及微信小程序端完成设备发现、连接、收发数据。它解决的不是“点个灯”这种演示级需求,而是把 BLE 的 GATT 服务、特征值读写、通知订阅这些底层细节,跟小程序侧的 API 调用串成了一条完整链路。适合谁?做过一点 Arduino、知道 UUID 是什么、但没真正把小程序和硬件连起来的人;也适合已经能跑通单端、卡在“小程序搜不到设备”或“连上就断”的熟手。源码包本身不包含硬件设计文件,重点在软件协议栈和交互逻辑,下面按实际复现顺序拆。

2. 先理清 BLE 通信模型:ESP32-C3 做外设,小程序做中心设备

2.1 GATT 角色分配与数据流向

BLE 通信里有两个角色:外设(Peripheral)和中心设备(Central)。ESP32-C3 在这份源码里被配置成外设,负责广播自己的存在、提供 GATT 服务;微信小程序运行在手机上,天然是中心设备,负责扫描、发起连接、读写特征值。数据流向不是“串口透传”那种双向管道,而是中心设备主动发起操作:读特征值、写特征值、订阅通知。外设只能通过通知(Notify)或指示(Indicate)把数据推给中心设备,不能主动发。这个模型决定了小程序端必须维护连接状态和订阅状态,否则会出现“写成功但收不到回复”的玄学现象。

常见做法是定义一个主服务 UUID,下面挂两个特征值:一个用于小程序写命令(Write),一个用于 ESP32-C3 上报数据(Notify)。源码里通常会把 UUID 写成宏或常量,方便两端对齐。注意,微信小程序对 UUID 的格式有要求:必须是 128 位标准 UUID,不能是 16 位短 UUID 直接填。如果你在 Arduino 端用了BLEUUID("FFE0")这种短 UUID,小程序端会直接过滤掉,连扫描结果都不显示。血泪经验:两端 UUID 必须逐字符一致,大小写不敏感但连字符位置要统一。

2.2 Arduino 框架下 BLE 库的选型与初始化

ESP32-C3 在 Arduino 环境下常用两个 BLE 库:内置的BLEDevice库和 NimBLE-Arduino。内置库体积大、编译慢,但 API 稳定;NimBLE 更轻量,适合 Flash 紧张的场景。这份源码基于 Arduino 框架,大概率用的是内置BLEDevice,因为它的示例最多、坑最少。初始化顺序不能乱:先BLEDevice::init()设置设备名,再创建 Server,然后创建 Service 和 Characteristic,最后加回调、启动广播。顺序错了会出现“广播发了但服务找不到”的情况。

#include <BLEDevice.h> #include <BLEServer.h> #include <BLEUtils.h> #include <BLE2902.h> // 定义 UUID,必须与小程序端完全一致 #define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" #define CHARACTERISTIC_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" // 小程序写 #define CHARACTERISTIC_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" // 通知上报 BLECharacteristic *pTxCharacteristic; class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer* pServer) { // 连接建立后可以在这里改广播参数或点灯指示 } void onDisconnect(BLEServer* pServer) { // 断开后必须重新启动广播,否则手机再也搜不到 pServer->startAdvertising(); } }; class MyRxCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string value = pCharacteristic->getValue(); if (value.length() > 0) { // 收到小程序下发的数据,按协议解析 } } }; void setup() { BLEDevice::init("ESP32C3_BLE_Demo"); BLEServer *pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); BLEService *pService = pServer->createService(SERVICE_UUID); // 写特征值,小程序往这里写 BLECharacteristic *pRxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_RX, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR ); pRxCharacteristic->setCallbacks(new MyRxCallbacks()); // 通知特征值,ESP32-C3 往这里推 pTxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_TX, BLECharacteristic::PROPERTY_NOTIFY ); pTxCharacteristic->addDescriptor(new BLE2902()); // 必须加,否则小程序订阅失败 pService->start(); BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); pAdvertising->setScanResponse(true); pAdvertising->setMinPreferred(0x06); // 兼容部分安卓机 pAdvertising->setMinPreferred(0x12); BLEDevice::startAdvertising(); }

上面代码里几个参数值得单独说:PROPERTY_WRITE_NR是无响应写,小程序端如果调用writeBLECharacteristicValue不传回调,走的就是无响应写,速度更快但丢包不报错;BLE2902描述符是通知功能的开关,不加的话小程序notifyBLECharacteristicValueChange会直接报错 10008;setMinPreferred那两个值是连接间隔参数,设太小某些安卓机会连不上,设 0x06 和 0x12 是社区里比较稳的折中。广播启动后,ESP32-C3 会持续发广播包,直到被连接。

3. 微信小程序端:从扫描到收发的完整链路

3.1 初始化蓝牙适配器与扫描设备

小程序端所有 BLE 操作都挂在wx命名空间下,第一步不是直接扫描,而是先openBluetoothAdapter。这个接口在安卓和 iOS 上行为有差异:iOS 上如果用户没开蓝牙,会直接 fail;安卓上可能返回成功但实际不可用。所以正确做法是 fail 回调里提示用户检查蓝牙开关,而不是只靠 success。打开适配器后再调startBluetoothDevicesDiscovery,传入 services 过滤数组,只扫你关心的服务 UUID,否则周围几十个设备全出来,列表刷新卡顿。

// 初始化蓝牙适配器 wx.openBluetoothAdapter({ success(res) { // 适配器就绪后才能扫描 wx.startBluetoothDevicesDiscovery({ services: ['6E400001-B5A3-F393-E0A9-E50E24DCCA9E'], // 只扫这个服务 allowDuplicatesKey: false, // 去重,避免同一设备反复上报 success() { // 监听设备发现事件 wx.onBluetoothDeviceFound((result) => { const devices = result.devices; devices.forEach(device => { // device.name 可能为空,用 localName 兜底 if (device.name || device.localName) { // 把设备塞进页面列表 } }); }); } }); }, fail(err) { // 这里要区分“用户没开蓝牙”和“小程序没权限” console.error('蓝牙适配器打开失败', err); } });

allowDuplicatesKey: false是个容易忽略的参数。设 true 时同一设备每次广播都触发回调,页面列表会疯狂刷新;设 false 后只上报一次,但设备信号强度变化不会更新。如果你要做 RSSI 测距,就得设 true 并自己做节流。另外device.name在安卓上经常为空,必须用device.localName兜底,否则你的设备列表里全是“未知设备”。

3.2 连接、订阅通知与数据收发

扫描到目标设备后,用createBLEConnection发起连接,拿到 deviceId。连接成功后第一件事不是写数据,而是getBLEDeviceServices拿到服务列表,再getBLEDeviceCharacteristics拿到特征值列表,确认哪个能写、哪个能通知。这一步很多新手跳过,直接硬编码 UUID 去写,结果特征值句柄对不上,写操作返回 10004。正确流程是动态发现,然后把可写和可通知的特征值 UUID 存下来。

// 假设 deviceId 已经从扫描结果拿到 wx.createBLEConnection({ deviceId: deviceId, success() { // 连接成功后必须延迟一下再发现服务,否则安卓上可能拿不到完整列表 setTimeout(() => { wx.getBLEDeviceServices({ deviceId: deviceId, success(serviceRes) { serviceRes.services.forEach(service => { wx.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: service.uuid, success(charRes) { charRes.characteristics.forEach(char => { // 找到可写特征值,用于下发命令 if (char.properties.write || char.properties.writeNoResponse) { // 保存 char.uuid } // 找到可通知特征值,用于接收上报 if (char.properties.notify || char.properties.indicate) { // 保存 char.uuid,并开启订阅 wx.notifyBLECharacteristicValueChange({ deviceId: deviceId, serviceId: service.uuid, characteristicId: char.uuid, state: true, success() { // 订阅成功后才能收到 onBLECharacteristicValueChange } }); } }); } }); }); } }); }, 200); // 200ms 是经验值,太短会失败 } }); // 监听特征值变化,接收 ESP32-C3 上报的数据 wx.onBLECharacteristicValueChange((result) => { // result.value 是 ArrayBuffer,需要转成 Uint8Array 再解析 const data = new Uint8Array(result.value); // 按协议解析 data });

写数据时,小程序要求传入 ArrayBuffer,不能直接传字符串。常见做法是用TextEncoder或手动转。注意单次写长度限制:安卓上默认 20 字节,iOS 上可能更少。超过长度必须分包,否则写失败但回调可能不报错。源码里如果没做分包,你发长命令就会翻车。另外notifyBLECharacteristicValueChange的state: true是开启订阅,关的时候要传 false,否则下次连接会重复订阅。

4. 避坑与排查:BLE 调试中最容易翻车的五个点

4.1 现象:小程序扫描不到 ESP32-C3 的广播

原因通常有三个:一是 Arduino 端广播没启动,或者启动后又被onDisconnect里的逻辑停掉了;二是 UUID 格式不对,小程序过滤时直接排除;三是广播间隔太长,小程序扫描窗口没覆盖到。解决:先确认BLEDevice::startAdvertising()在 setup 里被调用,且没有在别处调stopAdvertising();再把小程序扫描的 services 参数去掉,看能否扫到设备名,如果能扫到就是 UUID 问题;最后把广播间隔设成 100ms 左右测试。

4.2 现象:连接成功但立刻断开,或者连上后服务列表为空

原因:安卓上连接后立即调getBLEDeviceServices会返回空,因为底层 GATT 还没完成服务发现。解决:在createBLEConnection的 success 里加 200 到 500 毫秒延迟,或者监听onBLEConnectionStateChange确认连接稳定后再发现服务。另外 ESP32-C3 端如果onConnect里做了耗时操作(比如串口打印大量日志),也会拖慢服务发现,建议只做标志位设置。

4.3 现象:写特征值返回 10004 或 10005

10004 是“没有找到指定特征值”,10005 是“没有找到指定服务”。原因几乎都是 UUID 不匹配:小程序端用的 UUID 是短格式,或者大小写、连字符跟 Arduino 端不一致。解决:把两端的 UUID 字符串复制到同一个文本编辑器里逐字符比对,特别注意 Arduino 端BLEUUID构造时是否自动补全了 128 位。建议统一用全大写、带连字符的 128 位格式。

4.4 现象:订阅通知成功,但收不到 ESP32-C3 发来的数据

原因:ESP32-C3 端调setValue后没有调notify(),或者notify()时没有连接有效的中心设备。另外BLE2902描述符没加,小程序订阅时虽然返回成功,但底层没真正打开通知通道。解决:确认pTxCharacteristic->notify()在setValue之后被调用,且BLE2902已添加。如果还是不行,在小程序端onBLECharacteristicValueChange里打印原始 ArrayBuffer,看是否收到空包。

4.5 现象:iOS 上能连,安卓上连不上;或者反过来

原因:广播参数和连接参数在不同平台上的兼容性差异。setMinPreferred(0x06)和0x12这组值在多数安卓机上稳,但部分机型要求更长的连接间隔。另外 iOS 对设备名有缓存,改了BLEDevice::init的名字后 iOS 可能还显示旧名。解决:安卓连不上时,把setMinPreferred改成0x20以上测试;iOS 显示旧名时,在手机设置里忽略该设备再重新扫描。源码里如果写死了连接间隔,建议留一个宏方便改。

5. 进阶技巧:用分包与校验把 BLE 通信做稳

BLE 单包有效载荷有限,安卓默认 20 字节,iOS 可能协商到 185 字节但小程序端不一定支持。实际项目里传 JSON 或长指令,必须自己做分包协议。我一般会在 ESP32-C3 端定义一个简单的帧格式:包头 0xAA、长度、序号、载荷、校验和。小程序端收到通知后先拼包,校验通过再解析。下面是一个最小实现思路。

// ESP32-C3 端分包发送示例 void sendPacket(uint8_t* payload, size_t len) { const size_t MAX_CHUNK = 18; // 留 2 字节给包头和序号 uint8_t seq = 0; for (size_t offset = 0; offset < len; offset += MAX_CHUNK) { size_t chunkLen = min(MAX_CHUNK, len - offset); uint8_t buffer[20]; buffer[0] = 0xAA; // 包头 buffer[1] = seq++; // 序号 memcpy(&buffer[2], payload + offset, chunkLen); pTxCharacteristic->setValue(buffer, chunkLen + 2); pTxCharacteristic->notify(); delay(10); // 给协议栈一点时间,太快会丢包 } }

小程序端对应维护一个接收缓冲区,按序号排序,收到完整帧后校验。校验和可以用简单的累加和,也可以用 CRC8,看你对可靠性的要求。注意delay(10)这个值:太小会丢包,太大影响吞吐。实测 10ms 在多数安卓机上稳,iOS 可以降到 5ms。另外如果小程序端同时有多个页面在监听 BLE 事件,记得在页面onUnload里调offBLECharacteristicValueChange,否则回调会重复触发,数据解析错乱。

从那以后我每次调 BLE 项目,都强制先跑一遍“扫描-连接-发现服务-订阅-收发”的最小闭环,确认两端 UUID 和通知描述符都对了,再往上叠业务逻辑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询