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:
小龙虾
2026-07-11 17:51:42 +08:00
parent fedb08e215
commit c0ca4e2a9f
18 changed files with 404 additions and 630 deletions
+124
View File
@@ -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 -4
View File
@@ -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"
+11 -21
View File
@@ -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 表示拿到一份有效数据。
-15
View File
@@ -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
+1 -57
View File
@@ -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();
-50
View File
@@ -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 回调:中继已开始。
-19
View File
@@ -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;
-16
View File
@@ -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:
-103
View File
@@ -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);
-2
View File
@@ -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); }
// ── 状态互斥锁(保护复合读-改-写操作)── // ── 状态互斥锁(保护复合读-改-写操作)──
-86
View File
@@ -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)
-24
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
-16
View File
@@ -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';