- 添加s_lastLoggedError数组跟踪各通道最后记录的错误计数 - 实现错误计数变化时才触发告警,避免重复打印 - 扩展错误日志信息,包含TEC、REC和总线状态详细信息 - 修复P0/P1页面携带相同统计数据时的重复打印问题 perf(ota): 移除冗余的OTA协调器状态检查方法 - 删除isGd32BusyState、isEsp32BusyState等未使用的状态判断方法 - 移除esp32FinalizeErrCategory错误分类函数 - 清理相关的头文件声明,减少代码冗余 refactor(relay): 移除未使用的中继状态获取方法 - 删除getRelaySeq、getRelayTotalPackets状态查询接口 - 移除getGd32Version版本获取方法 - 清理StateLockGuard中的isLocked方法
6.0 KiB
6.0 KiB
修复 ESP32 OTA 重启 UI 竞态问题
问题描述
前端显示 SHA-256 校验通过,设备自动重启中... 消息后,"重启中"阶段 UI 不会稳定触发(仅偶发触发一次)。
根因分析
后端时序(http_handlers.cpp + ota_coordinator.cpp):
httpd_resp_send()发送 HTTP 响应(L148)vTaskDelay(500ms)等待 TCP 刷新(L151)restartEsp32()→notifyEsp32Rebooting()+vTaskDelay(500ms)+esp_restart()(L152)
即:HTTP 响应发出后,设备仍存活约 1000ms 才真正重启。
前端竞态(app.js):
verifyEsp32Firmware()收到/ota/update成功响应后,在 L1496 设置isEsp32Rebooting = true,L1497 推进阶段到 5"重启中"。- 此时轮询以 1s 间隔运行(上传结束后
resumePollingAfterUpload()已恢复 1s 间隔)。 - 在 ~1000ms 的 pre-reboot 窗口内,设备仍能响应
/system/status轮询。 pollStatus()成功路径(app.js:1740-1745)发现isEsp32Rebooting === true,立即清除标志并推进到阶段 6"启动完成"+ 弹出"ESP32 已恢复在线" —— 全部错误。- 当设备 ~1s 后真正重启时,轮询失败,但
isEsp32Rebooting已被清为 false,失败分支走"设备忙"或"未连接",永远不会再显示"重启中"。
偶发触发的原因:仅当轮询时序恰好使得 pre-reboot 窗口内没有成功的轮询(例如轮询正在 in-flight 且设备重启打断它)时,"重启中"UI 才能正确显示。
修复方案
引入 rebootObservedDown 标志,要求"先观察到设备下线,再接受其恢复在线",避免在 pre-reboot 窗口内被成功轮询误清除重启状态。
改动文件:tools/app.js
1. 新增全局标志(L28 附近)
在 let rebootStartTime = 0; 下方新增:
let rebootObservedDown = false; // 是否已观察到设备实际下线(重启)
2. arming 点 1:verifyEsp32Firmware() 成功路径(L1496-L1498)
isEsp32Rebooting = true;
rebootObservedDown = false; // ← 新增
updateEsp32StagesFromPhase(5); // 重启中
rebootStartTime = Date.now();
3. arming 点 2:restartEsp32() 手动重启(L1531-L1532)
isEsp32Rebooting = true;
rebootObservedDown = false; // ← 新增
rebootStartTime = Date.now();
4. pollStatus() 成功路径(L1740-L1746)—— 核心修复
将无条件清除改为"先确认设备下线过再清除":
if (isEsp32Rebooting) {
var rbElapsed = Math.round((Date.now() - rebootStartTime) / 1000);
if (!rebootObservedDown && rbElapsed < 10) {
// pre-reboot 窗口:设备尚未实际重启(后端有 ~1s 延迟才 esp_restart)
// 保持"重启中"状态,不清除标志,不推进阶段
addLog('I', '系统', '设备尚未实际重启,等待 esp_restart...');
} else {
// 设备已重启并恢复在线(rebootObservedDown=true)
// 或超 10s 安全阈值仍未观察到下线(重启过快未捕获或未重启)——接受恢复
isEsp32Rebooting = false;
rebootObservedDown = false;
updateEsp32StagesFromPhase(6); // 启动完成
addLog('I', '系统', 'ESP32 已恢复在线');
showToast('ESP32 已恢复在线', 'success');
}
}
try { updateSystemStatusUi(data); } catch (e2) { console.warn('updateSystemStatusUi 异常:', e2); }
// pre-reboot 窗口内 updateSystemStatusUi 会把连接胶囊刷成"已连接",需覆盖回"重启中"
if (isEsp32Rebooting) {
applyRebootingRuntimeState();
}
5. pollStatus() 失败路径 isEsp32Rebooting 分支(L1781-L1791)—— 标记已下线
} else if (isEsp32Rebooting) {
rebootObservedDown = true; // ← 新增:确认设备已下线(正在重启)
var elapsed = Math.round((Date.now() - rebootStartTime) / 1000);
if (elapsed > 60) {
isEsp32Rebooting = false;
rebootObservedDown = false; // ← 新增
addLog('E', '系统', 'ESP32 重启超时,设备未恢复');
showToast('ESP32 重启超时,请检查设备', 'error');
updateConnectionUi(false, '未连接');
applyDisconnectedRuntimeState();
} else {
applyRebootingRuntimeState();
}
}
时序验证(修复后)
| 时间 | 事件 | isEsp32Rebooting | rebootObservedDown | UI |
|---|---|---|---|---|
| t=0 | 收到 /ota/update 响应,arming | true | false | 阶段5"重启中" |
| t=0~1s | pre-reboot 窗口,轮询成功 | true | false | 保持"重启中"(不清除)✓ |
| t=1s | esp_restart(),设备下线 | true | false | — |
| t=1~2s | 轮询失败 | true | true | "重启中 Ns" ✓ |
| t=3~4s | 设备重启完成,轮询成功 | → false | → false | 阶段6"启动完成" ✓ |
假设与决策
- 10s 安全阈值:ESP32 重启(boot ROM + bootloader + app init + WiFi init)通常 1-3s,1s 间隔轮询至少 1-2 次失败,
rebootObservedDown必然被设置。10s 阈值仅用于兜底(设备未实际重启或重启极快未捕获到下线)。 - 不修改后端时序:后端 1000ms 延迟(500ms TCP 刷新 + 500ms reboot 通知窗口)是必要的(确保响应送达 + 状态上报),问题在前端竞态处理。
- 仅修改 WiFi 路径:BLE 路径使用事件驱动(0x07 REBOOT_NOW + 断连重连),不受此竞态影响。
验证步骤
- 通过 WiFi 上传 ESP32 固件并完成 SHA-256 校验
- 确认显示
SHA-256 校验通过,设备自动重启中...后,阶段稳定停留在 5"重启中",连接胶囊显示"重启中 Ns"计数 - 不应出现阶段跳到 6"启动完成"再回退的情况
- 等待设备实际重启完成,确认阶段推进到 6"启动完成"并弹出"ESP32 已恢复在线"
- 多次重复验证(此前为偶发问题,修复后应 100% 稳定)