1. 为什么我要折腾平板写 ESP32 代码这件事去年冬天我接了个小项目需要在几个 ESP32 节点上做 BLE 广播和传感器数据采集。项目本身不复杂但有个场景把我卡住了设备装在户外一个铁皮柜里每次改几行代码就得抱着笔记本蹲在柜子旁边USB 线一插一拔笔记本电量掉得飞快冬天手还冻得敲不动键盘。当时我就想要是能拿个平板通过蓝牙直接把代码推上去、跑起来、看日志那该多省事。后来我在 GitHub 上翻到了PyBLE这个项目第一反应是这不就是我想要的东西吗。它的定位很直接用平板或手机通过 BLE低功耗蓝牙连接 ESP32在移动端直接写 MicroPython 代码、上传、运行、看输出。不需要数据线不需要电脑甚至不需要 WiFi 环境。对于我这种经常要在现场调试、又不想背一堆设备的人来说这个思路非常对味。这篇文章我打算把 PyBLE 这个项目从头到尾拆一遍。不是那种官方文档翻译式的介绍而是把我自己实际用下来踩过的坑、想明白的原理、以及一些文档里不会写的细节都摊开讲。适合谁看手上有 ESP32、玩过一点 MicroPython、想摆脱电脑束缚做移动端调试的人也适合对 BLE 通信机制好奇、想搞明白平板到底怎么把代码塞进芯片里的开发者。如果你连 ESP32 都没摸过建议先补一下基础烧录和 REPL 交互再回来看这篇会顺畅很多。核心关键词我先摆出来后面会反复围绕它们展开ESP32、BLE、PyBLE、MicroPython、IDE。这五个词基本就是整个项目的骨架——ESP32 是硬件载体MicroPython 是运行环境BLE 是通信通道PyBLE 是工具本身IDE 是它呈现给用户的形态。2. PyBLE 到底解决了什么问题以及它的整体设计思路2.1 传统 ESP32 开发流程的三个痛点在讲 PyBLE 之前得先说清楚它针对的是什么场景。常规的 ESP32 MicroPython 开发流程大概是这样电脑装 Thonny 或者 VS Code Pymakr 插件USB 线连上开发板通过串口进 REPL然后上传文件、运行脚本。这套流程在实验室里没问题但一旦离开桌面就暴露三个问题。第一个是物理连接的限制。USB 线长度有限设备装进外壳、埋进管道、挂到高处之后你很难舒服地接线。我那个铁皮柜的项目板子固定在柜子内侧USB 口朝里每次插线都得把手伸进去盲插特别别扭。第二个是设备重量和续航。笔记本再轻也有一公斤多加上电源适配器现场调试一待就是半天。平板就轻多了而且很多平板续航能撑一整天这在户外场景里是实打实的优势。第三个是串口驱动的兼容性。ESP32 常用的 CP2102、CH340 这些 USB 转串口芯片在不同操作系统上驱动表现不一致尤其是平板这类设备OTG 供电和驱动支持经常出幺蛾子。而 BLE 是无线协议绕开了驱动这一层。PyBLE 的思路就是既然 ESP32 本身支持 BLE那为什么不把 BLE 当成一条无线串口来用这个想法其实不新鲜MicroPython 官方就提供了ubluetooth模块但官方只给了底层 API没有现成的移动端 IDE。PyBLE 做的事情是在这个底层能力之上搭了一套完整的移动端代码编辑 传输 执行 回显的闭环。2.2 BLE 作为传输通道的合理性分析有人可能会问为什么不用 WiFiWiFi 传文件不是更快吗这个问题我认真想过结论是在现场调试这个特定场景下BLE 比 WiFi 更合适原因有三。第一是配网成本。WiFi 连接需要知道 SSID 和密码现场环境未必有可用的 WiFi就算有让 ESP32 连上、拿到 IP、再让平板去连同一个网络这一套流程本身就够折腾的。BLE 不需要配网设备上电广播平板扫描到就能连零配置。第二是功耗。ESP32 的 WiFi 模块工作电流在 100mA 以上峰值能到 200mA 多而 BLE 广播和连接状态下的平均电流通常在几十 mA 甚至更低。对于电池供电的节点这个差距很关键。第三是连接确定性。WiFi 受路由器、信道拥堵影响现场环境复杂时连接不稳定。BLE 是点对点直连只要距离够一般 10 米内没问题连接质量很可控。当然 BLE 也有明显短板带宽低。BLE 4.2 的理论吞吐量也就几十 KB/s实际传输有效载荷更少。所以 PyBLE 不适合传大文件但对于几百行到几千行的 MicroPython 脚本完全够用。一个 5KB 的脚本几秒钟就传完了。2.3 PyBLE 的架构分层我把 PyBLE 的工作机制拆成三层来理解这样后面讲细节的时候不容易乱。最底层是 BLE GATT 通信层。ESP32 作为 BLE 外设Peripheral暴露一个 GATT 服务服务里包含几个特征值Characteristic。平板作为 BLE 中心设备Central连接后通过写特征值发送数据、通过订阅通知特征值接收数据。这一层是标准 BLE 协议任何支持 BLE 的设备都能对接。中间层是数据帧协议。BLE 单次传输的数据量有限默认 MTU 23 字节协商后一般能到 100-200 字节所以代码不能一次性发完必须分片。PyBLE 在应用层定义了一套简单的帧格式把代码内容切成小块每块带上序号接收端按序重组。同时还要处理命令和数据的区分比如开始传输传输结束执行代码这些控制指令。最上层是 IDE 交互层。这是用户直接接触的部分代码编辑器、文件列表、连接按钮、运行按钮、输出窗口。它把用户的编辑操作翻译成中间层的帧再把底层收到的回显渲染到输出窗口。理解这三层之后很多问题就迎刃而解了。比如为什么传大文件会卡是底层 MTU 限制导致的为什么有时候代码传了一半断了是中间层分片重组没做完为什么输出窗口有时候乱码是上层对回显数据的编码处理问题。3. 核心细节拆解BLE 通信、MicroPython 端配置与 IDE 交互3.1 ESP32 端的 BLE 服务是怎么搭起来的PyBLE 要在 ESP32 上跑前提是 ESP32 端得有一个服务端程序负责广播、接受连接、接收数据、执行代码、回传结果。这个程序本身也是 MicroPython 写的通常是一个main.py或者boot.py开机自动运行。核心用到的是 MicroPython 的ubluetooth模块。下面这段是我根据常见实践整理的最小服务端骨架实际 PyBLE 的固件会更完整但逻辑是一致的import bluetooth import ustruct _IRQ_CENTRAL_CONNECT 1 _IRQ_CENTRAL_DISCONNECT 2 _IRQ_GATTS_WRITE 3 # 自定义服务 UUID 和特征 UUID _SERVICE_UUID bluetooth.UUID(6E400001-B5A3-F393-E0A9-E50E24DCCA9E) _RX_CHAR (bluetooth.UUID(6E400002-B5A3-F393-E0A9-E50E24DCCA9E), bluetooth.FLAG_WRITE | bluetooth.FLAG_WRITE_NO_RESPONSE) _TX_CHAR (bluetooth.UUID(6E400003-B5A3-F393-E0A9-E50E24DCCA9E), bluetooth.FLAG_NOTIFY) _SERVICE (_SERVICE_UUID, (_RX_CHAR, _TX_CHAR)) class BLEUART: def __init__(self, ble, namePyBLE): self._ble ble self._ble.active(True) self._ble.irq(self._irq) ((self._rx_handle, self._tx_handle),) self._ble.gatts_register_services((_SERVICE,)) self._connections set() self._payload bytearray() self._advertise(name) def _irq(self, event, data): if event _IRQ_CENTRAL_CONNECT: conn_handle, _, _ data self._connections.add(conn_handle) elif event _IRQ_CENTRAL_DISCONNECT: conn_handle, _, _ data self._connections.discard(conn_handle) self._advertise() elif event _IRQ_GATTS_WRITE: conn_handle, attr_handle data if attr_handle self._rx_handle: chunk self._ble.gatts_read(self._rx_handle) self._payload.extend(chunk) self._try_execute() def _advertise(self, nameNone): name name or PyBLE payload bytearray() # 简化版广播数据构造 payload ustruct.pack(BB, 2, 0x01) b\x06 payload ustruct.pack(BB, len(name) 1, 0x09) name.encode() self._ble.gap_advertise(100000, adv_datapayload) def send(self, data): for conn_handle in self._connections: self._ble.gatts_notify(conn_handle, self._tx_handle, data)这段代码里有几个关键点值得展开说。UUID 的选择。上面用的6E400001-...是 Nordic UART ServiceNUS的 UUID这是 BLE 串口通信里事实上的通用标准。用这套 UUID 的好处是很多现成的 BLE 调试 App 都能直接识别方便你排查问题。PyBLE 如果自定义 UUID那平板端就必须用配套的 App灵活性差一些。RX 和 TX 的方向。这里容易搞混RX 是平板发给 ESP32的方向所以对 ESP32 来说是接收特征值属性是 WRITETX 是ESP32 发给平板的方向对 ESP32 来说是发送属性是 NOTIFY。命名是从 ESP32 视角来的别搞反了。IRQ 回调机制。MicroPython 的 BLE 是事件驱动的连接、断开、收到数据都会触发_irq。收到数据后不能直接执行因为一次写过来的可能只是半截代码得先攒着等收到传输结束标志再执行。这就是_payload缓冲区和_try_execute的作用。3.2 数据分片与重组BLE 传输的核心难点BLE 的 MTU最大传输单元默认是 23 字节扣掉 3 字节的 ATT 头实际能用的有效载荷只有 20 字节。虽然可以在连接后协商更大的 MTUESP32 上一般能协商到 517实际有效 512 左右但为了兼容性很多实现还是按 20 字节来分片。假设你要传一个 2000 字节的脚本按 20 字节分片就是 100 个包。每个包都要经过平板写特征值 → BLE 协议栈 → ESP32 收到 → 触发 IRQ这一套流程。如果平板发得太快ESP32 处理不过来就会丢包。所以中间层协议必须解决两个问题分片顺序和流控。分片顺序靠序号解决。每个包前面加一个字节的序号接收端按序号拼接。序号循环使用0-255 循环一圈。如果发现序号跳变说明中间丢了包得请求重传或者直接报错。流控靠确认机制解决。平板发一个包等 ESP32 回一个 ACK再发下一个。这种方式最稳但速度慢。更高效的做法是窗口机制一次发 N 个包等 ESP32 确认收到第 N 个后再发下一批。PyBLE 具体用哪种取决于实现版本但原理不外乎这两类。我在实际使用中遇到过一个典型问题传大脚本时偶尔会卡住。排查下来发现是流控窗口设得太大ESP32 的 BLE 缓冲区溢出导致某个包丢了后面的包序号对不上整个传输就挂住了。解决办法是把窗口调小或者增加超时重传。这个细节在文档里通常不会写但实际用起来很关键。3.3 平板端 IDE 的交互设计平板端的 IDE 是 PyBLE 的门面它的设计直接决定了用起来顺不顺手。我总结下来一个合格的移动端 MicroPython IDE 需要具备这几个能力。代码编辑器。平板上打字本来就难受所以编辑器必须支持语法高亮、自动缩进、括号匹配。MicroPython 对缩进敏感一个 Tab 和四个空格的混用就能让代码报错编辑器如果能自动统一缩进能省很多事。另外代码补全也很重要machine、network、bluetooth这些模块的常用 API 如果能提示写起来快很多。文件管理。ESP32 的文件系统里可能有多个文件main.py、boot.py、各种库文件。IDE 需要能列出这些文件、查看内容、新建、删除、重命名。传输的时候要能选择覆盖还是追加这个细节很实用——调试的时候经常需要在现有文件基础上改几行而不是整个替换。连接管理。扫描附近的 BLE 设备、显示设备名称和信号强度、连接、断开、重连。这里有个坑ESP32 的 BLE 名称如果设得太通用比如都叫 ESP32附近有多个设备时就分不清哪个是哪个。建议在固件里把名称设成带唯一标识的比如 PyBLE-A3F2后四位是 MAC 地址的一部分。输出窗口。这是调试的关键。ESP32 执行代码后的print输出、异常信息、REPL 回显都要实时显示在这里。BLE 的 NOTIFY 是异步的输出窗口要做好缓冲和刷新不然高频输出时会卡顿。另外输出内容要能滚动、能清空、能复制方便你把报错信息拷出来搜索。运行控制。至少要有运行当前脚本停止执行软重启三个按钮。软重启对应的是machine.reset()或者 REPL 里的 Ctrl-D能把 ESP32 恢复到初始状态重新跑main.py。调试死循环的时候这个按钮能救命。4. 完整实操流程从零把 PyBLE 跑起来4.1 准备工作硬件、固件与工具清单先把需要的东西列清楚免得做到一半发现缺东西。类别具体项说明硬件ESP32 开发板任意支持 BLE 的型号ESP32-WROOM、ESP32-S3 都行硬件平板或手机Android 或 iOS支持 BLE 4.0 以上软件MicroPython 固件从官网下载对应 ESP32 型号的 .bin 文件软件烧录工具esptool.py 或 Flash Download Tool软件PyBLE 应用从项目 Release 页面获取或自行编译软件串口终端首次烧录和排查问题时用如 PuTTY、minicom这里要特别提醒一点不是所有 ESP32 都支持 BLE。ESP32 系列里ESP32-S2 是没有蓝牙的只有 WiFi。ESP32、ESP32-S3、ESP32-C3 都支持 BLE。买板子的时候看清楚型号别买错了。4.2 烧录 MicroPython 固件这一步和常规的 ESP32 MicroPython 烧录没区别但有几个细节要注意。首先擦除 Flash。如果板子上之前跑的是 Arduino 程序或者其他固件直接烧 MicroPython 可能会因为分区表不兼容而启动失败。稳妥的做法是先全片擦除esptool.py --chip esp32 --port /dev/ttyUSB0 erase_flashWindows 上端口名是COM3之类的Mac 上是/dev/tty.usbserial-xxxxLinux 上是/dev/ttyUSB0。端口号用esptool.py chip_id或者系统设备管理器确认。然后烧录固件esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 esp32-20240602-v1.23.0.bin0x1000是 ESP32 的固件起始地址这个不能错。ESP32-S3 和 C3 的起始地址可能不同一般是0x0具体看固件发布页的说明。烧完之后用串口终端连上去波特率 115200按一下板子上的 EN 键或者重新上电应该能看到 MicroPython 的 REPL 提示符。看到这个就说明固件跑起来了。注意有些 ESP32 板子需要按住 BOOT 键再按 EN 键才能进入下载模式烧录完成后松开 BOOT 再按一次 EN 才能正常运行。这个操作因板子而异第一次用建议查一下板子的原理图。4.3 部署 PyBLE 服务端脚本固件烧好后需要把 PyBLE 的服务端脚本放到 ESP32 上。有两种方式。方式一通过串口上传。用ampy或者mpremote这类工具把服务端脚本传到板子上命名为main.py这样开机就会自动运行。mpremote connect /dev/ttyUSB0 fs cp pyble_server.py :main.py方式二直接在 REPL 里粘贴。如果脚本不长可以直接在串口终端里进 REPL把代码粘贴进去然后Ctrl-D软重启。但这种方式不适合长脚本容易粘贴出错。传完之后软重启板子服务端脚本就会开始运行ESP32 会进入 BLE 广播状态。这时候用平板的蓝牙扫描应该能看到一个名为 PyBLE 或者类似名称的设备。4.4 平板端连接与首次调试打开 PyBLE 应用进入设备扫描界面找到你的 ESP32点击连接。连接成功后应用一般会显示已连接状态并自动读取 ESP32 上的文件列表。第一次连接可能会遇到几个问题我按概率从高到低列一下。扫描不到设备。先确认 ESP32 的服务端脚本真的在跑——用串口终端看有没有报错。再确认平板的蓝牙已开启并且应用有蓝牙权限Android 6.0 以上需要位置权限才能扫描 BLE。如果还不行把 ESP32 和平板的距离拉近到 1 米内试试。连接后立刻断开。这通常是服务端脚本的 IRQ 处理有问题比如连接事件里做了耗时操作导致 BLE 协议栈超时。检查_irq回调里有没有print大量内容或者sleep。连上了但传不了代码。检查 RX 和 TX 的 UUID 是否和平板端配置的一致。如果用的是自定义 UUID两边必须完全匹配。另外确认特征值的属性设置正确RX 是 WRITETX 是 NOTIFY。传了代码但不执行。检查服务端的传输结束判断逻辑。有些实现是靠特定结束符比如\x04触发执行如果平板端没发这个结束符ESP32 就会一直等代码攒在缓冲区里不执行。4.5 一个完整的调试示例假设我要在 ESP32 上跑一个 BLE 广播 温度读取的脚本用 PyBLE 的流程是这样的。先在平板上新建一个文件写代码import bluetooth import time from machine import ADC, Pin # 读取内部温度传感器部分型号支持 def read_temp(): adc ADC(Pin(34)) adc.atten(ADC.ATTN_11DB) raw adc.read() # 简化的电压转温度估算实际需按传感器手册校准 voltage raw / 4095 * 3.3 temp (voltage - 0.5) * 100 return temp while True: t read_temp() print(Temperature:, t) time.sleep(2)写完后点运行PyBLE 会把代码分片传给 ESP32ESP32 收到后执行。输出窗口会每两秒打印一次温度值。如果代码有语法错误输出窗口会显示SyntaxError和行号你回到编辑器改完再运行就行。这个过程听起来简单但实际用的时候输出窗口的实时性是最影响体验的。BLE 的 NOTIFY 有延迟如果 ESP32 打印太快输出会积压。我的经验是调试阶段在print之间加个time.sleep(0.1)让 BLE 有时间把数据发出去输出会流畅很多。5. 常见问题与排查技巧实录5.1 BLE 连接类问题速查现象可能原因排查方法解决方式扫描不到设备服务端未运行串口看 REPL 输出重新上传 main.py 并软重启扫描不到设备权限不足检查应用权限授予蓝牙和位置权限连接后立即断开IRQ 阻塞检查回调函数移除回调里的耗时操作连接不稳定距离过远靠近设备保持在 5 米内连接不稳定2.4G 干扰换环境测试远离路由器、微波炉重复连接失败连接数满重启 ESP32软重启释放连接槽BLE 连接类问题里连接后立即断开是最常见的。我踩过好几次最后发现都是 IRQ 回调里写了print导致的。MicroPython 的print在 BLE 中断上下文里执行会阻塞协议栈超过几百毫秒就会触发 BLE 超时断开。解决办法是把要打印的内容存到一个队列里在主循环里再输出。5.2 代码传输类问题传输中断。前面提过大文件传输时如果流控没做好容易丢包。判断方法是看输出窗口有没有传输超时或者序号错误的提示。解决方式是减小分片大小、增大重传次数、或者把大脚本拆成多个小文件。传输内容不完整。有时候代码传过去了但最后几行丢了。这通常是传输结束标志没发出去或者 ESP32 在收到结束标志前就执行了。检查平板端的发送逻辑确保所有分片发完后再发结束标志。中文注释乱码。MicroPython 默认用 UTF-8 编码但如果平板端发送时用了其他编码中文注释就会乱码。确保编辑器保存文件时用 UTF-8传输时不要做额外的编码转换。5.3 代码执行类问题代码不执行。先确认传输是否完整再看 ESP32 的 REPL 有没有报错。如果 REPL 里手动粘贴同样的代码能跑那就是传输环节的问题。执行到一半卡死。多半是代码里有死循环或者阻塞操作。这时候用 PyBLE 的停止按钮发一个中断信号对应 REPL 的 Ctrl-C或者直接软重启。如果停止按钮也没反应就只能断电重启了。内存不足。ESP32 的 RAM 有限MicroPython 跑复杂脚本时可能报MemoryError。解决办法是精简代码、少用大列表、及时gc.collect()。PyBLE 的 IDE 如果能显示 ESP32 的内存占用对排查这类问题很有帮助。5.4 我踩过的三个坑第一个坑UUID 写反了。我第一次自己改服务端脚本时把 RX 和 TX 的 UUID 搞反了结果平板能连上但发什么都收不到。排查了半天才反应过来。记住RX 是 ESP32 接收TX 是 ESP32 发送从 ESP32 视角命名。第二个坑MTU 协商失败。有些平板的 BLE 协议栈不支持大 MTU 协商实际还是按 23 字节走。如果你的分片逻辑假设 MTU 是 512就会出错。稳妥的做法是连接后先读一下实际协商的 MTU再决定分片大小。第三个坑文件系统写满。ESP32 的 Flash 分区里给文件系统的空间有限反复上传大文件可能把空间占满导致新文件写不进去。定期清理不用的文件或者用os.statvfs()查看剩余空间。6. 这套方案还能怎么扩展PyBLE 目前的能力集中在编辑-传输-执行-回显这个闭环上但它的底层是 BLE GATT 通信理论上可以扩展出更多玩法。比如远程文件同步。现在是从平板推代码到 ESP32反过来也可以ESP32 把运行日志、传感器数据写进文件平板通过 BLE 拉取下来。这样就能做长时间的数据采集不用一直连着串口。再比如多设备管理。一个平板同时连接多个 ESP32分别给它们推不同的代码做批量部署。这在做传感器网络的时候很有用。BLE 理论上支持一个 Central 连多个 Peripheral但实际能连几个取决于平板的协议栈一般 4-7 个。还有OTA 升级的轻量替代。传统 OTA 走 WiFi需要配网、需要服务器。用 BLE 传固件虽然慢但对于小体积的 MicroPython 脚本完全可以当成轻量 OTA来用。现场设备出问题拿平板过去连一下推个修复脚本比拆机接线快多了。我自己最期待的是代码片段库功能。把常用的 BLE 初始化、WiFi 连接、传感器读取这些代码做成模板平板上点一下就能插入改几个参数就能用。对于不熟悉 MicroPython API 的人来说这能大幅降低门槛。不过话说回来PyBLE 这类工具再方便也替代不了电脑上的完整 IDE。复杂项目还是得在电脑上写、用 Git 管理、跑单元测试。PyBLE 的定位是现场调试的补充不是全流程开发的主力。想清楚这一点用起来心态就对了。我在实际使用中的体会是BLE 调试最大的价值不是方便而是降低试错成本。以前改一行代码要接线、开软件、等连接一套下来两分钟现在平板拿起来就连改完就推十秒钟搞定。这种即时反馈对调试效率的提升比想象中大得多。尤其是调 BLE 相关代码的时候本身就在跟蓝牙打交道用蓝牙来调试逻辑上还更自洽一些。
