feat(can): 优化CAN反馈任务错误日志记录机制
- 添加s_lastLoggedError数组跟踪各通道最后记录的错误计数 - 实现错误计数变化时才触发告警,避免重复打印 - 扩展错误日志信息,包含TEC、REC和总线状态详细信息 - 修复P0/P1页面携带相同统计数据时的重复打印问题 perf(ota): 移除冗余的OTA协调器状态检查方法 - 删除isGd32BusyState、isEsp32BusyState等未使用的状态判断方法 - 移除esp32FinalizeErrCategory错误分类函数 - 清理相关的头文件声明,减少代码冗余 refactor(relay): 移除未使用的中继状态获取方法 - 删除getRelaySeq、getRelayTotalPackets状态查询接口 - 移除getGd32Version版本获取方法 - 清理StateLockGuard中的isLocked方法
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
# 修复 ESP32 OTA 重启 UI 竞态问题
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
前端显示 `SHA-256 校验通过,设备自动重启中...` 消息后,"重启中"阶段 UI 不会稳定触发(仅偶发触发一次)。
|
||||||
|
|
||||||
|
## 根因分析
|
||||||
|
|
||||||
|
**后端时序**([http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L148-L152) + [ota_coordinator.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L550-L556)):
|
||||||
|
1. `httpd_resp_send()` 发送 HTTP 响应(L148)
|
||||||
|
2. `vTaskDelay(500ms)` 等待 TCP 刷新(L151)
|
||||||
|
3. `restartEsp32()` → `notifyEsp32Rebooting()` + `vTaskDelay(500ms)` + `esp_restart()`(L152)
|
||||||
|
|
||||||
|
即:**HTTP 响应发出后,设备仍存活约 1000ms 才真正重启**。
|
||||||
|
|
||||||
|
**前端竞态**([app.js](file:///g:/Robot_v2/DCU/ESP32_COMM/tools/app.js#L1496-L1498)):
|
||||||
|
1. `verifyEsp32Firmware()` 收到 `/ota/update` 成功响应后,在 L1496 设置 `isEsp32Rebooting = true`,L1497 推进阶段到 5"重启中"。
|
||||||
|
2. 此时轮询以 1s 间隔运行(上传结束后 `resumePollingAfterUpload()` 已恢复 1s 间隔)。
|
||||||
|
3. **在 ~1000ms 的 pre-reboot 窗口内,设备仍能响应 `/system/status` 轮询**。
|
||||||
|
4. `pollStatus()` 成功路径([app.js:1740-1745](file:///g:/Robot_v2/DCU/ESP32_COMM/tools/app.js#L1740-L1745))发现 `isEsp32Rebooting === true`,**立即清除标志并推进到阶段 6"启动完成"+ 弹出"ESP32 已恢复在线"** —— 全部错误。
|
||||||
|
5. 当设备 ~1s 后真正重启时,轮询失败,但 `isEsp32Rebooting` 已被清为 false,失败分支走"设备忙"或"未连接",**永远不会再显示"重启中"**。
|
||||||
|
|
||||||
|
**偶发触发的原因**:仅当轮询时序恰好使得 pre-reboot 窗口内没有成功的轮询(例如轮询正在 in-flight 且设备重启打断它)时,"重启中"UI 才能正确显示。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
引入 `rebootObservedDown` 标志,要求"先观察到设备下线,再接受其恢复在线",避免在 pre-reboot 窗口内被成功轮询误清除重启状态。
|
||||||
|
|
||||||
|
### 改动文件:[tools/app.js](file:///g:/Robot_v2/DCU/ESP32_COMM/tools/app.js)
|
||||||
|
|
||||||
|
#### 1. 新增全局标志(L28 附近)
|
||||||
|
|
||||||
|
在 `let rebootStartTime = 0;` 下方新增:
|
||||||
|
```js
|
||||||
|
let rebootObservedDown = false; // 是否已观察到设备实际下线(重启)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. arming 点 1:`verifyEsp32Firmware()` 成功路径(L1496-L1498)
|
||||||
|
|
||||||
|
```js
|
||||||
|
isEsp32Rebooting = true;
|
||||||
|
rebootObservedDown = false; // ← 新增
|
||||||
|
updateEsp32StagesFromPhase(5); // 重启中
|
||||||
|
rebootStartTime = Date.now();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. arming 点 2:`restartEsp32()` 手动重启(L1531-L1532)
|
||||||
|
|
||||||
|
```js
|
||||||
|
isEsp32Rebooting = true;
|
||||||
|
rebootObservedDown = false; // ← 新增
|
||||||
|
rebootStartTime = Date.now();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. `pollStatus()` 成功路径(L1740-L1746)—— 核心修复
|
||||||
|
|
||||||
|
将无条件清除改为"先确认设备下线过再清除":
|
||||||
|
|
||||||
|
```js
|
||||||
|
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)—— 标记已下线
|
||||||
|
|
||||||
|
```js
|
||||||
|
} 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 + 断连重连),不受此竞态影响。
|
||||||
|
|
||||||
|
## 验证步骤
|
||||||
|
|
||||||
|
1. 通过 WiFi 上传 ESP32 固件并完成 SHA-256 校验
|
||||||
|
2. 确认显示 `SHA-256 校验通过,设备自动重启中...` 后,阶段稳定停留在 5"重启中",连接胶囊显示"重启中 Ns"计数
|
||||||
|
3. 不应出现阶段跳到 6"启动完成"再回退的情况
|
||||||
|
4. 等待设备实际重启完成,确认阶段推进到 6"启动完成"并弹出"ESP32 已恢复在线"
|
||||||
|
5. 多次重复验证(此前为偶发问题,修复后应 100% 稳定)
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 死函数清理与兼容标记计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
逐函数扫描 `main/`(C++)、`tools/`(JS)及根目录 Python 脚本,识别:
|
||||||
|
1. **死函数**:已定义但无调用、未注册、未绑定,直接清理。
|
||||||
|
2. **旧版本兼容函数**:仍被调用但无实际作用,或注释/命名表明为兼容保留,进行明确标记(保留并加注释,或连同调用点一起移除)。
|
||||||
|
|
||||||
|
扫描已完成,共覆盖约 349 个 C++ 定义、~130 个 JS 函数、2 个 Python 脚本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、C++ 死函数(建议删除)
|
||||||
|
|
||||||
|
| # | 函数/方法 | 文件 | 行 | 说明 |
|
||||||
|
|---|-----------|------|----|------|
|
||||||
|
| 1 | `esp_err_t ota_prepare_handler(httpd_req_t *req)` | `main/server/http_handlers.cpp` | 202 | 完整实现,但 `wifi_ota.cpp` 的 `kHttpRoutes` 未注册 `/ota/prepare`,无任何调用。 |
|
||||||
|
| 2 | `bool WifiOta::getGd32StorageDone() const` | `main/server/wifi_ota.h` | 73 | 内联 getter;`setGd32StorageDone()` 在用,getter 无调用。 |
|
||||||
|
| 3 | `bool OtaRelay::StateLockGuard::isLocked() const` | `main/relay/ota_relay.h` | 241 | 公共嵌套类方法,无调用。 |
|
||||||
|
| 4 | `int canFdSendNoReply(...)` | `main/transport/canfd_bridge.cpp/h` | 93/61 | 无调用者。 |
|
||||||
|
| 5 | `int canFdPullIdSnapshot(...)` | `main/transport/canfd_bridge.cpp/h` | 183/158 | 无调用者。 |
|
||||||
|
| 6 | `bool getLatestIdSnapshot(...)` | `main/monitor/can_feedback_task.cpp/h` | 156/44 | 无调用者;`getLatestCanFeedback()` 已覆盖同一需求。 |
|
||||||
|
| 7 | `const char* OtaCoordinator::getSystemStateString() const` | `main/ota/ota_coordinator.cpp/h` | 711/438 | 注释已写明“保持向后兼容”,但无任何调用。 |
|
||||||
|
| 8 | `int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)` | `main/ota/ota_coordinator.cpp/h` | 563/466 | 声明、定义齐全,无调用。 |
|
||||||
|
| 9 | `bool OtaCoordinator::isGd32Busy() const` | `main/ota/ota_coordinator.cpp/h` | 680/424 | 无调用;`isGd32BusyState()` 仍保留供内部使用。 |
|
||||||
|
| 10 | `bool OtaCoordinator::isEsp32Busy() const` | `main/ota/ota_coordinator.cpp/h` | 686/431 | 无调用;`isEsp32BusyState()` 仍保留。 |
|
||||||
|
| 11 | `bool OtaCoordinator::isGd32StorageDone() const` | `main/ota/ota_coordinator.cpp/h` | 723/? | 无调用;`getSnapshot()` 直接读取 `m_gd32StorageDone`。 |
|
||||||
|
| 12 | `static void OtaRelay::getGd32Version(gd32_version_t *out)` | `main/relay/ota_relay.cpp/h` | 144/156 | 无调用;`getSnapshot()` 已包含 version。 |
|
||||||
|
| 13 | `static uint32_t OtaRelay::getRelaySeq()` | `main/relay/ota_relay.cpp/h` | 126/141 | 无调用;`getSnapshot()` 已包含 seq。 |
|
||||||
|
| 14 | `static uint32_t OtaRelay::getRelayTotalPackets()` | `main/relay/ota_relay.cpp/h` | 132/146 | 无调用;`getSnapshot()` 已包含 total_packets。 |
|
||||||
|
|
||||||
|
### C++ 清理动作
|
||||||
|
- 删除上述函数的实现与对应头文件声明。
|
||||||
|
- 删除 `ota_prepare_handler` 后,检查并清理其专用的 `clear_esp32_prepare` / `clear_gd32_prepare` 静态状态变量是否仍被其他 handler 使用;若仅由 `ota_prepare_handler` 使用则一并移除。
|
||||||
|
- 删除 `getLatestIdSnapshot` 后,若 `id_snapshot_cache_t` 类型不再被使用,保留在头文件中(作为公共类型)或同步删除。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、C++ 旧版本兼容标记(建议保留并强化注释)
|
||||||
|
|
||||||
|
| 函数/成员 | 文件 | 说明 |
|
||||||
|
|-----------|------|------|
|
||||||
|
| `ble_ota_start_advertising(void)` | `main/server/ble_ota.cpp` | 注释已写“kept as free function for BLE callback compatibility”,确实被注册使用,保留并维持注释。 |
|
||||||
|
| `volatile bool m_wifiConnected` | `main/monitor/health_monitor.h` | 注释已写“保留为历史兼容”,保留。 |
|
||||||
|
| CANFD/SPI/Flash 迁移注释 | `protocol.cpp`、`spi_master.h/.cpp`、`ota_relay.h` | 已是历史迁移说明,无需改动。 |
|
||||||
|
| NVS 兼容 fallback | `gd32_flash_store.cpp` | 注释“视为空元数据,保持兼容”,保留。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、JS 死函数(建议删除)
|
||||||
|
|
||||||
|
### `tools/app.js`
|
||||||
|
| 函数 | 行 | 说明 |
|
||||||
|
|------|----|------|
|
||||||
|
| `sendCanFrame()` | 925 | 引用已移除的 `#canChannel/#canId/#canDlc/#canData`。 |
|
||||||
|
| `clearCanFields()` | 968 | 同上。 |
|
||||||
|
| `updateCanHint()` | 376 | 同上;事件绑定也指向已移除 DOM。 |
|
||||||
|
| `setHelper(id, text, type)` | 370 | 仅被 `updateCanHint()` 调用。 |
|
||||||
|
| `retryEsp32Upload()` | 1629 | 无按钮/调用者。 |
|
||||||
|
| `cancelEsp32Ota()` | 1643 | 无按钮/调用者。 |
|
||||||
|
| `applyEsp32Firmware()` | 1655 | 当前流程由 `verifyEsp32Firmware()` 直接 `/ota/update` 应用。 |
|
||||||
|
| `triggerRelay()` | 1759 | 仅转发 `triggerGd32Relay()`,注释写“旧页面兜底”。 |
|
||||||
|
|
||||||
|
### `tools/app.js` 兼容/空壳处理
|
||||||
|
| 函数 | 行 | 建议 |
|
||||||
|
|------|----|------|
|
||||||
|
| `showVerifyActions(type)` | 179 | 空壳,被 `verifyEsp32Firmware()` 等调用。删除函数体及所有调用点(因为 UI 按钮已移除)。 |
|
||||||
|
| `setOtaProgressBadge()` | 782 | 空壳局部函数,被多处调用。删除函数及所有调用点。 |
|
||||||
|
|
||||||
|
### `tools/ble_ota.js`
|
||||||
|
| 函数 | 行 | 说明 |
|
||||||
|
|------|----|------|
|
||||||
|
| `restartEsp32()` | 837 | IIFE 内部定义,无绑定/调用;全局 `restartEsp32()` 来自 `app.js` 已绑定按钮。 |
|
||||||
|
|
||||||
|
### `tools/can.js`
|
||||||
|
| 函数 | 行 | 说明 |
|
||||||
|
|------|----|------|
|
||||||
|
| `metaCard(label, value)` | 747 | 从未调用。 |
|
||||||
|
| `renderSignalGrid(/* message */)` | 751 | 空壳,无调用者。 |
|
||||||
|
| `renderSelectedMessage()` | 742 | 空壳但**仍被 `selectMessage` 等多个调用点调用**,是兼容 shim。保留并强化注释“旧版信号编辑面板兼容 shim”。 |
|
||||||
|
|
||||||
|
### `tools/index.html`
|
||||||
|
| 函数 | 行 | 说明 |
|
||||||
|
|------|----|------|
|
||||||
|
| `window.attachSharedFirmwareDropBridge` | 639 | 定义但从未调用;`app.js` 的 `setupDropZone()` 已处理 drop。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Python 脚本
|
||||||
|
|
||||||
|
- `ota_upload_test.py`、`serial_monitor.py` 均为纯顺序脚本,无函数定义,无需改动。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、验证步骤
|
||||||
|
|
||||||
|
1. 修改后执行 `idf.py build`(或等价的 `idf.py fullclean` + `build`)验证 C++ 无编译/链接错误。
|
||||||
|
2. 浏览器打开 `tools/index.html`、`tools/can.html`,验证控制台无 `xxx is not defined` 报错。
|
||||||
|
3. 检查 C++ 中是否产生新的 `-Wunused-function` / `-Wunused-variable` 警告。
|
||||||
|
4. 对存疑项做二次 grep 确认无调用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、风险与保留项
|
||||||
|
|
||||||
|
- `canFdSendNoReply` / `canFdPullIdSnapshot` 是公共 API,当前无调用。若你计划后续通过 HTTP/SPI 暴露它们,可以改为加 `[[maybe_unused]]` 或保留注释;否则按死函数删除。
|
||||||
|
- `ota_prepare_handler` 实现完整,若你打算未来启用 `/ota/prepare` 端点,应改为注册而非删除。本次计划按“当前死代码”删除处理。
|
||||||
|
- `renderSelectedMessage()` 仍被调用,保留作为明确的旧版兼容 shim。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、交付物
|
||||||
|
|
||||||
|
1. 修改后的 C++ 源文件与头文件(死函数已删除,兼容项已标记/保留)。
|
||||||
|
2. 修改后的 JS 文件(死函数已删除,空壳 shim 已清理)。
|
||||||
|
3. 一份简短变更摘要,列出删除的函数及保留的兼容 shim。
|
||||||
|
4. `idf.py build` 成功输出。
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
## 实施计划:DCU 初始化时通过 PC9 复位 ESP32
|
||||||
|
|
||||||
|
### 背景确认
|
||||||
|
- 目标仓库:`G:/Robot_v2/DCU/DCU_APP/GD32E508_DCU_APP`(GD32 侧 DCU 固件)。
|
||||||
|
- PC9 当前在 DCU_APP 中未被使用,不会与 USART0/CAN/SPI/EXMC 等冲突。
|
||||||
|
- ESP32 EN/RESET 为低电平复位、高电平释放;你给出的 50ms 低电平保持后释放逻辑正确。
|
||||||
|
- 项目风格:配置集中在 `sys/config.h`,外设封装为 `device/*.h` 中的 C++ class,延迟统一用 `SysTickTimer::delayMs()`,GPIO 清 0 用 `gpio_bit_reset()`。
|
||||||
|
|
||||||
|
### 修改点
|
||||||
|
|
||||||
|
#### 1. `sys/config.h`:增加 ESP32 复位引脚配置
|
||||||
|
在 SPI Ready 配置附近新增:
|
||||||
|
```cpp
|
||||||
|
/** @brief ESP32 硬件复位控制端口(PC9 接 ESP32 EN/RESET)。 */
|
||||||
|
constexpr uint32_t CONFIG_ESP32_RESET_PORT = GPIOC;
|
||||||
|
/** @brief ESP32 硬件复位控制引脚。 */
|
||||||
|
constexpr uint32_t CONFIG_ESP32_RESET_PIN = GPIO_PIN_9;
|
||||||
|
/** @brief ESP32 复位低电平保持时间,单位 ms。 */
|
||||||
|
constexpr uint32_t CONFIG_ESP32_RESET_HOLD_MS = 50U;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 新增 `device/esp32_reset.h`
|
||||||
|
按现有 `device/watchdog.h` 风格编写:
|
||||||
|
```cpp
|
||||||
|
#ifndef DEVICE_ESP32_RESET_H
|
||||||
|
#define DEVICE_ESP32_RESET_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
class Esp32Reset {
|
||||||
|
public:
|
||||||
|
/** @brief 初始化 PC9 为推挽输出,先拉低 50ms 再释放 ESP32。 */
|
||||||
|
static void init();
|
||||||
|
|
||||||
|
/** @brief 主动拉低 ESP32 RESET(进入复位)。 */
|
||||||
|
static void assertReset();
|
||||||
|
|
||||||
|
/** @brief 释放 ESP32 RESET(高电平,ESP32 启动)。 */
|
||||||
|
static void releaseReset();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* DEVICE_ESP32_RESET_H */
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 新增 `device/esp32_reset.cpp`
|
||||||
|
按现有 `device/watchdog.cpp` 风格编写:
|
||||||
|
```cpp
|
||||||
|
#include "device/esp32_reset.h"
|
||||||
|
#include "sys/config.h"
|
||||||
|
#include "sys/systick.h"
|
||||||
|
#include "main.h"
|
||||||
|
|
||||||
|
void Esp32Reset::init()
|
||||||
|
{
|
||||||
|
/* 1. 使能 GPIOC 时钟 */
|
||||||
|
rcu_periph_clock_enable(RCU_GPIOC);
|
||||||
|
|
||||||
|
/* 2. 先把输出数据寄存器清零,确保配置成输出时就是低电平 */
|
||||||
|
gpio_bit_reset(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
|
||||||
|
|
||||||
|
/* 3. 配置 PC9 为推挽输出 */
|
||||||
|
gpio_init(CONFIG_ESP32_RESET_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CONFIG_ESP32_RESET_PIN);
|
||||||
|
|
||||||
|
/* 4. 保持复位 50ms,等电源和其他 IO 稳定 */
|
||||||
|
SysTickTimer::delayMs(CONFIG_ESP32_RESET_HOLD_MS);
|
||||||
|
|
||||||
|
/* 5. 释放 ESP32 */
|
||||||
|
gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Esp32Reset::assertReset()
|
||||||
|
{
|
||||||
|
gpio_bit_reset(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Esp32Reset::releaseReset()
|
||||||
|
{
|
||||||
|
gpio_bit_set(CONFIG_ESP32_RESET_PORT, CONFIG_ESP32_RESET_PIN);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. `sys/initial.cpp`:在初始化序列中加入 ESP32 复位
|
||||||
|
- 新增 `#include "device/esp32_reset.h"`。
|
||||||
|
- 在 `SysTickTimer::initMs()` 之后、`Usart0::configurePins()` 之前调用 `Esp32Reset::init()`,原因:
|
||||||
|
- 需要 SysTick 才能做 50ms 延迟;
|
||||||
|
- 尽早释放 ESP32,让它在 DCU 继续初始化 USART/CAN/SPI 期间完成自身启动,等 SPI 命令总线初始化时 ESP32 已经可用。
|
||||||
|
|
||||||
|
修改后顺序:
|
||||||
|
```cpp
|
||||||
|
nvic_priority_group_set(...);
|
||||||
|
SysTickTimer::initMs();
|
||||||
|
Esp32Reset::init(); // 新增
|
||||||
|
Usart0::configurePins();
|
||||||
|
Usart0::init();
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. `GD32E508_DCU_APP.cproject.yml`:把新文件加入构建
|
||||||
|
在 `groups → device` 中增加:
|
||||||
|
```yaml
|
||||||
|
- file: ./device/esp32_reset.h
|
||||||
|
- file: ./device/esp32_reset.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码适配说明
|
||||||
|
- 你原代码中的 `GPIO_BC(GPIOA) = GPIO_PIN_9` 等价于 `gpio_bit_reset(...)`,项目里统一用后一个 API。
|
||||||
|
- `delay_ms(50)` 替换为 `SysTickTimer::delayMs(CONFIG_ESP32_RESET_HOLD_MS)`,与项目中其他复位/ADC 稳定延迟一致。
|
||||||
|
- 端口从 `GPIOA/PA9` 改为 `GPIOC/PC9` 的 `CONFIG_*` 常量,便于后续硬件变更。
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
- 重新生成并编译 CMSIS csolution 工程(`cbuild DCU_APP.csolution.yml` 或 Keil Studio),确认新文件被编译、无重复定义、无未定义符号。
|
||||||
|
- 若编译失败,根据错误输出修正头文件包含或 cproject.yml 路径。
|
||||||
|
|
||||||
|
### 风险/注意事项
|
||||||
|
- `Esp32Reset::init()` 调用时 SysTick 已就绪,因此 50ms 阻塞等待是安全的(此时中断已使能但尚未初始化其他外设)。
|
||||||
|
- 若后续需要在运行期再次复位 ESP32,可直接调用 `Esp32Reset::assertReset()` + `SysTickTimer::delayMs(...)` + `Esp32Reset::releaseReset()`。
|
||||||
|
- 本次修改不改变 PA9 的 USART0_TX 功能,调试串口保持正常。
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
// 固件版本
|
// 固件版本
|
||||||
#define FW_VERSION_MAJOR 0
|
#define FW_VERSION_MAJOR 0
|
||||||
#define FW_VERSION_MINOR 3
|
#define FW_VERSION_MINOR 3
|
||||||
#define FW_VERSION_PATCH 39
|
#define FW_VERSION_PATCH 40
|
||||||
#define FW_VERSION_STRING "0.3.39"
|
#define FW_VERSION_STRING "0.3.40"
|
||||||
|
|
||||||
// 编译时间
|
// 编译时间
|
||||||
#define BUILD_TIME "2026-07-09 02:43:44"
|
#define BUILD_TIME "2026-07-11 17:47:55"
|
||||||
|
|
||||||
// Git 信息
|
// Git 信息
|
||||||
#define GIT_HASH "661ae54"
|
#define GIT_HASH "fedb08e"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ static volatile bool s_paused = false;
|
|||||||
/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */
|
/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */
|
||||||
static can_feedback_result_t s_work [CAN_CHANNEL_MAX];
|
static can_feedback_result_t s_work [CAN_CHANNEL_MAX];
|
||||||
static uint8_t s_workIdx[CAN_CHANNEL_MAX];
|
static uint8_t s_workIdx[CAN_CHANNEL_MAX];
|
||||||
|
static uint32_t s_lastLoggedError[CAN_CHANNEL_MAX];
|
||||||
|
|
||||||
static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
||||||
{
|
{
|
||||||
@@ -86,19 +87,22 @@ static void taskLoop(void *)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 解析本页并累积到工作缓冲,page=0 会自动重置 */
|
/* 解析本页并累积到工作缓冲,page=0 会自动重置 */
|
||||||
const uint8_t page = ring_payload[1];
|
|
||||||
if (canFdParseFeedbackPage(ring_payload, ring_len,
|
if (canFdParseFeedbackPage(ring_payload, ring_len,
|
||||||
&s_work[ch], &s_workIdx[ch]) == 0) {
|
&s_work[ch], &s_workIdx[ch]) == 0) {
|
||||||
publish(ch, s_work[ch]);
|
publish(ch, s_work[ch]);
|
||||||
|
|
||||||
/* 仅在出现错误时打印告警,正常工作静默 */
|
/* 累计错误发生变化时才告警一次;P0/P1 携带相同统计,不能重复打印。 */
|
||||||
if (s_work[ch].stats.error_count != 0U) {
|
if (s_work[ch].stats.error_count != 0U
|
||||||
|
&& s_work[ch].stats.error_count != s_lastLoggedError[ch]) {
|
||||||
DEBUG_WARN(kTag,
|
DEBUG_WARN(kTag,
|
||||||
"CH%u P%u seq=%lu err=%lu",
|
"CH%u seq=%lu err=%lu TEC=%u REC=%u state=0x%02X",
|
||||||
static_cast<unsigned>(ch),
|
static_cast<unsigned>(ch),
|
||||||
static_cast<unsigned>(page),
|
|
||||||
static_cast<unsigned long>(s_work[ch].stats.snapshot_seq),
|
static_cast<unsigned long>(s_work[ch].stats.snapshot_seq),
|
||||||
static_cast<unsigned long>(s_work[ch].stats.error_count));
|
static_cast<unsigned long>(s_work[ch].stats.error_count),
|
||||||
|
static_cast<unsigned>(s_work[ch].stats.tec),
|
||||||
|
static_cast<unsigned>(s_work[ch].stats.rec),
|
||||||
|
static_cast<unsigned>(s_work[ch].stats.bus_state));
|
||||||
|
s_lastLoggedError[ch] = s_work[ch].stats.error_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,6 +127,7 @@ bool startCanFeedbackTask()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
memset(s_latest, 0, sizeof(s_latest));
|
memset(s_latest, 0, sizeof(s_latest));
|
||||||
|
memset(s_lastLoggedError, 0, sizeof(s_lastLoggedError));
|
||||||
memset(s_valid, 0, sizeof(s_valid));
|
memset(s_valid, 0, sizeof(s_valid));
|
||||||
memset(s_work, 0, sizeof(s_work));
|
memset(s_work, 0, sizeof(s_work));
|
||||||
memset(s_workIdx, 0, sizeof(s_workIdx));
|
memset(s_workIdx, 0, sizeof(s_workIdx));
|
||||||
@@ -153,21 +158,6 @@ void pauseCanFeedbackTask(bool pause)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getLatestIdSnapshot(uint8_t channel, id_snapshot_cache_t *out)
|
|
||||||
{
|
|
||||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || s_mutex == nullptr) return false;
|
|
||||||
bool ok = false;
|
|
||||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
||||||
if (s_latest[channel].valid) {
|
|
||||||
out->count = s_latest[channel].valid;
|
|
||||||
memcpy(out->entries, s_latest[channel].entries, sizeof(out->entries));
|
|
||||||
ok = true;
|
|
||||||
}
|
|
||||||
xSemaphoreGive(s_mutex);
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 拷贝某通道的最新快照(线程安全)。
|
* @brief 拷贝某通道的最新快照(线程安全)。
|
||||||
* @return true 表示拿到一份有效数据。
|
* @return true 表示拿到一份有效数据。
|
||||||
|
|||||||
@@ -7,13 +7,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "transport/canfd_bridge.h"
|
#include "transport/canfd_bridge.h"
|
||||||
|
|
||||||
/** @brief ID 快照缓存结构。 */
|
|
||||||
typedef struct {
|
|
||||||
id_snapshot_entry_t entries[CANFD_ID_SNAPSHOT_MAX];
|
|
||||||
uint8_t count;
|
|
||||||
bool valid;
|
|
||||||
} id_snapshot_cache_t;
|
|
||||||
|
|
||||||
/** @brief 暂停/恢复 CAN 反馈轮询(OTA 期间调用 pause=true)。 */
|
/** @brief 暂停/恢复 CAN 反馈轮询(OTA 期间调用 pause=true)。 */
|
||||||
void pauseCanFeedbackTask(bool pause);
|
void pauseCanFeedbackTask(bool pause);
|
||||||
|
|
||||||
@@ -35,14 +28,6 @@ bool startCanFeedbackTask();
|
|||||||
*/
|
*/
|
||||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out);
|
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取指定通道的最新 ID 快照(线程安全)。
|
|
||||||
* @param channel 0/1/2。
|
|
||||||
* @param out 输出缓冲。
|
|
||||||
* @return true 有有效数据。
|
|
||||||
*/
|
|
||||||
bool getLatestIdSnapshot(uint8_t channel, id_snapshot_cache_t *out);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -555,18 +555,6 @@ void OtaCoordinator::restartEsp32()
|
|||||||
// 不会返回
|
// 不会返回
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 将 ESP32 OTA finalize 错误码分类。
|
|
||||||
* @param err finalize 返回的 esp_err_t。
|
|
||||||
* @return 0=成功,1=SHA不匹配,2=其他失败。
|
|
||||||
*/
|
|
||||||
int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)
|
|
||||||
{
|
|
||||||
if (err == ESP_OK) return 0; // 成功
|
|
||||||
if (err == ESP_ERR_INVALID_VERSION) return 1; // SHA 不匹配
|
|
||||||
return 2; // 其他失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 中止
|
// 中止
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -655,39 +643,12 @@ bool OtaCoordinator::isIdleState(ota_system_state_t s) const
|
|||||||
s == OTA_SYS_GD32_COMPLETE);
|
s == OTA_SYS_GD32_COMPLETE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief 判断给定状态值是否属于 GD32 忙态(不加锁,纯判断)。 */
|
|
||||||
bool OtaCoordinator::isGd32BusyState(ota_system_state_t s) const
|
|
||||||
{
|
|
||||||
return (s == OTA_SYS_GD32_STORING ||
|
|
||||||
s == OTA_SYS_GD32_RELAYING);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief 判断给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。 */
|
|
||||||
bool OtaCoordinator::isEsp32BusyState(ota_system_state_t s) const
|
|
||||||
{
|
|
||||||
return (s == OTA_SYS_ESP32_RECEIVING ||
|
|
||||||
s == OTA_SYS_ESP32_FINALIZING ||
|
|
||||||
s == OTA_SYS_ESP32_REBOOTING);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||||||
bool OtaCoordinator::isIdle() const
|
bool OtaCoordinator::isIdle() const
|
||||||
{
|
{
|
||||||
return isIdleState(getSystemState());
|
return isIdleState(getSystemState());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
|
|
||||||
bool OtaCoordinator::isGd32Busy() const
|
|
||||||
{
|
|
||||||
return isGd32BusyState(getSystemState());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
|
|
||||||
bool OtaCoordinator::isEsp32Busy() const
|
|
||||||
{
|
|
||||||
return isEsp32BusyState(getSystemState());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
|
/** @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
|
||||||
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。 */
|
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。 */
|
||||||
const char* OtaCoordinator::stateToString(ota_system_state_t s)
|
const char* OtaCoordinator::stateToString(ota_system_state_t s)
|
||||||
@@ -706,29 +667,12 @@ const char* OtaCoordinator::stateToString(ota_system_state_t s)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。
|
|
||||||
* 委托 stateToString(getSystemState()),保持向后兼容。 */
|
|
||||||
const char* OtaCoordinator::getSystemStateString() const
|
|
||||||
{
|
|
||||||
return stateToString(getSystemState());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
|
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
|
||||||
bool OtaCoordinator::getGd32AppAlive() const
|
bool OtaCoordinator::getGd32AppAlive() const
|
||||||
{
|
{
|
||||||
return OtaRelay::getGd32AppAlive();
|
return OtaRelay::getGd32AppAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief 获取 GD32 固件是否已存储完成(线程安全)。 */
|
|
||||||
bool OtaCoordinator::isGd32StorageDone() const
|
|
||||||
{
|
|
||||||
// ★ M1/L3: m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast
|
|
||||||
bool h = lock();
|
|
||||||
bool done = m_gd32StorageDone;
|
|
||||||
unlock(h);
|
|
||||||
return done;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// OTA 状态快照(PR1: 收口状态读取)
|
// OTA 状态快照(PR1: 收口状态读取)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -738,7 +682,7 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
|||||||
|
|
||||||
// ── coordinator 自身状态(一次 mutex,避免二次加锁导致快照不一致)──
|
// ── coordinator 自身状态(一次 mutex,避免二次加锁导致快照不一致)──
|
||||||
// 持锁时一次性取 m_sysState,并调不加锁的 stateToString()
|
// 持锁时一次性取 m_sysState,并调不加锁的 stateToString()
|
||||||
// 就地转字符串。旧实现释放锁后再调 getSystemStateString() 二次加锁,
|
// 就地转字符串。旧实现释放锁后再调 stateToString(getSystemState()) 二次加锁,
|
||||||
// 两次取值之间状态可能变化,导致 snap.sys_state 与 snap.sys_state_str
|
// 两次取值之间状态可能变化,导致 snap.sys_state 与 snap.sys_state_str
|
||||||
// 描述不同时刻状态。
|
// 描述不同时刻状态。
|
||||||
bool h = lock();
|
bool h = lock();
|
||||||
|
|||||||
@@ -395,20 +395,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool isIdleState(ota_system_state_t s) const;
|
bool isIdleState(ota_system_state_t s) const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查给定状态值是否属于 GD32 忙态(不加锁,纯判断)。
|
|
||||||
* @param s 状态枚举值。
|
|
||||||
* @return true 表示 STORING 或 RELAYING。
|
|
||||||
*/
|
|
||||||
bool isGd32BusyState(ota_system_state_t s) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。
|
|
||||||
* @param s 状态枚举值。
|
|
||||||
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
|
|
||||||
*/
|
|
||||||
bool isEsp32BusyState(ota_system_state_t s) const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
|
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
|
||||||
* 内部仍加锁取一次状态后调 isIdleState()。
|
* 内部仍加锁取一次状态后调 isIdleState()。
|
||||||
@@ -416,27 +402,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool isIdle() const;
|
bool isIdle() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查是否有 GD32 相关操作正在进行。
|
|
||||||
* 内部仍加锁取一次状态后调 isGd32BusyState()。
|
|
||||||
* @return true 表示 STORING 或 RELAYING。
|
|
||||||
*/
|
|
||||||
bool isGd32Busy() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 检查是否有 ESP32 OTA 正在进行。
|
|
||||||
* 内部仍加锁取一次状态后调 isEsp32BusyState()。
|
|
||||||
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
|
|
||||||
*/
|
|
||||||
bool isEsp32Busy() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 获取系统状态的可读字符串描述。
|
|
||||||
* 内部委托 stateToString(getSystemState()),保持向后兼容。
|
|
||||||
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
|
|
||||||
*/
|
|
||||||
const char* getSystemStateString() const;
|
|
||||||
|
|
||||||
// ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)──
|
// ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)──
|
||||||
/**
|
/**
|
||||||
* @brief 获取 GD32 APP 存活状态。
|
* @brief 获取 GD32 APP 存活状态。
|
||||||
@@ -444,27 +409,12 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool getGd32AppAlive() const;
|
bool getGd32AppAlive() const;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief GD32 固件存储完成标志(供状态查询)。
|
|
||||||
* @return true 表示固件已完整存储。
|
|
||||||
*/
|
|
||||||
bool isGd32StorageDone() const;
|
|
||||||
|
|
||||||
// ── 完全复位 ──
|
// ── 完全复位 ──
|
||||||
/**
|
/**
|
||||||
* @brief 完全复位协调器和所有子系统状态。
|
* @brief 完全复位协调器和所有子系统状态。
|
||||||
*/
|
*/
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// ── ESP32 OTA finalize 错误分类(BLE/WiFi 共用)──
|
|
||||||
/**
|
|
||||||
* @brief 把 finalize 返回的 esp_err_t 映射为前端可读的事件码。
|
|
||||||
* 统一 BLE/WiFi 两端语义,便于日志/状态一致。
|
|
||||||
* @param err finalize 返回的 esp_err_t。
|
|
||||||
* @return 0=成功;1=SHA 不匹配;2=其他失败。
|
|
||||||
*/
|
|
||||||
static int esp32FinalizeErrCategory(esp_err_t err);
|
|
||||||
|
|
||||||
// ── IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)──
|
// ── IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)──
|
||||||
/**
|
/**
|
||||||
* @brief IRelayListener 回调:中继已开始。
|
* @brief IRelayListener 回调:中继已开始。
|
||||||
|
|||||||
@@ -123,31 +123,12 @@ relay_state_t OtaRelay::getState()
|
|||||||
return relayState;
|
return relayState;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t OtaRelay::getRelaySeq()
|
|
||||||
{
|
|
||||||
StateLockGuard lock;
|
|
||||||
return relaySeq;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t OtaRelay::getRelayTotalPackets()
|
|
||||||
{
|
|
||||||
StateLockGuard lock;
|
|
||||||
return relayTotalPackets;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OtaRelay::getGd32AppAlive()
|
bool OtaRelay::getGd32AppAlive()
|
||||||
{
|
{
|
||||||
StateLockGuard lock;
|
StateLockGuard lock;
|
||||||
return gd32AppAlive;
|
return gd32AppAlive;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OtaRelay::getGd32Version(gd32_version_t *out)
|
|
||||||
{
|
|
||||||
if (!out) return;
|
|
||||||
StateLockGuard lock;
|
|
||||||
*out = gd32Version;
|
|
||||||
}
|
|
||||||
|
|
||||||
RelaySnapshot OtaRelay::getSnapshot()
|
RelaySnapshot OtaRelay::getSnapshot()
|
||||||
{
|
{
|
||||||
StateLockGuard lock;
|
StateLockGuard lock;
|
||||||
|
|||||||
@@ -134,26 +134,11 @@ public:
|
|||||||
* @return 当前 relay_state_t 状态值。
|
* @return 当前 relay_state_t 状态值。
|
||||||
*/
|
*/
|
||||||
static relay_state_t getState();
|
static relay_state_t getState();
|
||||||
/**
|
|
||||||
* @brief 获取中继当前序号.
|
|
||||||
* @return 当前中继序号。
|
|
||||||
*/
|
|
||||||
static uint32_t getRelaySeq();
|
|
||||||
/**
|
|
||||||
* @brief 获取中继总包数.
|
|
||||||
* @return 本次中继的总包数。
|
|
||||||
*/
|
|
||||||
static uint32_t getRelayTotalPackets();
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取GD32 APP存活标志位.
|
* @brief 获取GD32 APP存活标志位.
|
||||||
* @return true 表示 GD32 APP 存活。
|
* @return true 表示 GD32 APP 存活。
|
||||||
*/
|
*/
|
||||||
static bool getGd32AppAlive();
|
static bool getGd32AppAlive();
|
||||||
/**
|
|
||||||
* @brief 获取GD32固件版本.
|
|
||||||
* @param out 输出版本信息结构体指针。
|
|
||||||
*/
|
|
||||||
static void getGd32Version(gd32_version_t *out);
|
|
||||||
/**
|
/**
|
||||||
* @brief 一次性读取所有中继状态字段(单次锁保护)。
|
* @brief 一次性读取所有中继状态字段(单次锁保护)。
|
||||||
* 消除逐 getter 调用之间 relay task 更新状态导致的不一致。
|
* 消除逐 getter 调用之间 relay task 更新状态导致的不一致。
|
||||||
@@ -238,7 +223,6 @@ public:
|
|||||||
~StateLockGuard() {
|
~StateLockGuard() {
|
||||||
if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); }
|
if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); }
|
||||||
}
|
}
|
||||||
bool isLocked() const { return m_locked; }
|
|
||||||
StateLockGuard(const StateLockGuard&) = delete;
|
StateLockGuard(const StateLockGuard&) = delete;
|
||||||
StateLockGuard& operator=(const StateLockGuard&) = delete;
|
StateLockGuard& operator=(const StateLockGuard&) = delete;
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -199,109 +199,6 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t ota_prepare_handler(httpd_req_t *req)
|
|
||||||
{
|
|
||||||
http_set_cors_headers(req);
|
|
||||||
DEBUG_INFO("WIFI", "=== HTTP: POST /ota/prepare ===");
|
|
||||||
|
|
||||||
char buf[256] = {0};
|
|
||||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
|
||||||
if (ret <= 0) {
|
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "recv failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
buf[ret] = '\0';
|
|
||||||
|
|
||||||
cJSON *root = cJSON_Parse(buf);
|
|
||||||
if (!root) {
|
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON parse failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
cJSON *type_item = cJSON_GetObjectItem(root, "type");
|
|
||||||
cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
|
|
||||||
if (!type_item || !cJSON_IsString(type_item) ||
|
|
||||||
!size_item || !cJSON_IsNumber(size_item)) {
|
|
||||||
cJSON_Delete(root);
|
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing type or total_size");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *type = cJSON_GetStringValue(type_item);
|
|
||||||
uint32_t total_size = (uint32_t)cJSON_GetNumberValue(size_item);
|
|
||||||
cJSON_Delete(root);
|
|
||||||
|
|
||||||
if (type && strcmp(type, "esp32") == 0) {
|
|
||||||
clear_esp32_prepare();
|
|
||||||
|
|
||||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
if (err == ESP_ERR_INVALID_STATE) {
|
|
||||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
|
|
||||||
"ESP32 OTA occupied by another channel");
|
|
||||||
} else {
|
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
|
||||||
}
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_esp32_prepared = true;
|
|
||||||
s_esp32_prepare_size = total_size;
|
|
||||||
s_esp32_prepare_epoch = OtaCoordinator::instance().getOtaEpoch();
|
|
||||||
const char resp[] = "{\"status\": \"ok\", \"target\": \"esp32\"}";
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
|
||||||
httpd_resp_send(req, resp, strlen(resp));
|
|
||||||
DEBUG_INFO("WIFI", "ESP32 OTA 准备完成: %u 字节", total_size);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type && strcmp(type, "gd32") == 0) {
|
|
||||||
clear_gd32_prepare();
|
|
||||||
|
|
||||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
|
||||||
DEBUG_WARN("WIFI", "GD32 prepare 前复位中继状态: %d", OtaRelay::getState());
|
|
||||||
OtaRelay::instance().reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 统一调用 prepareGd32Storage 完成:validate + begin + erase + clearMeta + saveMeta
|
|
||||||
// 与 BLE 路径 / ota_upload_handler 共享同一序列,消除手写重复
|
|
||||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(total_size, OTA_CHANNEL_WIFI);
|
|
||||||
if (prep_err == ESP_ERR_INVALID_SIZE) {
|
|
||||||
DEBUG_ERR("WIFI", "prepare: firmware size invalid: %u", total_size);
|
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "firmware size invalid");
|
|
||||||
return ESP_FAIL;
|
|
||||||
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
|
||||||
DEBUG_ERR("WIFI", "GD32 prepare 协调器拒绝(系统忙)");
|
|
||||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
|
|
||||||
"system busy with another OTA operation");
|
|
||||||
return ESP_FAIL;
|
|
||||||
} else if (prep_err != ESP_OK) {
|
|
||||||
DEBUG_ERR("WIFI", "GD32 prepare 失败: %s", esp_err_to_name(prep_err));
|
|
||||||
char err_msg[96];
|
|
||||||
snprintf(err_msg, sizeof(err_msg), "prepare gd32_ota failed: %s",
|
|
||||||
esp_err_to_name(prep_err));
|
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
WifiOta::instance().stateLock();
|
|
||||||
WifiOta::instance().setGd32StorageDone(false);
|
|
||||||
WifiOta::instance().stateUnlock();
|
|
||||||
|
|
||||||
s_gd32_prepared = true;
|
|
||||||
s_gd32_prepare_size = total_size;
|
|
||||||
s_gd32_prepare_epoch = OtaCoordinator::instance().getOtaEpoch();
|
|
||||||
const char resp[] = "{\"status\": \"ok\", \"target\": \"gd32\"}";
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
|
||||||
httpd_resp_send(req, resp, strlen(resp));
|
|
||||||
DEBUG_INFO("WIFI", "GD32 OTA 准备完成: %u 字节", total_size);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "unknown type");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t ota_upload_handler(httpd_req_t *req)
|
esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||||
{
|
{
|
||||||
http_set_cors_headers(req);
|
http_set_cors_headers(req);
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ public:
|
|||||||
bool getStaRetryExhausted() const { return m_staRetryExhausted.load(); }
|
bool getStaRetryExhausted() const { return m_staRetryExhausted.load(); }
|
||||||
/** @brief 设置 STA 重试耗尽标志。 */
|
/** @brief 设置 STA 重试耗尽标志。 */
|
||||||
void setStaRetryExhausted(bool v) { m_staRetryExhausted.store(v); }
|
void setStaRetryExhausted(bool v) { m_staRetryExhausted.store(v); }
|
||||||
/** @brief 获取 GD32 固件存储完成标志。 */
|
|
||||||
bool getGd32StorageDone() const { return m_gd32StorageDone.load(); }
|
|
||||||
/** @brief 设置 GD32 固件存储完成标志。 */
|
/** @brief 设置 GD32 固件存储完成标志。 */
|
||||||
void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); }
|
void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); }
|
||||||
// ── 状态互斥锁(保护复合读-改-写操作)──
|
// ── 状态互斥锁(保护复合读-改-写操作)──
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
|||||||
|
|
||||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||||
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
constexpr uint8_t kSpiCanSetModeType = 0xC2U;
|
||||||
constexpr uint8_t kSpiIdSnapshotType = 0xC3U;
|
|
||||||
constexpr size_t kFeedbackPayloadLen = 123U; // 与 GD32 协议表对齐 (kTxBufSize=128 - overhead=5)
|
constexpr size_t kFeedbackPayloadLen = 123U; // 与 GD32 协议表对齐 (kTxBufSize=128 - overhead=5)
|
||||||
|
|
||||||
static inline uint32_t readU32Le(const uint8_t *p) {
|
static inline uint32_t readU32Le(const uint8_t *p) {
|
||||||
@@ -86,21 +85,6 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
|||||||
return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
|
return sendCanFrameOverSpi(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 发送一帧无需上层消费响应的 CANFD 数据。
|
|
||||||
* GD32 APP 仍会返回 ACK/ERROR,这里主动读走响应,避免残留到下一笔 SPI 事务。
|
|
||||||
*/
|
|
||||||
int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
|
|
||||||
const uint8_t *data, uint8_t dlc)
|
|
||||||
{
|
|
||||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (sendCanFrameOverSpi(channel, ext_id, data, dlc, nullptr, 0U, 1000U) >= 0) ? 0 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
* @brief 解析 GD32 主动推送的一页 CAN 反馈 payload,累积到结果结构中。
|
||||||
*
|
*
|
||||||
@@ -176,76 +160,6 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 拉取指定通道的不重复 ID 快照表(GD32 SPI 帧类型 0xC3)。
|
|
||||||
* 分页拉取,每页 8 条,最多拉 2 页(16 条)。
|
|
||||||
*/
|
|
||||||
int canFdPullIdSnapshot(can_channel_t channel,
|
|
||||||
id_snapshot_entry_t *out,
|
|
||||||
uint8_t *out_count,
|
|
||||||
uint32_t timeout_ms)
|
|
||||||
{
|
|
||||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || out_count == nullptr) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*out_count = 0U;
|
|
||||||
uint8_t total = 0U;
|
|
||||||
|
|
||||||
for (uint8_t page = 0U; page < 2U; ++page) {
|
|
||||||
uint8_t req[2] = { static_cast<uint8_t>(channel), page };
|
|
||||||
uint8_t resp_buf[kFeedbackPayloadLen] = {};
|
|
||||||
uint8_t resp_type = 0U;
|
|
||||||
size_t resp_len = 0U;
|
|
||||||
|
|
||||||
esp_err_t ret = SpiMaster::instance().otaSendAndPoll(
|
|
||||||
kSpiIdSnapshotType,
|
|
||||||
req, 2U,
|
|
||||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, timeout_ms);
|
|
||||||
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 4U) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* resp: [ch][page][total_count][entry_count][entries...] */
|
|
||||||
if (page == 0U) {
|
|
||||||
total = resp_buf[2];
|
|
||||||
}
|
|
||||||
const uint8_t entryCount = resp_buf[3];
|
|
||||||
if (entryCount == 0U) break;
|
|
||||||
|
|
||||||
uint16_t offset = 4U;
|
|
||||||
for (uint8_t i = 0U; i < entryCount && *out_count < CANFD_ID_SNAPSHOT_MAX; ++i) {
|
|
||||||
if (offset + 16U > resp_len) break;
|
|
||||||
|
|
||||||
id_snapshot_entry_t &e = out[*out_count];
|
|
||||||
e.id = readU32Le(&resp_buf[offset]);
|
|
||||||
e.seq = readU32Le(&resp_buf[offset + 4U]);
|
|
||||||
e.rx_count = readU32Le(&resp_buf[offset + 8U]);
|
|
||||||
e.dlc = resp_buf[offset + 12U];
|
|
||||||
const uint8_t flags = resp_buf[offset + 13U];
|
|
||||||
e.fd_flag = (flags & 0x01U) ? 1U : 0U;
|
|
||||||
e.brs = (flags & 0x02U) ? 1U : 0U;
|
|
||||||
e.esi = (flags & 0x04U) ? 1U : 0U;
|
|
||||||
|
|
||||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
|
||||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
|
||||||
const uint16_t entrySize = 16U + dataLen;
|
|
||||||
if (offset + entrySize > resp_len) break;
|
|
||||||
|
|
||||||
memcpy(e.data, &resp_buf[offset + 16U], dataLen);
|
|
||||||
/* 清零剩余字节 */
|
|
||||||
for (uint8_t d = dataLen; d < CANFD_ID_SNAPSHOT_DATA_MAX; ++d) {
|
|
||||||
e.data[d] = 0U;
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += entrySize;
|
|
||||||
(*out_count)++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (*out_count >= total) break; /* 已拉完 */
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
int canFdSetRxMode(can_rx_mode_t mode,
|
int canFdSetRxMode(can_rx_mode_t mode,
|
||||||
can_rx_mode_t *actual,
|
can_rx_mode_t *actual,
|
||||||
uint32_t timeout_ms)
|
uint32_t timeout_ms)
|
||||||
|
|||||||
@@ -50,18 +50,6 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
|||||||
uint8_t *recv_buf, uint8_t recv_max,
|
uint8_t *recv_buf, uint8_t recv_max,
|
||||||
uint32_t timeout_ms);
|
uint32_t timeout_ms);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 发送一帧无需响应的 CANFD 数据。
|
|
||||||
* @param channel CAN 通道编号(0~2)。
|
|
||||||
* @param ext_id 扩展帧 ID(29 位)。
|
|
||||||
* @param data CAN 数据负载。
|
|
||||||
* @param dlc 数据长度。
|
|
||||||
* @return 成功返回 0,失败返回 -1。
|
|
||||||
*/
|
|
||||||
int canFdSendNoReply(can_channel_t channel, uint32_t ext_id,
|
|
||||||
const uint8_t *data, uint8_t dlc);
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 不重复 ID 快照表(SPI 0xC3 协议配套)
|
// 不重复 ID 快照表(SPI 0xC3 协议配套)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -147,16 +135,4 @@ int canFdSetRxMode(can_rx_mode_t mode,
|
|||||||
can_rx_mode_t *actual,
|
can_rx_mode_t *actual,
|
||||||
uint32_t timeout_ms);
|
uint32_t timeout_ms);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 拉取指定通道的不重复 ID 快照表(SPI 帧 0xC3,分页)。
|
|
||||||
* @param channel 目标通道 0/1/2。
|
|
||||||
* @param out 输出条目数组(至少 CANFD_ID_SNAPSHOT_MAX 个元素)。
|
|
||||||
* @param out_count 输出实际条目数。
|
|
||||||
* @param timeout_ms SPI 超时。
|
|
||||||
* @return 0 成功,<0 失败。
|
|
||||||
*/
|
|
||||||
int canFdPullIdSnapshot(can_channel_t channel,
|
|
||||||
id_snapshot_entry_t *out,
|
|
||||||
uint8_t *out_count,
|
|
||||||
uint32_t timeout_ms);
|
|
||||||
#endif // CANFD_BRIDGE_H
|
#endif // CANFD_BRIDGE_H
|
||||||
|
|||||||
+23
-180
@@ -26,6 +26,7 @@ let pageVisible = !document.hidden; // 页面可见性:切到后台/最小
|
|||||||
let latestSystemStatus = null;
|
let latestSystemStatus = null;
|
||||||
let isEsp32Rebooting = false;
|
let isEsp32Rebooting = false;
|
||||||
let rebootStartTime = 0;
|
let rebootStartTime = 0;
|
||||||
|
let rebootObservedDown = false; // 是否已观察到设备实际下线(重启),用于避免 pre-reboot 窗口内误清除重启状态
|
||||||
let pollingPausedForUpload = false;
|
let pollingPausedForUpload = false;
|
||||||
let uploadPollChip = null; // 当前暂停轮询的上传目标,用于区分 WiFi/日志策略
|
let uploadPollChip = null; // 当前暂停轮询的上传目标,用于区分 WiFi/日志策略
|
||||||
let activeXhr = null; // 当前上传 XHR,用于 abort
|
let activeXhr = null; // 当前上传 XHR,用于 abort
|
||||||
@@ -176,11 +177,6 @@ function cleanupAfterOtaFailure(reason) {
|
|||||||
addLog('E', 'OTA', '刷写流程已中止: ' + (reason || '未知原因'));
|
addLog('E', 'OTA', '刷写流程已中止: ' + (reason || '未知原因'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVerifyActions(type) {
|
|
||||||
// 已移除确认/重试/取消按钮:SHA-256 校验通过后自动重启,校验失败仅记录日志
|
|
||||||
void type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
|
/** @description 设置当前活跃 OTA 阶段并更新 UI 步骤指示器。 */
|
||||||
function setActivePhase(phase) {
|
function setActivePhase(phase) {
|
||||||
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
|
if (phase === 'gd32Upload' || phase === 'gd32Relay') phase = 'gd32Ota';
|
||||||
@@ -367,38 +363,6 @@ function updateUploadButtonLabel() {
|
|||||||
if (esp32Btn) esp32Btn.disabled = !esp32File || isUploading;
|
if (esp32Btn) esp32Btn.disabled = !esp32File || isUploading;
|
||||||
if (gd32Btn) gd32Btn.disabled = !gd32File || isUploading;
|
if (gd32Btn) gd32Btn.disabled = !gd32File || isUploading;
|
||||||
}
|
}
|
||||||
function setHelper(id, text, type) {
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
if (!el) return;
|
|
||||||
el.className = 'helper-note ' + (type || '');
|
|
||||||
el.innerHTML = text;
|
|
||||||
}
|
|
||||||
function updateCanHint() {
|
|
||||||
var dlcEl = document.getElementById('canDlc');
|
|
||||||
var dataEl = document.getElementById('canData');
|
|
||||||
var hintEl = document.getElementById('canHint');
|
|
||||||
if (!dlcEl || !dataEl || !hintEl) return;
|
|
||||||
var dlc = parseInt(dlcEl.value) || 0;
|
|
||||||
var dataHex = dataEl.value.trim().replace(/\s/g, '');
|
|
||||||
if (!dataHex) {
|
|
||||||
setHelper('canHint', '输入十六进制数据后会自动检查字节数是否与 DLC 一致。', '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!/^[0-9a-fA-F]*$/.test(dataHex)) {
|
|
||||||
setHelper('canHint', '数据包含非 HEX 字符,请只输入 <code>0-9 A-F</code>。', 'err');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (dataHex.length % 2 !== 0) {
|
|
||||||
setHelper('canHint', 'HEX 长度必须为偶数,当前还差半个字节。', 'err');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var bytes = dataHex.length / 2;
|
|
||||||
if (dlc && bytes !== dlc) {
|
|
||||||
setHelper('canHint', '当前数据为 <b>' + bytes + '</b> 字节,但 DLC = <b>' + dlc + '</b>。发送前建议对齐。', 'warn');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setHelper('canHint', 'HEX 数据合法,当前长度 <b>' + bytes + '</b> 字节,与 DLC 一致。', 'ok');
|
|
||||||
}
|
|
||||||
function resetStageFlow() {
|
function resetStageFlow() {
|
||||||
['s1', 's2', 's3', 's4', 's5', 's6'].forEach(function (id) {
|
['s1', 's2', 's3', 's4', 's5', 's6'].forEach(function (id) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
@@ -779,7 +743,6 @@ function updateSystemStatusUi(data) {
|
|||||||
var total = ota.total_size || 0;
|
var total = ota.total_size || 0;
|
||||||
var stored = ota.stored_size || 0;
|
var stored = ota.stored_size || 0;
|
||||||
var storageProgress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
|
var storageProgress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
|
||||||
function setOtaProgressBadge() {} // OTA 进度卡片已移除,保留空壳避免调用报错
|
|
||||||
// GD32 relay forwarding progress
|
// GD32 relay forwarding progress
|
||||||
var relaySeq = ota.relay_seq || 0;
|
var relaySeq = ota.relay_seq || 0;
|
||||||
var relayTotal = ota.relay_total_packets || 0;
|
var relayTotal = ota.relay_total_packets || 0;
|
||||||
@@ -800,7 +763,6 @@ function updateSystemStatusUi(data) {
|
|||||||
} else if (isGd32Complete || rs === 5 || (gd32AppAlive && sysState === 'GD32_COMPLETE')) {
|
} else if (isGd32Complete || rs === 5 || (gd32AppAlive && sysState === 'GD32_COMPLETE')) {
|
||||||
markPhaseDone('gd32Ota');
|
markPhaseDone('gd32Ota');
|
||||||
currentOtaTarget = null; // GD32 OTA 完成,允许切换目标
|
currentOtaTarget = null; // GD32 OTA 完成,允许切换目标
|
||||||
setOtaProgressBadge(100);
|
|
||||||
setProgress(100, 'GD32 刷写完成');
|
setProgress(100, 'GD32 刷写完成');
|
||||||
var sbDone = document.getElementById('stageStatusBar');
|
var sbDone = document.getElementById('stageStatusBar');
|
||||||
var stDone = document.getElementById('stageStatusText');
|
var stDone = document.getElementById('stageStatusText');
|
||||||
@@ -809,30 +771,25 @@ function updateSystemStatusUi(data) {
|
|||||||
} else if (isGd32Storing || (total > 0 && stored < total && !isUploading)) {
|
} else if (isGd32Storing || (total > 0 && stored < total && !isUploading)) {
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
|
var storeOverall = Math.min(45, Math.round(storageProgress * 0.45));
|
||||||
setOtaProgressBadge(storeOverall);
|
|
||||||
if (!isUploading) {
|
if (!isUploading) {
|
||||||
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
|
setProgress(storeOverall, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
|
||||||
}
|
}
|
||||||
} else if (rs === 1) {
|
} else if (rs === 1) {
|
||||||
// HANDSHAKING: 握手 + 挑战认证
|
// HANDSHAKING: 握手 + 挑战认证
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
setOtaProgressBadge(50);
|
|
||||||
setProgress(50, '握手与认证中...');
|
setProgress(50, '握手与认证中...');
|
||||||
} else if (rs === 2) {
|
} else if (rs === 2) {
|
||||||
// SIZE_NEG: 固件大小协商
|
// SIZE_NEG: 固件大小协商
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
setOtaProgressBadge(55);
|
|
||||||
setProgress(55, '协商固件大小...');
|
setProgress(55, '协商固件大小...');
|
||||||
} else if (rs === 3) {
|
} else if (rs === 3) {
|
||||||
// TRANSFERRING: 数据包传输
|
// TRANSFERRING: 数据包传输
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
||||||
setOtaProgressBadge(relayOverall);
|
|
||||||
setProgress(relayOverall, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包');
|
setProgress(relayOverall, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包');
|
||||||
} else if (rs === 4) {
|
} else if (rs === 4) {
|
||||||
// FINISHING: 结束OTA + APP存活验证
|
// FINISHING: 结束OTA + APP存活验证
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
setOtaProgressBadge(95);
|
|
||||||
if (gd32AppAlive) {
|
if (gd32AppAlive) {
|
||||||
markPhaseDone('gd32Ota');
|
markPhaseDone('gd32Ota');
|
||||||
currentOtaTarget = null; // GD32 OTA 完成,允许切换目标
|
currentOtaTarget = null; // GD32 OTA 完成,允许切换目标
|
||||||
@@ -846,10 +803,7 @@ function updateSystemStatusUi(data) {
|
|||||||
}
|
}
|
||||||
} else if (total > 0 && stored >= total) {
|
} else if (total > 0 && stored >= total) {
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
setOtaProgressBadge(45);
|
|
||||||
setProgress(45, '上传完成,等待自动启动 GD32 OTA...');
|
setProgress(45, '上传完成,等待自动启动 GD32 OTA...');
|
||||||
} else {
|
|
||||||
setOtaProgressBadge(storageProgress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
||||||
@@ -921,62 +875,6 @@ async function refreshSystemStatusNow(event) {
|
|||||||
clearTimeout(manualTimer);
|
clearTimeout(manualTimer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ==================== CAN 帧发送 ====================
|
|
||||||
async function sendCanFrame() {
|
|
||||||
var chEl = document.getElementById('canChannel');
|
|
||||||
var idEl = document.getElementById('canId');
|
|
||||||
var dlcEl = document.getElementById('canDlc');
|
|
||||||
var dataEl = document.getElementById('canData');
|
|
||||||
if (!chEl || !idEl || !dlcEl || !dataEl) {
|
|
||||||
showToast('请在独立 CAN 页面中发送帧', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var ch = parseInt(chEl.value);
|
|
||||||
var idStr = idEl.value.trim();
|
|
||||||
var dlc = parseInt(dlcEl.value) || 8;
|
|
||||||
var dataStr = dataEl.value.trim();
|
|
||||||
var extId = 0;
|
|
||||||
if (idStr.toLowerCase().indexOf('0x') === 0) extId = parseInt(idStr, 16);
|
|
||||||
else { extId = parseInt(idStr, 10); if (isNaN(extId)) extId = parseInt(idStr, 16); }
|
|
||||||
if (isNaN(extId)) { showToast('ID 格式错误', 'error'); return; }
|
|
||||||
var dataHex = dataStr.replace(/\s/g, '');
|
|
||||||
if (dataHex.length % 2 !== 0) { showToast('数据长度必须为偶数', 'error'); return; }
|
|
||||||
addLog('I', 'CAN', '发送 CH' + ch + ' ID=0x' + extId.toString(16).toUpperCase() + ' DLC=' + dlc);
|
|
||||||
try {
|
|
||||||
var resp = await fetch(getBaseUrl() + '/can/send', {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ ch: ch, id: extId, dlc: dlc, data: dataHex }),
|
|
||||||
signal: AbortSignal.timeout(5000)
|
|
||||||
});
|
|
||||||
var result = await resp.json();
|
|
||||||
if (result.status === 'ok') {
|
|
||||||
addLog('I', 'CAN', '响应: ' + (result.recv || '(无数据)') + ' (' + (result.recv_len || 0) + 'B)');
|
|
||||||
var rd = document.getElementById('canResp');
|
|
||||||
rd.style.display = 'block';
|
|
||||||
document.getElementById('canRespText').textContent =
|
|
||||||
result.recv ? '0x' + result.recv + ' (' + result.recv_len + ' 字节)' : '无响应数据';
|
|
||||||
showToast('CH' + ch + ' 发送成功', 'success');
|
|
||||||
} else {
|
|
||||||
addLog('E', 'CAN', '发送失败: code=' + result.code);
|
|
||||||
showToast('发送失败: code=' + result.code, 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
addLog('E', 'CAN', '发送失败: ' + e.message);
|
|
||||||
showToast('发送失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function clearCanFields() {
|
|
||||||
var idEl = document.getElementById('canId');
|
|
||||||
var dlcEl = document.getElementById('canDlc');
|
|
||||||
var dataEl = document.getElementById('canData');
|
|
||||||
var respEl = document.getElementById('canResp');
|
|
||||||
if (!idEl || !dlcEl || !dataEl) return;
|
|
||||||
idEl.value = '0x18DA00F1';
|
|
||||||
dlcEl.value = '8';
|
|
||||||
dataEl.value = '0102030405060708';
|
|
||||||
if (respEl) respEl.style.display = 'none';
|
|
||||||
updateCanHint();
|
|
||||||
}
|
|
||||||
// ==================== SPI 发送 ====================
|
// ==================== SPI 发送 ====================
|
||||||
function updateSpiHint() {
|
function updateSpiHint() {
|
||||||
var typeHex = document.getElementById('spiType').value.trim();
|
var typeHex = document.getElementById('spiType').value.trim();
|
||||||
@@ -1118,10 +1016,6 @@ function setupDropZone(zoneId, chip) {
|
|||||||
}
|
}
|
||||||
setupDropZone('uploadZoneEsp32', 'esp32');
|
setupDropZone('uploadZoneEsp32', 'esp32');
|
||||||
setupDropZone('uploadZoneGd32', 'gd32');
|
setupDropZone('uploadZoneGd32', 'gd32');
|
||||||
var canDataEl = document.getElementById('canData');
|
|
||||||
var canDlcEl = document.getElementById('canDlc');
|
|
||||||
if (canDataEl) canDataEl.addEventListener('input', updateCanHint);
|
|
||||||
if (canDlcEl) canDlcEl.addEventListener('input', updateCanHint);
|
|
||||||
var spiTypeEl = document.getElementById('spiType');
|
var spiTypeEl = document.getElementById('spiType');
|
||||||
var spiDataEl = document.getElementById('spiData');
|
var spiDataEl = document.getElementById('spiData');
|
||||||
if (spiTypeEl) spiTypeEl.addEventListener('input', updateSpiHint);
|
if (spiTypeEl) spiTypeEl.addEventListener('input', updateSpiHint);
|
||||||
@@ -1599,9 +1493,9 @@ async function verifyEsp32Firmware() {
|
|||||||
// 服务器已设置启动分区并自动重启,前端直接进入重启等待状态
|
// 服务器已设置启动分区并自动重启,前端直接进入重启等待状态
|
||||||
addLog('I', 'OTA', 'SHA-256 校验通过,设备自动重启中...');
|
addLog('I', 'OTA', 'SHA-256 校验通过,设备自动重启中...');
|
||||||
showToast('校验通过,设备重启中...', 'success');
|
showToast('校验通过,设备重启中...', 'success');
|
||||||
showVerifyActions('none');
|
|
||||||
updateEsp32StagesFromPhase(4); // 应用固件
|
updateEsp32StagesFromPhase(4); // 应用固件
|
||||||
isEsp32Rebooting = true;
|
isEsp32Rebooting = true;
|
||||||
|
rebootObservedDown = false; // 重置:尚未观察到设备实际下线
|
||||||
updateEsp32StagesFromPhase(5); // 重启中
|
updateEsp32StagesFromPhase(5); // 重启中
|
||||||
rebootStartTime = Date.now();
|
rebootStartTime = Date.now();
|
||||||
} else {
|
} else {
|
||||||
@@ -1610,7 +1504,6 @@ async function verifyEsp32Firmware() {
|
|||||||
showToast('校验失败: ' + detail, 'error');
|
showToast('校验失败: ' + detail, 'error');
|
||||||
var resultEl = document.getElementById('sviResult');
|
var resultEl = document.getElementById('sviResult');
|
||||||
if (resultEl) { resultEl.textContent = '✗ ' + detail; resultEl.className = 'svi-val err'; }
|
if (resultEl) { resultEl.textContent = '✗ ' + detail; resultEl.className = 'svi-val err'; }
|
||||||
showVerifyActions('retry');
|
|
||||||
cleanupAfterOtaFailure('校验失败');
|
cleanupAfterOtaFailure('校验失败');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1621,70 +1514,10 @@ async function verifyEsp32Firmware() {
|
|||||||
showToast('应用请求失败: ' + e.message, 'error');
|
showToast('应用请求失败: ' + e.message, 'error');
|
||||||
var resultEl = document.getElementById('sviResult');
|
var resultEl = document.getElementById('sviResult');
|
||||||
if (resultEl) { resultEl.textContent = '✗ 请求失败: ' + e.message; resultEl.className = 'svi-val err'; }
|
if (resultEl) { resultEl.textContent = '✗ 请求失败: ' + e.message; resultEl.className = 'svi-val err'; }
|
||||||
showVerifyActions('retry');
|
|
||||||
cleanupAfterOtaFailure('应用请求失败');
|
cleanupAfterOtaFailure('应用请求失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retryEsp32Upload() {
|
|
||||||
addLog('I', 'OTA', '重新上传固件...');
|
|
||||||
showVerifyActions('none');
|
|
||||||
var verifyInfo = document.getElementById('stageVerifyInfo');
|
|
||||||
if (verifyInfo) verifyInfo.style.display = 'none';
|
|
||||||
var sviFront = document.getElementById('sviFrontSha');
|
|
||||||
var sviDevice = document.getElementById('sviDeviceSha');
|
|
||||||
var sviResult = document.getElementById('sviResult');
|
|
||||||
if (sviFront) sviFront.textContent = '-';
|
|
||||||
if (sviDevice) sviDevice.textContent = '-';
|
|
||||||
if (sviResult) { sviResult.textContent = '-'; sviResult.className = 'svi-val'; }
|
|
||||||
startUpload('esp32');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cancelEsp32Ota() {
|
|
||||||
addLog('I', 'OTA', '用户取消 OTA 刷写');
|
|
||||||
showToast('已取消 OTA', 'error');
|
|
||||||
showVerifyActions('none');
|
|
||||||
var xhr = activeXhr;
|
|
||||||
activeXhr = null;
|
|
||||||
if (xhr) {
|
|
||||||
try { xhr.abort(); } catch (_) {}
|
|
||||||
}
|
|
||||||
cleanupAfterOtaFailure('用户取消');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyEsp32Firmware() {
|
|
||||||
addLog('I', 'OTA', '确认应用固件并重启 ESP32...');
|
|
||||||
showToast('正在应用固件并重启...', 'success');
|
|
||||||
try {
|
|
||||||
var resp = await fetch(getBaseUrl() + '/ota/update', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
signal: AbortSignal.timeout(120000)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp.ok) {
|
|
||||||
var data = await resp.json();
|
|
||||||
addLog('I', 'OTA', '升级完成: ' + (data.message || ''));
|
|
||||||
updateEsp32StagesFromPhase(4); // 应用固件
|
|
||||||
isEsp32Rebooting = true;
|
|
||||||
updateEsp32StagesFromPhase(5); // 重启中
|
|
||||||
rebootStartTime = Date.now();
|
|
||||||
} else {
|
|
||||||
var detail = '应用失败: HTTP ' + resp.status;
|
|
||||||
try { var d = await resp.json(); detail += ' - ' + (d.message || ''); } catch (_) {}
|
|
||||||
addLog('E', 'OTA', detail);
|
|
||||||
showToast(detail, 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// fetch 异常仅代表请求未完成,不能推断设备已重启。
|
|
||||||
addLog('E', 'OTA', '应用请求失败: ' + e.message + '(设备状态未知,等待轮询探测)');
|
|
||||||
showToast('应用请求失败: ' + e.message, 'error');
|
|
||||||
cleanupAfterOtaFailure('应用请求失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function restartEsp32() {
|
async function restartEsp32() {
|
||||||
if (!confirm('确定要重启 ESP32 吗?')) return;
|
if (!confirm('确定要重启 ESP32 吗?')) return;
|
||||||
addLog('I', '系统', '=== 请求重启 ESP32 ===');
|
addLog('I', '系统', '=== 请求重启 ESP32 ===');
|
||||||
@@ -1698,6 +1531,7 @@ async function restartEsp32() {
|
|||||||
addLog('I', '系统', '重启已触发: ' + data.message);
|
addLog('I', '系统', '重启已触发: ' + data.message);
|
||||||
showToast('ESP32 重启中,页面将自动恢复...', 'success');
|
showToast('ESP32 重启中,页面将自动恢复...', 'success');
|
||||||
isEsp32Rebooting = true;
|
isEsp32Rebooting = true;
|
||||||
|
rebootObservedDown = false; // 重置:尚未观察到设备实际下线
|
||||||
rebootStartTime = Date.now();
|
rebootStartTime = Date.now();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1754,12 +1588,6 @@ async function triggerGd32Relay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 手动触发 GD32 OTA (调试/旧页面兜底) ====================
|
|
||||||
/** @description 触发 GD32 OTA 中继(封装函数,调用实际 API)。 */
|
|
||||||
async function triggerRelay() {
|
|
||||||
// 转发到统一入口:自动使用最近一次上传的 GD32 文件 SHA
|
|
||||||
await triggerGd32Relay();
|
|
||||||
}
|
|
||||||
// ==================== 轮询状态 ====================
|
// ==================== 轮询状态 ====================
|
||||||
var consecutivePollFailures = 0;
|
var consecutivePollFailures = 0;
|
||||||
var statusTimerId = null;
|
var statusTimerId = null;
|
||||||
@@ -1913,12 +1741,26 @@ async function pollStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isEsp32Rebooting) {
|
if (isEsp32Rebooting) {
|
||||||
isEsp32Rebooting = false;
|
var rbElapsed = Math.round((Date.now() - rebootStartTime) / 1000);
|
||||||
updateEsp32StagesFromPhase(6); // 启动完成
|
if (!rebootObservedDown && rbElapsed < 10) {
|
||||||
addLog('I', '系统', 'ESP32 已恢复在线');
|
// pre-reboot 窗口:后端发响应后约 1s 才 esp_restart,期间设备仍响应轮询
|
||||||
showToast('ESP32 已恢复在线', 'success');
|
// 保持"重启中"状态,不清除标志,不推进阶段
|
||||||
|
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); }
|
try { updateSystemStatusUi(data); } catch (e2) { console.warn('updateSystemStatusUi 异常:', e2); }
|
||||||
|
// pre-reboot 窗口内 updateSystemStatusUi 会把连接胶囊刷成"已连接",需覆盖回"重启中"
|
||||||
|
if (isEsp32Rebooting) {
|
||||||
|
applyRebootingRuntimeState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
clearTimeout(pollTimer);
|
clearTimeout(pollTimer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1954,9 +1796,11 @@ async function pollStatus() {
|
|||||||
if (isUploading) {
|
if (isUploading) {
|
||||||
applyUploadingRuntimeState();
|
applyUploadingRuntimeState();
|
||||||
} else if (isEsp32Rebooting) {
|
} else if (isEsp32Rebooting) {
|
||||||
|
rebootObservedDown = true; // 确认设备已下线(正在重启)
|
||||||
var elapsed = Math.round((Date.now() - rebootStartTime) / 1000);
|
var elapsed = Math.round((Date.now() - rebootStartTime) / 1000);
|
||||||
if (elapsed > 60) {
|
if (elapsed > 60) {
|
||||||
isEsp32Rebooting = false;
|
isEsp32Rebooting = false;
|
||||||
|
rebootObservedDown = false;
|
||||||
addLog('E', '系统', 'ESP32 重启超时,设备未恢复');
|
addLog('E', '系统', 'ESP32 重启超时,设备未恢复');
|
||||||
showToast('ESP32 重启超时,请检查设备', 'error');
|
showToast('ESP32 重启超时,请检查设备', 'error');
|
||||||
updateConnectionUi(false, '未连接');
|
updateConnectionUi(false, '未连接');
|
||||||
@@ -2030,7 +1874,6 @@ async function pollLogs() {
|
|||||||
}
|
}
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
updateUploadButtonLabel();
|
updateUploadButtonLabel();
|
||||||
updateCanHint();
|
|
||||||
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
|
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
|
||||||
updateSpiHint();
|
updateSpiHint();
|
||||||
// IP 输入变化时自动恢复轮询
|
// IP 输入变化时自动恢复轮询
|
||||||
|
|||||||
+1
-25
@@ -833,31 +833,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== 重启设备 + 自动重连 =====
|
// ===== 自动重连 =====
|
||||||
async function restartEsp32() {
|
|
||||||
if (!state.ctrlChar || !state.device) return;
|
|
||||||
if (!confirm('确定要重启 ESP32 吗?\n\n重启后页面会自动重连。')) return;
|
|
||||||
if (state.expectedReboot) return;
|
|
||||||
state.expectedReboot = true;
|
|
||||||
state.reconnectAttempts = 0;
|
|
||||||
if (typeof connectionMode !== 'undefined' && connectionMode === 'ble') {
|
|
||||||
if (els.connState) els.connState.textContent = '重启中';
|
|
||||||
if (els.connState && els.connState.parentElement) els.connState.parentElement.className = 'pill processing';
|
|
||||||
}
|
|
||||||
setConnectionState(!!state.server);
|
|
||||||
bleLog('SYS', '=== 触发 ESP32 重启 (0xFA) ===');
|
|
||||||
showToast('ESP32 重启中...', 'success');
|
|
||||||
try {
|
|
||||||
await state.ctrlChar.writeValue(new Uint8Array([0xFA]));
|
|
||||||
bleLog('SYS', '0xFA 已发,等待设备端推送 FINISH_REBOOT_NOW 后断开');
|
|
||||||
} catch (e) {
|
|
||||||
bleLog('SYS', '0xFA 发送失败: ' + e.message, 'E');
|
|
||||||
showToast('0xFA 发送失败', 'error');
|
|
||||||
state.expectedReboot = false;
|
|
||||||
setConnectionState(!!state.server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryReconnect() {
|
async function tryReconnect() {
|
||||||
if (!state.device || !state.expectedReboot) {
|
if (!state.device || !state.expectedReboot) {
|
||||||
state.reconnecting = false;
|
state.reconnecting = false;
|
||||||
|
|||||||
+7
-12
@@ -739,18 +739,13 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSelectedMessage() {
|
/**
|
||||||
// 顶部独立的“信号编辑”面板已移除,所有信号编辑现在通过报文行展开完成。
|
* @description 旧版“顶部独立信号编辑面板”的兼容 shim。
|
||||||
// 这里保留函数以兼容旧调用方(selectMessage 等),不再触碰已删除的 DOM。
|
* 该面板已移除,信号编辑现在通过报文行展开完成;
|
||||||
}
|
* 仍被 selectMessage、handleDbcFile、clearDbc、handleSignalInputEvent 调用,
|
||||||
|
* 保留空函数以避免修改所有调用方。
|
||||||
function metaCard(label, value) {
|
*/
|
||||||
return '<div class="can-meta-card"><span>' + sanitizeHtml(label) + '</span><b>' + sanitizeHtml(value) + '</b></div>';
|
function renderSelectedMessage() {}
|
||||||
}
|
|
||||||
|
|
||||||
function renderSignalGrid(/* message */) {
|
|
||||||
// 旧的独立信号编辑表已移除,信号编辑全部走报文行内展开 renderInlineSignalGrid。
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSignalRow(message, signal, index) {
|
function renderSignalRow(message, signal, index) {
|
||||||
var bag = getSignalValueMap(message);
|
var bag = getSignalValueMap(message);
|
||||||
|
|||||||
@@ -636,22 +636,6 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
|||||||
if (sentCount) sentCount.textContent = '0';
|
if (sentCount) sentCount.textContent = '0';
|
||||||
};
|
};
|
||||||
|
|
||||||
window.attachSharedFirmwareDropBridge = function () {
|
|
||||||
['esp32', 'gd32'].forEach(function (chip) {
|
|
||||||
var zone = document.getElementById(chip === 'gd32' ? 'uploadZoneGd32' : 'uploadZoneEsp32');
|
|
||||||
if (!zone) return;
|
|
||||||
zone.addEventListener('drop', function (event) {
|
|
||||||
var file = event && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0] ? event.dataTransfer.files[0] : null;
|
|
||||||
if (file) {
|
|
||||||
window.sharedFirmwareFiles[chip] = { file: file, handle: null };
|
|
||||||
}
|
|
||||||
window.flashTarget = chip;
|
|
||||||
applySharedModeUi();
|
|
||||||
setTimeout(syncBleFileFromSelectedTarget, 120);
|
|
||||||
}, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.applySharedModeUi = function () {
|
window.applySharedModeUi = function () {
|
||||||
var isBle = window.connectionMode === 'ble';
|
var isBle = window.connectionMode === 'ble';
|
||||||
var isGd32 = window.flashTarget === 'gd32';
|
var isGd32 = window.flashTarget === 'gd32';
|
||||||
|
|||||||
Reference in New Issue
Block a user