Files
ESP32_COMM/.trae/documents/fix-ota-fetch-error-misjudgment.md
T
小龙虾 fedb08e215 fix: 修复:改进OTA错误处理和BLE OTA性能
- 重构app.js中的OTA错误处理,以准确报告获取失败而不假设重启,从而实现正确的状态轮询。
- 通过将握手状态封装在SpiLinkState类中来增强SPI主状态管理,提高清晰度并减少直接的状态操作。
 引入otaSendAndPoll方法,以更好地处理SPI通信中的命令发送和响应轮询。
- 调整BLE OTA数据包确认逻辑,防止导致固件录变慢的虚假确认,确保仅在实际取得进展时才发送确认。
- 修改确认超时设置,以更好地适应闪存写入速度,减少不重传。
2026-07-09 02:49:24 +08:00

122 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 修复 OTA 校验请求异常误判为"设备已重启"Plan
## Summary
当前端在 OTA 校验/应用阶段(`POST /ota/update`)发生 fetch 网络异常(如 "Failed to fetch")时,代码无条件假设"设备已重启"并进入 60 秒重启等待。但服务端实际要先完成 `finalizeForReboot`(SHA 计算可能耗时数十秒)才发送响应、再延迟 500ms 重启。fetch 异常发生时设备可能仍在 finalize、可能崩溃、也可能只是网络抖动——并非一定是正常重启。这种误判掩盖了真实失败原因,让用户长时间等待一个不会发生的"重启完成"。
需将 catch 块改为:**如实报告请求失败,不臆测设备状态,由后续 `pollStatus` 探测设备真实状态**。
## Current State Analysis
### 问题代码位置
- `tools/app.js` L1616-1623:`verifyEsp32Firmware()` 的 catch 块
- `tools/app.js` L1676-1681:`applyEsp32Firmware()` 的 catch 块(相同误判模式)
### 当前 catch 块行为(两处一致)
```javascript
} catch (e) {
addLog('W', 'OTA', '应用请求出现异常(设备可能已重启): ' + e.message);
// 设备在收到请求后即会重启,网络中断属于正常现象,直接进入重启等待
showVerifyActions('none');
isEsp32Rebooting = true; // ← 无条件假设重启
updateEsp32StagesFromPhase(5); // ← UI 切到"重启中"
rebootStartTime = Date.now();
}
```
### 服务端实际时序(`http_handlers.cpp` L137-153)
1. 收到 `/ota/update` → 调用 `finalizeForReboot()`(含 `computeShaFromPartition` 读回整分区算 SHA,大固件耗时数秒到十几秒)
2. finalize 成功 → 发送 HTTP 响应 `{"status":"ok",...}`
3. `vTaskDelay(500ms)` → `restartEsp32()` → `esp_restart()`
**只有第 2 步成功发送响应后设备才会重启**。fetch 异常说明响应未到达前端,设备大概率未正常重启。
### 误判的连锁影响
- 设置 `isEsp32Rebooting = true` → `pollStatus`(L1953-1963)进入 60 秒"重启中"等待分支
- 跳过 L1939 的"5 次失败诊断"逻辑,不会降速、不会显示"未连接"
- 60 秒后才报"ESP32 重启超时"——用户白等一分钟,且看不到真实失败原因
- 若设备实际仍在 finalize 或已崩溃,`/system/status` 也无法响应,但 UI 一直显示"重启中"
### 对照:上传阶段的正确处理(L1532-1537)
```javascript
xhr.addEventListener('error', function() {
cleanupAfterOtaFailure('网络错误');
});
```
上传阶段网络错误会调用 `cleanupAfterOtaFailure`,如实标记失败。校验/应用阶段应保持一致。
## Proposed Changes
### 文件:`g:\Robot_v2\DCU\ESP32_COMM\tools\app.js`
#### 改动 1:`verifyEsp32Firmware()` catch 块(L1616-1623)
**What**: 移除"假设设备已重启"的逻辑,改为如实报告请求失败,并启动轮询探测设备真实状态。
**Why**: fetch 异常只代表请求未成功完成,不能推断设备状态。设备可能仍在 finalize、可能已崩溃、可能网络抖动。应由 `pollStatus` 探测后给出准确结论。
**How**: 替换为:
```javascript
} catch (e) {
// fetch 异常仅代表请求未完成,不能推断设备已重启:
// 设备可能仍在 finalize(SHA 计算耗时)、可能已崩溃、可能网络抖动。
// 如实报告失败,由 pollStatus 探测设备真实状态。
addLog('E', 'OTA', '应用请求失败: ' + e.message + '(设备状态未知,等待轮询探测)');
showToast('应用请求失败: ' + e.message, 'error');
var resultEl = document.getElementById('sviResult');
if (resultEl) { resultEl.textContent = '✗ 请求失败: ' + e.message; resultEl.className = 'svi-val err'; }
showVerifyActions('retry');
cleanupAfterOtaFailure('应用请求失败');
}
```
要点:
- 日志级别 W → E(请求失败是错误,不是警告)
- 日志文案去掉"设备可能已重启"臆测,改为"设备状态未知,等待轮询探测"
- 不再设置 `isEsp32Rebooting = true`,让 `pollStatus` 走正常失败诊断路径
- 调用 `cleanupAfterOtaFailure` 标记失败态(与上传阶段错误处理一致)
- 显示 retry 按钮(`showVerifyActions('retry')`)让用户可重试
#### 改动 2:`applyEsp32Firmware()` catch 块(L1676-1681)
**What**: 同样的修复,移除重启假设。
**Why**: 与改动 1 相同的误判逻辑,需保持一致。
**How**: 替换为:
```javascript
} catch (e) {
// fetch 异常仅代表请求未完成,不能推断设备已重启。
addLog('E', 'OTA', '应用请求失败: ' + e.message + '(设备状态未知,等待轮询探测)');
showToast('应用请求失败: ' + e.message, 'error');
cleanupAfterOtaFailure('应用请求失败');
}
```
要点:
- 日志级别 W → E
- 不再设置 `isEsp32Rebooting = true`
- 调用 `cleanupAfterOtaFailure` 标记失败态
## Assumptions & Decisions
1. **不保留任何"假设重启"的快速路径**:即使用户确实遇到设备正常重启的场景,`pollStatus` 也会在设备恢复后正确识别(L1912-1917 的 `isEsp32Rebooting` 分支虽然不再被这两处设置,但其他正常路径如 `data.status === 'ok'` 仍会设置)。去掉误判路径不会影响正常重启的检测。
2. **依赖 `pollStatus` 探测真实状态**:`pollStatus` 已有完善的设备状态探测逻辑(L1939 的 5 次失败诊断、L1964 的 `lastDeviceBusy` 判断)。fetch 异常后让轮询接管,能在设备真重启时通过 `lastSysStateCode === 7`(ESP32_REBOOTING)或 `lastDeviceBusy` 保持 1s 轮询,在设备真断联时降速诊断。
3. **`showVerifyActions('retry')`**:调研报告提到 L179-182 的 `showVerifyActions('retry')` 函数体可能已空(按钮移除)。即使按钮不存在,调用也是安全的(DOM 查询返回 null 时不会报错)。保留调用以备未来恢复按钮。
4. **不修改服务端**:服务端的 finalize→响应→重启时序是合理的(先响应再重启,确保前端收到结果)。问题在前端错误推断。
5. **不改动 `verifyEsp32Firmware()` 成功路径(L1583-1606)的 `isEsp32Rebooting = true`**:成功路径下设备确实会重启(服务端已发送 ok 响应并执行 restart),此时设置重启等待是正确的。
## Verification
1. **代码审查**:确认两处 catch 块均已移除 `isEsp32Rebooting = true`、`rebootStartTime = Date.now()`、`updateEsp32StagesFromPhase(5)`,改为调用 `cleanupAfterOtaFailure`。
2. **成功路径不受影响**:确认 L1583-1606(校验通过)和 L1663-1669(应用成功)的成功路径仍正常设置 `isEsp32Rebooting`,设备正常重启时前端仍能正确显示重启等待。
3. **运行时验证**:
- 正常 OTA 升级:校验通过 → 设备重启 → `pollStatus` 检测到设备恢复 → 显示"ESP32 已恢复在线"(验证成功路径未受影响)
- 模拟网络中断(如 OTA 期间拔网线/关 WiFi):fetch 异常 → 日志显示"应用请求失败: ...(设备状态未知,等待轮询探测)" → `pollStatus` 走失败诊断路径 → 不再显示误导性的"重启中 Xs"
- 模拟设备 finalize 阶段崩溃:fetch 异常 → 同上 → `pollStatus` 探测到设备无响应 → 降速诊断 → 用户看到"未连接"而非"重启中"