fix: 修复:改进OTA错误处理和BLE OTA性能

- 重构app.js中的OTA错误处理,以准确报告获取失败而不假设重启,从而实现正确的状态轮询。
- 通过将握手状态封装在SpiLinkState类中来增强SPI主状态管理,提高清晰度并减少直接的状态操作。
 引入otaSendAndPoll方法,以更好地处理SPI通信中的命令发送和响应轮询。
- 调整BLE OTA数据包确认逻辑,防止导致固件录变慢的虚假确认,确保仅在实际取得进展时才发送确认。
- 修改确认超时设置,以更好地适应闪存写入速度,减少不重传。
This commit is contained in:
小龙虾
2026-07-09 02:49:24 +08:00
parent 661ae54424
commit fedb08e215
16 changed files with 432 additions and 1369 deletions
@@ -0,0 +1,121 @@
# 修复 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` 探测到设备无响应 → 降速诊断 → 用户看到"未连接"而非"重启中"