
简介这份源码包面向物联网与嵌入式开发者聚焦乐鑫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服务与微信小程序端代码整合在同一工程中读者可据此梳理广播、连接、数据收发与页面交互的实现方式并参考目录结构快速定位固件与前端模块提升开发与调试效率。1. 从一份源码包说起ESP32-C3 的 BLE 与微信小程序到底怎么打通很多人第一次拿到「ESP32-C3 BLE 微信小程序」这类源码包第一反应是打开 Arduino IDE 直接点上传结果串口打印正常、小程序却搜不到设备或者连上三秒就断。问题往往不在代码本身而在于没搞清楚这条链路上有三个独立角色ESP32-C3 作为 BLE 外设Peripheral微信小程序作为 BLE 中心设备Central以及两者之间那套必须严格对齐的 GATT 服务与特征值约定。标题里的「源码」两个字容易让人误以为拿来即用实际上它更像一份参考实现真正要跑通你得理解广播包怎么组、UUID 怎么定、MTU 怎么协商、小程序端 API 的调用时序为什么不能乱。这篇笔记面向的是手里有 ESP32-C3 开发板、想用微信小程序做 BLE 调试或产品原型的嵌入式与前端开发者从环境搭建一路写到断连排查把这份源码背后能复现的路径拆开讲清楚。2. 环境与工具链把 ESP32-C3 的 Arduino 框架跑起来2.1 开发板选型与 Arduino 核心版本ESP32-C3 是 RISC-V 架构和经典 ESP32 的 Xtensa 不同Arduino 框架对它的支持依赖 esp32 核心包的版本。常见做法是在 Arduino IDE 的「开发板管理器」里安装 esp32 by Espressif版本建议 2.0.14 及以上早期 1.x 版本对 C3 的 BLE 库支持不完整容易出现BLEDevice::init()后广播发不出来的情况。开发板型号选「ESP32C3 Dev Module」如果用的是带 USB-JTAG 的板子USB CDC On Boot 要设为 Enabled否则串口监视器看不到输出。安装完成后在「工具」菜单里确认三件事Flash Size 按实际模组选常见 4MB、Partition Scheme 选 Default 4MB with spiffs、Upload Speed 用 921600 比较稳。有读者反馈上传失败报Failed to connect to ESP32-C3八成是没进下载模式——按住 BOOT 键再点复位松开后立刻上传。2.2 依赖库清单与最小验证工程BLE 部分不需要额外装库esp32 核心包自带BLEDevice.h、BLEServer.h、BLEUtils.h等。但如果你要同时跑 WiFi 和 BLE注意 C3 的射频是共享的共存时 BLE 广播间隔会被拉长这是硬件特性不是 bug。先写一个最小验证工程确认板子和 BLE 栈都正常#include BLEDevice.h #include BLEServer.h #include BLEUtils.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) { Serial.println(Central connected); } void onDisconnect(BLEServer* pServer) { Serial.println(Central disconnected, restart advertising); // 断连后必须重新开始广播否则小程序再也搜不到 pServer-startAdvertising(); } }; void setup() { Serial.begin(115200); delay(1000); BLEDevice::init(ESP32C3_BLE_Demo); // 广播名称小程序按此过滤 BLEDevice::setMTU(247); // 协商较大 MTU减少分包 BLEServer *pServer BLEDevice::createServer(); pServer-setCallbacks(new MyServerCallbacks()); BLEService *pService pServer-createService(SERVICE_UUID); // TX设备向小程序发送数据Notify pTxCharacteristic pService-createCharacteristic( CHARACTERISTIC_TX, BLECharacteristic::PROPERTY_NOTIFY ); pTxCharacteristic-addDescriptor(new BLE2902()); // RX小程序向设备写数据Write BLECharacteristic *pRxCharacteristic pService-createCharacteristic( CHARACTERISTIC_RX, BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR ); pService-start(); BLEAdvertising *pAdvertising BLEDevice::getAdvertising(); pAdvertising-addServiceUUID(SERVICE_UUID); pAdvertising-setScanResponse(true); pAdvertising-setMinPreferred(0x06); pAdvertising-setMinPreferred(0x12); BLEDevice::startAdvertising(); Serial.println(BLE advertising started); } void loop() { // 每秒通过 Notify 发一条计数验证链路 static uint32_t counter 0; if (pTxCharacteristic) { char buf[32]; snprintf(buf, sizeof(buf), tick:%lu, counter); pTxCharacteristic-setValue((uint8_t*)buf, strlen(buf)); pTxCharacteristic-notify(); } delay(1000); }这段代码的逻辑分四层初始化 BLE 栈并设置设备名、创建 Server 与 Service、分别建立 Notify 和 Write 两个特征、最后配置广播参数并启动。参数上重点看三个地方。BLEDevice::setMTU(247)是请求值实际生效值由双方协商小程序端在 Android 上通常能到 247iOS 上系统会限制在 185 左右。setMinPreferred(0x06)和0x12分别对应最小和最大连接间隔单位是 1.25ms写 0x06 约 7.5ms追求低延迟可以这么设但功耗会上去。BLE2902描述符是 Notify 的开关没有它小程序端notifyBLECharacteristicValueChange会失败这是最常见的翻车点之一。烧录后打开串口看到BLE advertising started就说明外设侧就绪。此时用手机上的通用 BLE 调试 App 应该能搜到ESP32C3_BLE_Demo连上后能看到两个特征值。这一步过了再进小程序端。3. 微信小程序端BLE 连接、订阅与收发的完整时序3.1 小程序 BLE API 的调用顺序为什么不能乱微信小程序的 BLE 接口是一套异步 APIopenBluetoothAdapter→startBluetoothDevicesDiscovery→onBluetoothDeviceFound→createBLEConnection→getBLEDeviceServices→getBLEDeviceCharacteristics→notifyBLECharacteristicValueChange→onBLECharacteristicValueChange这个顺序不能跳。常见错误是搜到设备立刻createBLEConnection但此时deviceId在某些 Android 机型上还没稳定连接会随机失败。稳妥做法是在onBluetoothDeviceFound里按name过滤拿到目标设备后延迟 200ms 再连。另一个坑是 iOS 和 Android 的deviceId格式不同。iOS 返回的是 UUID 格式Android 返回的是 MAC 地址。如果你在代码里硬编码deviceId换平台必挂。正确做法是每次通过onBluetoothDeviceFound动态获取。3.2 一个可复用的 BLE 封装模块下面这段是小程序端的核心逻辑封装成模块方便复用// ble.js const SERVICE_UUID 6E400001-B5A3-F393-E0A9-E50E24DCCA9E; const TX_CHAR_UUID 6E400003-B5A3-F393-E0A9-E50E24DCCA9E; // 设备 Notify const RX_CHAR_UUID 6E400002-B5A3-F393-E0A9-E50E24DCCA9E; // 小程序 Write let deviceId ; let serviceId ; let txCharId ; let rxCharId ; function openAdapter() { return new Promise((resolve, reject) { wx.openBluetoothAdapter({ success: resolve, fail: (err) { // 10001 表示蓝牙未开启引导用户去设置 reject(err); } }); }); } function startDiscovery() { return new Promise((resolve) { wx.startBluetoothDevicesDiscovery({ // 按服务 UUID 过滤减少无关设备干扰 services: [SERVICE_UUID], allowDuplicatesKey: false, success: resolve }); }); } function findDevice(targetName, timeout 10000) { return new Promise((resolve, reject) { const timer setTimeout(() { wx.stopBluetoothDevicesDiscovery(); reject(new Error(scan timeout)); }, timeout); wx.onBluetoothDeviceFound((res) { const found res.devices.find(d d.name targetName); if (found) { clearTimeout(timer); wx.stopBluetoothDevicesDiscovery(); resolve(found.deviceId); } }); }); } function connect(id) { deviceId id; return new Promise((resolve, reject) { wx.createBLEConnection({ deviceId, timeout: 10000, success: resolve, fail: reject }); }); } function discoverCharacteristics() { return new Promise((resolve, reject) { wx.getBLEDeviceServices({ deviceId, success: (res) { // 遍历找到目标服务 const target res.services.find(s s.uuid.toUpperCase().includes(6E400001)); if (!target) return reject(new Error(service not found)); serviceId target.uuid; wx.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (cRes) { cRes.characteristics.forEach(c { const uuid c.uuid.toUpperCase(); if (uuid.includes(6E400003)) txCharId c.uuid; if (uuid.includes(6E400002)) rxCharId c.uuid; }); resolve(); }, fail: reject }); }, fail: reject }); }); } function enableNotify(onData) { return new Promise((resolve, reject) { wx.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId: txCharId, state: true, success: () { // 必须先开启 notify再注册回调否则丢第一条数据 wx.onBLECharacteristicValueChange((res) { const value new Uint8Array(res.value); const text String.fromCharCode.apply(null, value); onData(text); }); resolve(); }, fail: reject }); }); } function send(data) { const buffer new ArrayBuffer(data.length); const view new Uint8Array(buffer); for (let i 0; i data.length; i) view[i] data.charCodeAt(i); return new Promise((resolve, reject) { wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: rxCharId, value: buffer, success: resolve, fail: reject }); }); } module.exports { openAdapter, startDiscovery, findDevice, connect, discoverCharacteristics, enableNotify, send };这段封装的关键点在于findDevice里用services过滤而不是扫全部设备能显著降低 Android 上的搜索耗时enableNotify里先调notifyBLECharacteristicValueChange再注册onBLECharacteristicValueChange顺序反了会丢第一条 Notify 数据send里把字符串转 ArrayBuffer 时用的是charCodeAt只支持 ASCII如果要发中文得用TextEncoder或手动 UTF-8 编码。参数上createBLEConnection的timeout设 10000ms 是经验值低于 5000 在部分低端 Android 机上不够。startBluetoothDevicesDiscovery的allowDuplicatesKey设 false 可以减少回调频率但如果设备广播间隔较长比如 1s 以上可能漏掉这时要设 true。3.3 页面层调用与数据展示在页面的onLoad里串起来const ble require(../../utils/ble.js); Page({ data: { log: [] }, async onLoad() { try { await ble.openAdapter(); await ble.startDiscovery(); const id await ble.findDevice(ESP32C3_BLE_Demo); await ble.connect(id); await ble.discoverCharacteristics(); await ble.enableNotify((text) { this.setData({ log: [...this.data.log, RX: text] }); }); // 连接成功后发一条测试数据 await ble.send(hello); } catch (e) { console.error(BLE init failed, e); } } });这里用 async/await 把回调地狱拉平可读性高很多。注意onLoad里不要直接await太久小程序对页面加载有时间限制如果扫描超过 10s 建议拆到按钮触发。4. 避坑与排查BLE 链路上最容易翻车的五个地方4.1 小程序搜不到设备现象串口打印BLE advertising started但小程序onBluetoothDeviceFound一直不返回目标设备。原因通常有三个。一是广播包里没带 Service UUID小程序用services过滤时就匹配不到。检查pAdvertising-addServiceUUID(SERVICE_UUID)是否在startAdvertising之前调用。二是设备名超过 8 字节被截断ESP32C3_BLE_Demo有 17 个字符广播包里放不下实际广播的是缩短版。解决方法是把名字改短比如C3_Demo或者把完整名字放到 Scan Response 里。三是 Android 的定位权限没开微信小程序在 Android 上搜 BLE 设备需要系统定位权限用户没授权就搜不到。解决先确认广播包内容用通用 BLE 调试 App 看能否搜到能搜到就是小程序权限或过滤条件问题搜不到就是广播配置问题。4.2 连上后立刻断开现象createBLEConnection成功回调触发但马上收到onBLEConnectionStateChange断开事件。原因多半是 MTU 协商失败或连接参数不兼容。ESP32-C3 默认 MTU 是 23如果小程序端在连接后立刻请求大 MTU 而设备没准备好会触发断连。另一个原因是setMinPreferred设得太激进比如设成 0x067.5ms部分 Android 手机不接受这么短的间隔直接断。解决把setMTU的调用放到onConnect回调里而不是setup里等连接建立后再协商。连接间隔先设保守值比如setMinPreferred(0x18)30ms稳定后再往下调。4.3 Notify 收不到数据现象小程序端notifyBLECharacteristicValueChange成功但onBLECharacteristicValueChange不触发。原因TX 特征没有加BLE2902描述符或者小程序端订阅的特征 UUID 搞反了。注意设备端的 TXNotify对应小程序端的订阅对象设备端的 RXWrite对应小程序端的写入对象这两个容易混。解决在设备端确认pTxCharacteristic-addDescriptor(new BLE2902())存在在小程序端打印txCharId和rxCharId确认订阅的是 TX。4.4 发送中文乱码现象小程序发中文到设备串口打印出来是乱码。原因send函数里用charCodeAt逐字符转字节中文的 Unicode 码点超过 255截断后就是乱码。解决用TextEncoder做 UTF-8 编码function send(data) { const encoder new TextEncoder(); // 小程序基础库 2.10.0 支持 const bytes encoder.encode(data); const buffer bytes.buffer; return new Promise((resolve, reject) { wx.writeBLECharacteristicValue({ deviceId, serviceId, characteristicId: rxCharId, value: buffer, success: resolve, fail: reject }); }); }设备端接收时按字节流处理不要假设是 ASCII。4.5 断连后无法重连现象设备断开一次后小程序再也搜不到或连不上。原因ESP32 的 BLE 栈在断连后不会自动重新广播必须在onDisconnect回调里手动调startAdvertising()。另外小程序端如果没调closeBLEConnection就退出页面连接状态会残留下次连接报10003连接已存在。解决设备端在onDisconnect里加pServer-startAdvertising()小程序端在onUnload里调wx.closeBLEConnection和wx.closeBluetoothAdapter清理状态。5. 进阶技巧用分帧协议把 BLE 吞吐量拉满BLE 的单包有效载荷受 MTU 限制即使协商到 247 字节实际可用也就 244 左右。如果你要传图片或较长配置必须自己做分帧。我一般会在应用层加一个简单的帧头1 字节序号 1 字节总帧数 2 字节长度 数据。接收端按序号重组丢帧就请求重发。设备端发送分帧的写法void sendFramed(uint8_t *data, size_t len) { const size_t chunkSize 240; // 留出帧头空间 uint8_t total (len chunkSize - 1) / chunkSize; for (uint8_t i 0; i total; i) { size_t offset i * chunkSize; size_t thisLen min(chunkSize, len - offset); uint8_t frame[244]; frame[0] i; // 序号 frame[1] total; // 总帧数 frame[2] thisLen 0xFF; frame[3] (thisLen 8) 0xFF; memcpy(frame[4], data offset, thisLen); pTxCharacteristic-setValue(frame, thisLen 4); pTxCharacteristic-notify(); delay(15); // 给协议栈留出发送窗口太快会丢包 } }delay(15)这个值不是随便写的。BLE 连接间隔如果是 30ms连续 notify 太快会导致协议栈缓冲区溢出表现为小程序端收到的帧序号跳变。15ms 是在 30ms 间隔下实测不丢包的最小值如果你把连接间隔调到 15ms这个值可以降到 8ms 左右。小程序端重组时用一个数组按序号存帧收齐后拼接let frames []; let totalFrames 0; function onFrame(text) { const bytes new Uint8Array(text.split().map(c c.charCodeAt(0))); const seq bytes[0]; totalFrames bytes[1]; const len bytes[2] | (bytes[3] 8); const payload bytes.slice(4, 4 len); frames[seq] payload; if (frames.filter(Boolean).length totalFrames) { const full new Uint8Array(frames.reduce((acc, f) acc f.length, 0)); let offset 0; frames.forEach(f { full.set(f, offset); offset f.length; }); frames []; // full 就是完整数据 } }这套分帧逻辑在传 10KB 以内的配置数据时很稳再大就要考虑加校验和重传机制了。我自己的习惯是先在串口上把分帧收发跑通再接小程序因为串口调试能看到每一帧的原始字节比在小程序里猜要快得多。BLE 这东西玄学的地方在于同样的代码在不同手机上表现可能不一样所以关键参数一定要留可调接口别写死。希望帮到你。本文还有配套的精品资源点击获取