一半是 WiFi 蓝牙仲裁打架,一半是 NimBLE 底层库 bug

WiFi 密码错了、路由器暂时不可达时,ESP32-S3 会一直重连。这时打开配网 App,蓝牙能连上,通知也能订阅,点“扫描 WiFi 列表”却没有任何回包。

这件事不是单一故障。WiFi 重连时,共存仲裁器先把蓝牙射频挤掉,一条读对端版本的 HCI 命令失败;NimBLE 再把这条失败状态当成连接失败,BluFi 于是把发送通道自己关了。前一半是仲裁打架,后一半是底层库 bug。

现象:连得上,发不出

环境是 ESP-IDF v6.1-beta1,NimBLE,BluFi 配网。WiFi 一次就能连上时,整条配网流程正常。一旦 STA 进入反复重连,故障就变成状态相关的:

也就是说链路层是通的,GATT 写是通的,唯独板子主动发给手机的蓝牙包发不出去。

第一层:WiFi 重连把蓝牙射频挤掉

STA 反复重连时,Wi-Fi/BT 共存仲裁器会压制蓝牙无线。外设刚连上,NimBLE 还要补发一条 LE Read Remote Version Information,去读对端版本。这条 HCI 交换正好撞上仲裁窗口,控制器回 HCI 错误 0x1A(26,Unsupported Feature or Parameter Value)。

如果只是空口被挤占,收和发都应该一起变差。现在接收还在,只有板子往外发的包没了,说明射频干扰只是第一层。真正把发送通道关掉的,是后面那条库逻辑。

第二层:NimBLE 把读版本失败当成连接失败

BLE_GAP_EVENT_CONNECT 的 status 在 NimBLE 里有多处赋值。逐一排除后,只有 ble_gap_rx_rd_rem_ver_info_complete() 会把任意 HCI 状态原样带进连接事件。

读版本 HCI 事务完成后,完成事件里的 ev->status 被直接当成连接事件 status,丢给广播回调,也就是 esp_blufi 的 GAP 回调。仲裁干扰让这条命令失败后,esp_blufi 打出 connection failed; status=26。连接其实已经建立了,失败的只是事后读版本。

连接事件 status 的来源与污染路径

后果:is_connected 恒为 false

esp_blufi 用一个布尔量 is_connected 判断能不能往手机发数据。它只在连接事件 status == 0 时置位。连接事件被污染后,这个标志永远是 false。

btc_blufi_send_encap() 看到 false 直接返回。状态报告、WiFi 列表,所有板子到手机的数据都被静默丢掉。接收方向不查这个标志,所以手机写进来的请求照常处理。表现就是“连得上、发不出”。

上游已经修了库 bug,但还没进当前 IDF

相关 issue 是 espressif/esp-idf #18342 "Blufi connect error in Nimble",同属 BluFi + NimBLE 连接错误这一块。

这次定位到的具体路径,已经在上游 mynewt-nimble 修掉:连接事件 status 固定为 0,读版本、读特性改成连接事件投递之后的独立 HCI 事务,失败不再污染连接状态。ESP-IDF 对应迁移是 e08609e451 "Migrate to NimBLE 1.9.0"(2026-07-25)。v6.1-beta1、v6.0.2、v6.1-rc1 都还没带上。

现在能用的两步处理

两层原因对应两步处理:

升级到带 NimBLE 1.9.0 的 IDF 后,本地补丁可以拿掉。补丁改的是 IDF 安装目录,升级会丢,更新后要重新打。

补丁前后对照

用 PC 蓝牙模拟手机(bleak 直连,按 BluFi 帧格式发 GET_WIFI_LIST):

根因和修复都在硬件上对过。WiFi 重连时发不出蓝牙包,先同时看两件事:仲裁器有没有把读版本 HCI 挤失败,以及 NimBLE 有没有把这次失败写进连接事件。