一半是 WiFi 蓝牙仲裁打架,一半是 NimBLE 底层库 bug
WiFi 密码错了、路由器暂时不可达时,ESP32-S3 会一直重连。这时打开配网 App,蓝牙能连上,通知也能订阅,点“扫描 WiFi 列表”却没有任何回包。
这件事不是单一故障。WiFi 重连时,共存仲裁器先把蓝牙射频挤掉,一条读对端版本的 HCI 命令失败;NimBLE 再把这条失败状态当成连接失败,BluFi 于是把发送通道自己关了。前一半是仲裁打架,后一半是底层库 bug。
现象:连得上,发不出
环境是 ESP-IDF v6.1-beta1,NimBLE,BluFi 配网。WiFi 一次就能连上时,整条配网流程正常。一旦 STA 进入反复重连,故障就变成状态相关的:
- 手机能扫到设备,能连上配网 BLE,也能订阅通知。
- 手机写下发命令,设备收得到、也处理得了。
- 设备该回给手机的数据全部消失:WiFi 列表没有,状态报告也没有。
也就是说链路层是通的,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。连接其实已经建立了,失败的只是事后读版本。
后果: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 都还没带上。
现在能用的两步处理
两层原因对应两步处理:
- 修库 bug:IDF 本地补丁,slave 分支里把
ble_gap_event_connect_call(ev->conn_handle, ev->status)改成ble_gap_event_connect_call(ev->conn_handle, 0),和上游语义对齐。 - 躲开仲裁打架:手机明确请求扫描 WiFi 列表(
GET_WIFI_LIST)时,wifi_manager_suspend_sta()先挂起 STA 自动重连,SCAN_DONE立刻恢复。普通退避重连不要动。
升级到带 NimBLE 1.9.0 的 IDF 后,本地补丁可以拿掉。补丁改的是 IDF 安装目录,升级会丢,更新后要重新打。
补丁前后对照
用 PC 蓝牙模拟手机(bleak 直连,按 BluFi 帧格式发 GET_WIFI_LIST):
- 补丁前:PC 收到 0 条通知。
- 补丁后:收到 WiFi 列表通知,158 字节,里面是真实 SSID。
根因和修复都在硬件上对过。WiFi 重连时发不出蓝牙包,先同时看两件事:仲裁器有没有把读版本 HCI 挤失败,以及 NimBLE 有没有把这次失败写进连接事件。