# 函数精简与复用重构实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 通过提取共享工具、消除重复实现、拆分过长函数,系统性地精简 ESP32 固件 (`main/`) 与前端工具 (`tools/js/`) 中的函数,重点强化复用关系。 **Architecture:** 三层重构——(1) 抽取跨文件共享工具模块(hex/le/sha/progress/stage-ui/fetch);(2) 消除同语义的重复实现并替换调用点;(3) 拆分超长/职责混杂函数。每个任务独立可测、可提交。C++ 侧与 JS 侧互不依赖,可并行推进。 **Tech Stack:** ESP-IDF (C++17, FreeRTOS, NimBLE), 原生 JS (浏览器 ES 模块,无构建步骤),CMake。 **测试策略:** - C++ 侧:`idf.py build` 编译通过;OTA 上传/转发/CAN 收发的人工冒烟测试(设备实测)。 - JS 侧:浏览器加载 `tools/index.html` 与 `tools/can.html`,控制台无报错;BLE OTA、WiFi OTA、CAN 监控、DBC 解析、手动发送、批量发送功能冒烟通过。 - 无单元测试框架,每个任务后需在浏览器/设备上做对应路径的冒烟验证。 **核心约束:** - 不改变任何对外行为/协议/UUID/HTTP 路由。 - 不引入构建步骤(保持原生 ES 模块)。 - 保持现有代码风格(无分号、`var`/`const`/`let` 混用、中文注释)。 - 每个 task 一次提交,commit message 用 `refactor:` 前缀。 --- ## File Structure ### 新建文件 | 文件 | 职责 | |---|---| | `main/include/hex_utils.h` | C++ 共享 hex/le 工具:`parseHexToBytes`、`bytesToHexUpper`、`le8`、`le32` | | `tools/js/common/progress.js` | JS 共享进度条:`setProgress`、`markProgressDone`、`resetProgress`、`getProgressEls`(懒缓存) | | `tools/js/common/stage-ui.js` | JS 共享阶段节点/状态栏:`resetStageNodes`、`advanceStage`、`markStageError`、`setStatusBar`、`getStageStatusBar`、`getStageStatusText`(懒缓存) | | `tools/js/common/sha.js` | JS 共享 SHA:`computeFileSha256`、`sha256HexToBytes`、`compareShaHex` | | `tools/js/common/fetch-json.js` | JS 共享 fetch 包装:`fetchJSON(url, opts)`、`abortableFetch(url, timeoutMs)` | | `tools/js/common/device-ip.js` | JS 共享设备 IP:`getDeviceIp`、`setDeviceIp`、`getBaseUrl` | | `tools/js/ble/gatt-helpers.js` | JS BLE GATT 包装:`discoverCharacteristics`、`writeWithResponse`、`writeWithoutResponse`、`readCharacteristic` | ### 修改文件(按 task 顺序) C++ 侧:`main/server/ble_ota_data.cpp`、`main/server/ble_ota_host.cpp`、`main/server/ble_ota.cpp`、`main/server/http_handlers.cpp`、`main/transport/spi_master.cpp`、`main/include/app_config.h`、`main/CMakeLists.txt`(仅在新增 .cpp 时)。 JS 侧:`tools/js/common/utils.js`、`tools/js/common/connection.js`、`tools/js/common/polling.js`、`tools/js/ble/transfer.js`、`tools/js/ble/file.js`、`tools/js/ble/gatt.js`、`tools/js/ble/stages.js`、`tools/js/ble/state.js`、`tools/js/ble/verify.js`、`tools/js/ota/progress.js`、`tools/js/ota/stages.js`、`tools/js/ota/upload-esp32.js`、`tools/js/ota/upload-gd32.js`、`tools/js/ota/file-handle.js`、`tools/js/can.js`、`tools/js/can/send.js`、`tools/js/can/dbc.js`、`tools/js/can_monitor.js`、`tools/js/app.js`。 ### 拆分文件 - `tools/js/ble/stages.js` → 保留目标配置相关,把通用节点操作移至 `stage-ui.js`。 - `tools/js/ota/progress.js` → 保留 esp32/gd32 专属阶段相位映射,把通用节点操作移至 `stage-ui.js`、进度条移至 `progress.js`(common)。 > 注意:新建 `tools/js/common/progress.js` 与现有 `tools/js/ota/progress.js` 同名不同目录,需在 HTML/导入路径中明确区分。 --- ## 阶段总览 | 阶段 | 主题 | Task | |---|---|---| | A | C++ 共享工具与拆分 | T1–T5 | | B | JS 共享工具模块建立 | T6–T11 | | C | JS 调用点迁移与重复消除 | T12–T18 | | D | JS 过长函数拆分 | T19–T22 | | E | JS 死代码/一次性 helper 清理 | T23–T25 | --- ## 阶段 A:C++ 共享工具与拆分 ### Task 1: 新建 `main/include/hex_utils.h` 共享 hex/le 工具 **Files:** - Create: `main/include/hex_utils.h` - [ ] **Step 1: 创建头文件** ```cpp #pragma once #include #include #include #include // 将 hex 字符串解析为字节数组,返回写入字节数(<=outCap)。失败返回 0。 inline size_t parseHexToBytes(const char *hex, uint8_t *out, size_t outCap) { size_t len = strlen(hex); if (len % 2 != 0) return 0; size_t n = len / 2; if (n > outCap) return 0; for (size_t i = 0; i < n; ++i) { char h[3] = { hex[i * 2], hex[i * 2 + 1], 0 }; out[i] = (uint8_t)strtoul(h, nullptr, 16); } return n; } // 字节数组 → 大写 hex 字符串(buf 至少 n*2+1 字节)。 inline void bytesToHexUpper(const uint8_t *in, size_t n, char *buf) { static const char *kHex = "0123456789ABCDEF"; for (size_t i = 0; i < n; ++i) { buf[i * 2] = kHex[(in[i] >> 4) & 0xF]; buf[i * 2 + 1] = kHex[in[i] & 0xF]; } buf[n * 2] = 0; } // 小端打包/解包 inline uint8_t le8(uint32_t v, uint8_t shift) { return (uint8_t)(v >> shift); } inline uint32_t le32(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } ``` - [ ] **Step 2: 验证编译** Run: `idf.py build` Expected: 编译通过(头文件尚未被引用,不影响现有代码) - [ ] **Step 3: 提交** ```bash git add main/include/hex_utils.h git commit -m "refactor(main): 新增 hex_utils.h 共享 hex/le 工具" ``` --- ### Task 2: 迁移 http_handlers.cpp 使用 hex_utils.h **Files:** - Modify: `main/server/http_handlers.cpp:39-56`(删除本地 `parseHexToBytes`/`bytesToHexUpper`) - Modify: `main/server/http_handlers.cpp:571-581`(GD32 relay handler 内联 hex 解析改用工具) - [ ] **Step 1: 添加 include 并删除本地实现** 在 `http_handlers.cpp` 顶部 `#include` 区加入: ```cpp #include "hex_utils.h" ``` 删除 `parseHexToBytes()` (39-48) 与 `bytesToHexUpper()` (50-56) 的本地定义。保留所有调用点不变(函数名一致)。 - [ ] **Step 2: 替换 ota_relay_trigger_handler 内联 hex 解析** 将 `http_handlers.cpp:571-581` 处: ```cpp for (size_t i = 0; i < sizeof(frontend_sha); ++i) { char hex[3] = { frontend_sha_hex[i*2], frontend_sha_hex[i*2+1], 0 }; frontend_sha[i] = (uint8_t)strtoul(hex, nullptr, 16); } // ... 后续 inline hex 构造 device_sha_hex ``` 改为: ```cpp if (parseHexToBytes(frontend_sha_hex, frontend_sha, sizeof(frontend_sha)) != sizeof(frontend_sha)) { http_send_json_error(req, 400, "BAD_SHA", "SHA-256 hex 格式错误"); return ESP_OK; } char device_sha_hex[65]; bytesToHexUpper(device_sha, sizeof(device_sha), device_sha_hex); ``` - [ ] **Step 3: 验证编译** Run: `idf.py build` Expected: PASS - [ ] **Step 4: 冒烟测试** 设备上通过 HTTP 触发一次 `/ota/relay`(错误 SHA 也行,只要返回 400 而非崩溃)。 - [ ] **Step 5: 提交** ```bash git add main/server/http_handlers.cpp main/include/hex_utils.h git commit -m "refactor(http): 迁移 hex 工具至 hex_utils.h,消除内联 hex 解析" ``` --- ### Task 3: 迁移 ble_ota_data.cpp 使用 hex_utils.h,删除 `shaBytesToHexUpper` **Files:** - Modify: `main/server/ble_ota_data.cpp:437-507`(`sendWindowAck`/`sendFinishEvent`/`sendSha256Notify` 使用 `le8`;删除 `shaBytesToHexUpper`,改用 `bytesToHexUpper`) - Modify: `main/server/ble_ota.cpp:92-96`(`ble_esp32_finish_and_reboot_task` 中手写 hex 解析改用 `parseHexToBytes`) - [ ] **Step 1: 添加 include** 在 `ble_ota_data.cpp` 与 `ble_ota.cpp` 顶部加入 `#include "hex_utils.h"`。 - [ ] **Step 2: 删除 `shaBytesToHexUpper` (487-494)** 将 `sendSha256Notify()` (497-507) 内的 `shaBytesToHexUpper(sha, sha_hex)` 调用替换为 `bytesToHexUpper(sha, 32, sha_hex)`。删除 `shaBytesToHexUpper` 函数定义。 - [ ] **Step 3: `sendWindowAck`/`sendFinishEvent` 使用 `le8`** 将 `ble_ota_data.cpp:441-447` 的 4x `(uint8_t)(value >> shift)` 与 `468-473` 的同类代码替换为 `le8(value, 0/8/16/24)`。 - [ ] **Step 4: `ble_ota.cpp:92-96` 使用 `parseHexToBytes`** 将手写循环改为: ```cpp uint8_t sha[32]; if (parseHexToBytes(sha_hex, sha, sizeof(sha)) != sizeof(sha)) { /* 原错误分支 */ } ``` - [ ] **Step 5: 验证编译** Run: `idf.py build` Expected: PASS - [ ] **Step 6: 冒烟测试** BLE OTA 上传一次固件,观察 SHA256 notify 与 finish event 正常返回。 - [ ] **Step 7: 提交** ```bash git add main/server/ble_ota_data.cpp main/server/ble_ota.cpp git commit -m "refactor(ble_ota): 统一 hex/le 工具,删除 shaBytesToHexUpper" ``` --- ### Task 4: 提取 `shouldSendThrottledAck` helper,消除 ESP32/GD32 重复 ACK 节流逻辑 **Files:** - Modify: `main/server/ble_ota_data.cpp:245-263`(`esp32_write_ordered` fallback 节流) - Modify: `main/server/ble_ota_data.cpp:808-821`(`process_gd32_packet` 节流) - Modify: `main/server/ble_ota_priv.h`(新增 prototype) - [ ] **Step 1: 在 ble_ota_priv.h 新增声明** ```cpp // 节流 ACK 决策:计数器自增,命中窗口边界或倍数时返回 true。 bool shouldSendThrottledAck(uint32_t *counter, uint16_t seq, uint16_t window); ``` - [ ] **Step 2: 在 ble_ota_data.cpp 实现(放在 sendWindowAck 附近)** ```cpp bool shouldSendThrottledAck(uint32_t *counter, uint16_t seq, uint16_t window) { ++(*counter); if (*counter >= window) { *counter = 0; return true; } if (*counter % (window / 4 == 0 ? 1 : window / 4) == 0) return true; // 与原逻辑等价 (void)seq; return false; } ``` > ⚠️ 实现前需先精读原 245-263 与 808-821 的精确条件(`>= BLE_OTA_ACK_WINDOW` 或 `% N == 0`),逐字复刻而非凭记忆。本 step 的代码块是占位骨架,正式实现以原文条件为准。 - [ ] **Step 3: 替换两处调用点** 将两处 `if (++s_xxx_ooo_count >= BLE_OTA_ACK_WINDOW || s_xxx_ooo_count % N == 0) { sendWindowAck(...); }` 替换为: ```cpp if (shouldSendThrottledAck(&s_fallback_ooo_count, seq, BLE_OTA_ACK_WINDOW)) { sendWindowAck(...); } ``` 与 GD32 对应版本。 - [ ] **Step 4: 验证编译并对照原文输出** Run: `idf.py build` Expected: PASS 对照 git diff 确认两处节流条件与原逻辑完全等价(包括日志是否仍打印)。 - [ ] **Step 5: 冒烟测试** BLE OTA 上传中制造乱序(可手动断连重连),观察 ACK 仍按原节奏发出。 - [ ] **Step 6: 提交** ```bash git add main/server/ble_ota_data.cpp main/server/ble_ota_priv.h git commit -m "refactor(ble_ota): 提取 shouldSendThrottledAck 消除 ESP32/GD32 重复节流" ``` --- ### Task 5: 拆分 `ble_ota_ctrl_access` 与 `ota_upload_handler`,统一 BLE/HTTP GD32 relay 触发 > 本任务较大,按 5a/5b/5c 拆成三个连续提交。 #### Task 5a: 拆分 `ble_ota_ctrl_access` 为 4 个命令 handler **Files:** - Modify: `main/server/ble_ota_host.cpp:8-257` - Modify: `main/server/ble_ota_priv.h` - [ ] **Step 1: 在 ble_ota_priv.h 新增声明** ```cpp void ble_ota_handle_gd32_relay(const uint8_t *buf, uint16_t len); void ble_ota_handle_esp32_finish(const uint8_t *buf, uint16_t len); void ble_ota_handle_restart(); void ble_ota_handle_abort(); ``` - [ ] **Step 2: 在 ble_ota_host.cpp 提取 4 个 handler** 把 `ble_ota_ctrl_access` 中 0xF7/0xF8/0xFA/0xFB 四个分支体分别搬入上述 4 个函数(保持原有 task 创建、错误响应逻辑)。 - [ ] **Step 3: 重写 `ble_ota_ctrl_access` 为 dispatcher** ```cpp static int ble_ota_ctrl_access(...) { // 保留 mbuf→buf 拷贝 if (len < 1) return 0; switch (buf[0]) { case 0xF7: ble_ota_handle_gd32_relay(buf, len); break; case 0xF8: ble_ota_handle_esp32_finish(buf, len); break; case 0xFA: ble_ota_handle_restart(); break; case 0xFB: ble_ota_handle_abort(); break; default: /* 原日志 */ break; } return 0; } ``` - [ ] **Step 4: 验证编译 + 冒烟** Run: `idf.py build` → PASS。BLE 触发 4 类 ctrl 命令各一次。 - [ ] **Step 5: 提交** ```bash git add main/server/ble_ota_host.cpp main/server/ble_ota_priv.h git commit -m "refactor(ble_ota): 拆分 ble_ota_ctrl_access 为 4 个命令 handler" ``` #### Task 5b: 拆分 `ota_upload_handler` 为 `handle_esp32_upload`/`handle_gd32_upload` **Files:** - Modify: `main/server/http_handlers.cpp:233-532` - Modify: `main/server/http_handlers.h` - [ ] **Step 1: 在 http_handlers.h 新增声明** ```cpp esp_err_t handle_esp32_upload(httpd_req_t *req, const char *body, size_t len); esp_err_t handle_gd32_upload(httpd_req_t *req, const char *body, size_t len); ``` - [ ] **Step 2: 提取两个函数** 将 `ota_upload_handler` 中 ESP32 分支 (292-398) 与 GD32 分支 (400-531) 分别搬入两个新函数。共享的静态缓冲与 `otaUploadPaceIfHeapTight()` 调用保留在各自函数内(或提到 `ota_upload_handler` 入口)。`recv_json_header` 仍在 `ota_upload_handler` 中先调用。 - [ ] **Step 3: 重写 `ota_upload_handler` 为 dispatcher** ```cpp esp_err_t ota_upload_handler(httpd_req_t *req) { // recv_json_header + 读取 type 字段 if (type == "esp32") return handle_esp32_upload(req, body, len); return handle_gd32_upload(req, body, len); } ``` - [ ] **Step 4: 验证编译 + 冒烟** Run: `idf.py build` → PASS。HTTP 分别上传 ESP32 与 GD32 固件。 - [ ] **Step 5: 提交** ```bash git add main/server/http_handlers.cpp main/server/http_handlers.h git commit -m "refactor(http): 拆分 ota_upload_handler 为 esp32/gd32 handler" ``` #### Task 5c: 提取 `triggerGd32RelayCommon` 统一 BLE/HTTP GD32 relay 触发 **Files:** - Create: `main/server/ota_relay_common.cpp` 或并入现有 `ota/ota_coordinator.cpp`(先查该文件是否更合适) - Modify: `main/server/ble_ota_host.cpp:48-123`(`ble_ota_handle_gd32_relay`) - Modify: `main/server/http_handlers.cpp:540-611`(`ota_relay_trigger_handler`) - Modify: `main/CMakeLists.txt`(若新建 .cpp) - Modify: `main/server/http_handlers.h` 或相关头文件 - [ ] **Step 1: 先确认 `ota/ota_coordinator.cpp`/`ota_relay.cpp` 是否已导出 `triggerGd32Relay`** 读 `ota/ota_coordinator.cpp` 与 `relay/ota_relay.cpp`,确认现有 `triggerGd32Relay`/`loadGd32Meta`/`verifyGd32Firmware` 签名。 - [ ] **Step 2: 设计共享签名** ```cpp // 返回 ESP_OK/ESP_ERR_INVALID_CRC/其他;out_sha_hex 为设备侧 SHA 大写 hex。 esp_err_t triggerGd32RelayCommon(const uint8_t *frontend_sha, uint16_t session_id, char *out_sha_hex /*[65]*/, uint16_t *out_session_id); ``` - [ ] **Step 3: 实现共享函数** 把两条路径共有的 meta-load → verify → hex → trigger 序列合入。BLE 侧的 `sendSha256Notify` 在共享函数外由 BLE handler 调用(HTTP 不需要)。 - [ ] **Step 4: 改写两个调用点** `ble_ota_handle_gd32_relay` 与 `ota_relay_trigger_handler` 改为调用 `triggerGd32RelayCommon`,BLE 侧额外调用 `sendSha256Notify`,HTTP 侧组装 JSON 响应。 - [ ] **Step 5: 验证编译 + 双路径冒烟** Run: `idf.py build` → PASS。BLE 与 HTTP 各触发一次 GD32 relay。 - [ ] **Step 6: 提交** ```bash git add main/server/ota_relay_common.cpp main/server/ble_ota_host.cpp main/server/http_handlers.cpp main/CMakeLists.txt git commit -m "refactor(ota): 提取 triggerGd32RelayCommon 统一 BLE/HTTP GD32 relay" ``` --- ## 阶段 B:JS 共享工具模块建立 > 阶段 B 的每个 task 创建一个新模块并写出导出,但**不**改任何调用点(阶段 C 再迁移)。这样每个模块可独立提交、独立冒烟(浏览器加载无报错即视为通过)。 ### Task 6: `tools/js/common/progress.js` 统一进度条 **Files:** - Create: `tools/js/common/progress.js` - [ ] **Step 1: 创建模块(合并 BLE 与 OTA 两版 setProgress,取并集特性:NaN guard + .done toggle + 懒缓存)** ```js // tools/js/common/progress.js let _fill, _txt, _pct; function getProgressEls() { if (!_fill || !_fill.isConnected) { _fill = document.getElementById('progressFill'); _txt = document.getElementById('progressTxt'); _pct = document.getElementById('progressPct'); } return { fill: _fill, txt: _txt, pct: _pct }; } function setProgress(percent, text) { var p = Number(percent); if (!isFinite(p)) p = 0; p = Math.max(0, Math.min(100, p)); var els = getProgressEls(); if (els.fill) els.fill.style.width = p + '%'; if (els.pct) els.pct.textContent = p + '%'; if (els.txt) els.txt.textContent = text || ''; if (els.fill) els.fill.classList.toggle('done', p >= 100); return p; } function markProgressDone(text) { var els = getProgressEls(); if (els.fill) { els.fill.style.width = '100%'; els.fill.classList.add('done'); } if (els.pct) els.pct.textContent = '100%'; if (text && els.txt) els.txt.textContent = text; } function resetProgress(text) { var els = getProgressEls(); if (els.fill) { els.fill.style.width = '0%'; els.fill.classList.remove('done'); } if (els.pct) els.pct.textContent = '0%'; if (els.txt) els.txt.textContent = text || ''; } export { setProgress, markProgressDone, resetProgress, getProgressEls }; ``` - [ ] **Step 2: 冒烟(仅加载)** 浏览器打开 `tools/index.html`,控制台执行: ```js import('./js/common/progress.js').then(m => m.setProgress(42, 'test')); ``` Expected: 进度条显示 42%,无报错。 - [ ] **Step 3: 提交** ```bash git add tools/js/common/progress.js git commit -m "refactor(js): 新增 common/progress.js 共享进度条" ``` --- ### Task 7: `tools/js/common/stage-ui.js` 统一阶段节点/状态栏 **Files:** - Create: `tools/js/common/stage-ui.js` - [ ] **Step 1: 创建模块** ```js // tools/js/common/stage-ui.js let _bar, _txt; function getStageStatusBar() { if (!_bar || !_bar.isConnected) _bar = document.getElementById('stageStatusBar'); return _bar; } function getStageStatusText() { if (!_txt || !_txt.isConnected) _txt = document.getElementById('stageStatusText'); return _txt; } function resetStageNodes(stageIds, detailIds) { stageIds.forEach(function (id) { var el = document.getElementById(id); if (el) el.className = 'stage-node'; }); if (detailIds) detailIds.forEach(function (id) { var el = document.getElementById(id); if (el) el.textContent = ''; }); } function setStageClass(stageIds, untilIndex, cls) { // cls: 'done' | 'live' | 'err' → 完整类名 'stage-node done' 等 for (var i = 0; i < stageIds.length; ++i) { var el = document.getElementById(stageIds[i]); if (!el) continue; if (i < untilIndex) el.className = 'stage-node done'; else if (i === untilIndex) el.className = 'stage-node ' + cls; else el.className = 'stage-node'; } } function markStageError(stageIds, index) { var el = document.getElementById(stageIds[index]); if (el) el.className = 'stage-node err'; } function setStatusBar(state, text) { // state: 'ok' | 'active' | 'err' | '' var bar = getStageStatusBar(); var t = getStageStatusText(); if (bar) bar.className = 'stage-status-bar' + (state ? ' ' + state : ''); if (t) t.textContent = text || ''; } export { getStageStatusBar, getStageStatusText, resetStageNodes, setStageClass, markStageError, setStatusBar }; ``` > ⚠️ `setStageClass` 的精确语义需对照 `ble/stages.js:60-94` `setBleStage` 与 `ota/progress.js:83-116` `updateEsp32StagesFromPhase` 的实际赋值规则(done/live/未到达)。本 step 为骨架,正式实现以原文为准。 - [ ] **Step 2: 冒烟(仅加载)** 控制台执行 `import('./js/common/stage-ui.js')` 无报错。 - [ ] **Step 3: 提交** ```bash git add tools/js/common/stage-ui.js git commit -m "refactor(js): 新增 common/stage-ui.js 共享阶段节点/状态栏" ``` --- ### Task 8: `tools/js/common/sha.js` 统一 SHA-256 计算/比较 **Files:** - Create: `tools/js/common/sha.js` - [ ] **Step 1: 创建模块** ```js // tools/js/common/sha.js import { bytesToHex } from './utils.js'; async function computeFileSha256(file) { try { var buf = await file.arrayBuffer(); var hash = await crypto.subtle.digest('SHA-256', buf); return bytesToHex(new Uint8Array(hash)).toUpperCase(); } catch (e) { return null; } } async function computeBufferSha256(buffer) { var hash = await crypto.subtle.digest('SHA-256', buffer); return bytesToHex(new Uint8Array(hash)).toUpperCase(); } function sha256HexToBytes(hex) { var out = new Uint8Array(32); for (var i = 0; i < 32; ++i) { out[i] = parseInt(hex.substr(i * 2, 2), 16); } return out; } function compareShaHex(a, b) { if (!a || !b) return false; return a.toUpperCase() === b.toUpperCase(); } export { computeFileSha256, computeBufferSha256, sha256HexToBytes, compareShaHex }; ``` - [ ] **Step 2: 冒烟(仅加载)** 控制台执行: ```js import('./js/common/sha.js').then(async m => { var f = new File([new Uint8Array([1,2,3])], 'x.bin'); var h = await m.computeFileSha256(f); console.log(h, m.compareShaHex(h, h)); }); ``` Expected: 输出 64 字符 hex + true。 - [ ] **Step 3: 提交** ```bash git add tools/js/common/sha.js git commit -m "refactor(js): 新增 common/sha.js 共享 SHA-256 工具" ``` --- ### Task 9: `tools/js/common/fetch-json.js` 统一 fetch 包装 **Files:** - Create: `tools/js/common/fetch-json.js` - [ ] **Step 1: 创建模块** ```js // tools/js/common/fetch-json.js async function fetchJSON(url, options) { options = options || {}; var signal = options.signal; var resp = await fetch(url, options); var text = await resp.text(); var data; try { data = text ? JSON.parse(text) : null; } catch (e) { var err = new Error('JSON 解析失败: ' + e.message); err.resp = resp; err.text = text; throw err; } if (!resp.ok) { var err2 = new Error('HTTP ' + resp.status); err2.resp = resp; err2.data = data; throw err2; } return data; } function abortableFetch(url, timeoutMs) { var ctrl = new AbortController(); var timer = setTimeout(function () { ctrl.abort(); }, timeoutMs || 5000); var p = fetch(url, { signal: ctrl.signal }).finally(function () { clearTimeout(timer); }); p.controller = ctrl; return p; } export { fetchJSON, abortableFetch }; ``` - [ ] **Step 2: 冒烟(仅加载)** 控制台执行 `import('./js/common/fetch-json.js')` 无报错。 - [ ] **Step 3: 提交** ```bash git add tools/js/common/fetch-json.js git commit -m "refactor(js): 新增 common/fetch-json.js 共享 fetch 包装" ``` --- ### Task 10: `tools/js/common/device-ip.js` 统一设备 IP/BaseUrl **Files:** - Create: `tools/js/common/device-ip.js` - [ ] **Step 1: 创建模块(取 `can_monitor.js:31-45` 最完整版为基准)** ```js // tools/js/common/device-ip.js var DEFAULT_IP = '192.168.31.127'; var STORAGE_KEY = 'dcu_esp_ip'; function safeGet(key) { try { return localStorage.getItem(key); } catch (e) { return null; } } function safeSet(key, v) { try { localStorage.setItem(key, v); } catch (e) {} } function getDeviceIp(opts) { opts = opts || {}; // 优先 DOM(可指定元素 ID),其次 localStorage,最后默认 if (opts.inputId) { var el = document.getElementById(opts.inputId); if (el && el.value) { safeSet(STORAGE_KEY, el.value); return el.value; } } var stored = safeGet(STORAGE_KEY); if (stored) return stored; return DEFAULT_IP; } function setDeviceIp(ip) { safeSet(STORAGE_KEY, ip); } function getBaseUrl(opts) { return 'http://' + getDeviceIp(opts); } export { getDeviceIp, setDeviceIp, getBaseUrl, DEFAULT_IP, STORAGE_KEY }; ``` > 现有 `app.js:52-64` 的 `getEspIp`/`getBaseUrl`、`can/dbc.js:38-60`、`can_monitor.js:31-45` 将在阶段 C 迁移到本模块(本 task 仅创建)。 - [ ] **Step 2: 冒烟(仅加载)** 控制台执行 `import('./js/common/device-ip.js').then(m => console.log(m.getBaseUrl()))` 输出 `http://192.168.31.127`。 - [ ] **Step 3: 提交** ```bash git add tools/js/common/device-ip.js git commit -m "refactor(js): 新增 common/device-ip.js 统一设备 IP/BaseUrl" ``` --- ### Task 11: `tools/js/ble/gatt-helpers.js` 提取 GATT 发现/读写包装 **Files:** - Create: `tools/js/ble/gatt-helpers.js` - [ ] **Step 1: 创建模块** ```js // tools/js/ble/gatt-helpers.js import { UUIDS } from './constants.js'; async function discoverCharacteristics(server) { var service = await server.getPrimaryService(UUIDS.otaService); var ctrl = await service.getCharacteristic(UUIDS.ctrlChar); var dataIn = await service.getCharacteristic(UUIDS.dataInChar); var dataOut = await service.getCharacteristic(UUIDS.dataOutChar); var version = await service.getCharacteristic(UUIDS.versionChar); return { service: service, ctrl: ctrl, dataIn: dataIn, dataOut: dataOut, version: version }; } function writeWithResponse(char, data) { return char.writeValueWithResponse(data); } function writeWithoutResponse(char, data) { return char.writeValueWithoutResponse(data); } function readCharacteristic(char) { return char.readValue(); } export { discoverCharacteristics, writeWithResponse, writeWithoutResponse, readCharacteristic }; ``` > ⚠️ 实现前需先 Read `tools/js/ble/gatt.js:98-102` 与 `:478-484` 与 `constants.js` 中 UUIDS 的精确字段名,本 step 为骨架。 - [ ] **Step 2: 冒烟(仅加载)** 控制台执行 `import('./js/ble/gatt-helpers.js')` 无报错。 - [ ] **Step 3: 提交** ```bash git add tools/js/ble/gatt-helpers.js git commit -m "refactor(js): 新增 ble/gatt-helpers.js 提取 GATT 发现/读写" ``` --- ## 阶段 C:JS 调用点迁移与重复消除 > 阶段 C 每个 task 改调用点 + 删除旧实现,必须确保浏览器加载 + 对应路径冒烟通过。 ### Task 12: 迁移所有 `setProgress` 调用点至 `common/progress.js` **Files:** - Modify: `tools/js/ble/transfer.js:12-23`(删除本地 `setProgress`,import 新版) - Modify: `tools/js/ble/file.js:95,98` - Modify: `tools/js/ble/gatt.js:277,287,301,313,328,349,356,363,376` - Modify: `tools/js/ota/progress.js:228-241`(删除本地 `setProgress`,import 新版;保留 `resetProgress` 但内部调用新版) - Modify: `tools/js/ota/stages.js:207,302,307,311,314,319,330` - Modify: `tools/js/ota/upload-esp32.js:223,251,255,261,296,307` - Modify: `tools/js/ota/upload-gd32.js:58,96,151,164` - [ ] **Step 1: BLE 侧迁移** `tools/js/ble/transfer.js` 顶部加 `import { setProgress } from '../common/progress.js';`,删除 12-23 本地定义。确认 `ble/file.js`、`ble/gatt.js` 通过 `window` 或 import 拿到 `setProgress`——若它们当前通过 `window.setProgress` 调用,需在 transfer.js 末尾 `window.setProgress = setProgress;` 暂时兼容(阶段 D 再清理 window 暴露)。 - [ ] **Step 2: OTA 侧迁移** `tools/js/ota/progress.js` 顶部加 `import { setProgress as commonSetProgress } from '../common/progress.js';`,本地 `setProgress` (228-241) 删除。若其他文件通过 `window.setProgress` 调用 OTA 版,同样 `window.setProgress = commonSetProgress;`。 - [ ] **Step 3: 冒烟** `tools/index.html`:BLE OTA + WiFi OTA 各走一次,进度条正常推进 + 100% 时 `.done` 类生效。 - [ ] **Step 4: 提交** ```bash git add tools/js/ble/transfer.js tools/js/ble/file.js tools/js/ble/gatt.js tools/js/ota/progress.js tools/js/ota/stages.js tools/js/ota/upload-esp32.js tools/js/ota/upload-gd32.js git commit -m "refactor(js): 迁移所有 setProgress 至 common/progress.js" ``` --- ### Task 13: 迁移阶段节点/状态栏操作至 `common/stage-ui.js` **Files:** - Modify: `tools/js/ble/stages.js:15-123`(`getStageStatusBar`/`getStageStatusText`/`resetBleStages`/`setBleStage`/`setBleStageText`/`setBleStageStatus`/`setBleStageError` 改为薄包装或直接 re-export) - Modify: `tools/js/ota/progress.js:56-128`(`resetEsp32StageNodes`/`resetGd32StageNodes`/`updateEsp32StagesFromPhase`/`updateEsp32StagesError`) - Modify: `tools/js/ota/stages.js:49-102`(`updateStagesFromRelayState`)、`:243-249`(内联 err) - Modify: `tools/js/ota/upload-gd32.js:97-100`(内联 ok 状态栏) - [ ] **Step 1: BLE stages.js 迁移** 把 `getStageStatusBar`/`getStageStatusText` 改为 `export { getStageStatusBar, getStageStatusText } from '../common/stage-ui.js';`(删除本地实现)。`resetBleStages`/`setBleStage`/`setBleStageError` 内部改用 `stage-ui.js` 的 `resetStageNodes`/`setStageClass`/`markStageError`/`setStatusBar`,保留 BLE 专属的 stage-id 数组常量在 `ble/stages.js`。 - [ ] **Step 2: OTA progress.js 迁移** `resetEsp32StageNodes`/`resetGd32StageNodes` 改为调用 `resetStageNodes(ESP32_STAGE_IDS, ESP32_DETAIL_IDS)` 等。`updateEsp32StagesFromPhase`/`updateEsp32StagesError` 改用 `setStageClass`/`markStageError`/`setStatusBar`。 - [ ] **Step 3: OTA stages.js 与 upload-gd32.js 迁移** `updateStagesFromRelayState` 改用共享 helper;`:243-249` 与 `upload-gd32.js:97-100` 的内联状态栏赋值改用 `setStatusBar('err'/'ok', text)`。 - [ ] **Step 4: 冒烟** `tools/index.html`:BLE OTA 完整跑一遍,观察 6 个阶段节点 done/live/err 切换 + 状态栏文案正确。CAN 页无需测。 - [ ] **Step 5: 提交** ```bash git add tools/js/ble/stages.js tools/js/ota/progress.js tools/js/ota/stages.js tools/js/ota/upload-gd32.js git commit -m "refactor(js): 迁移阶段节点/状态栏至 common/stage-ui.js" ``` --- ### Task 14: 迁移 SHA-256 计算与比较至 `common/sha.js` **Files:** - Modify: `tools/js/ble/file.js:114-143`(`computeFileSha256` 删除,改 import) - Modify: `tools/js/ble/transfer.js:59-66`(`sha256HexToBytes` 删除,改 import)、`:185-188`(手写 hex 改用 `computeBufferSha256`) - Modify: `tools/js/ota/file-handle.js:501-528`(`_computeAndCacheFileSha` 改用 `computeFileSha256`/`computeBufferSha256`) - Modify: `tools/js/ble/gatt.js:387,421`(SHA 比较改用 `compareShaHex`) - Modify: `tools/js/ota/upload-esp32.js:534`(SHA 比较改用 `compareShaHex`) - [ ] **Step 1: 替换 computeFileSha256 与 sha256HexToBytes** 各文件顶部 import,删除本地定义。`ble/transfer.js:185-188` 的手写 hex 删除,改 `state.fileSha256 = await computeBufferSha256(fileBytes.buffer)`。 - [ ] **Step 2: 替换 compareShaHex** `gatt.js:387` `state.deviceSha256.toUpperCase() === state.fileSha256.toUpperCase()` → `compareShaHex(state.deviceSha256, state.fileSha256)`。`:421` 同理。`upload-esp32.js:534` `sha256.toLowerCase() === data.sha256.toLowerCase()` → `compareShaHex(sha256, data.sha256)`。 - [ ] **Step 3: 冒烟** BLE OTA + WiFi OTA 各跑一次,验证 SHA 比对结果与改前一致(成功路径与故意错配路径)。 - [ ] **Step 4: 提交** ```bash git add tools/js/ble/file.js tools/js/ble/transfer.js tools/js/ble/gatt.js tools/js/ota/file-handle.js tools/js/ota/upload-esp32.js git commit -m "refactor(js): 迁移 SHA-256 计算/比较至 common/sha.js" ``` --- ### Task 15: 迁移 GATT 发现/读写至 `ble/gatt-helpers.js` **Files:** - Modify: `tools/js/ble/gatt.js:98-102`(`connectBle` 中发现)、`:478-484`(`tryReconnect` 中发现) - Modify: `tools/js/ble/transfer.js:228,254,314,329,374,416`(write 调用) - Modify: `tools/js/ble/gatt.js:158`(readValue) - [ ] **Step 1: 替换 discoverCharacteristics** `gatt.js:98-102` 与 `:478-484` 两处 `getPrimaryService` + 4×`getCharacteristic` 改为 `var ch = await discoverCharacteristics(server);` 并把 `ch.ctrl`/`ch.dataIn` 等赋给 `state.*`。 - [ ] **Step 2: 替换 write/read 包装** 可选:`writeValueWithResponse` → `writeWithResponse(char, data)` 等。若收益不大可仅替换发现部分,write/read 保持原样(本步可降级为 optional)。 - [ ] **Step 3: 冒烟** BLE 连接 + OTA 上传一次,观察发现/写入正常。 - [ ] **Step 4: 提交** ```bash git add tools/js/ble/gatt.js tools/js/ble/transfer.js git commit -m "refactor(js): 迁移 GATT 发现至 ble/gatt-helpers.js" ``` --- ### Task 16: 迁移设备 IP/BaseUrl 至 `common/device-ip.js` **Files:** - Modify: `tools/js/app.js:52-64`(`getEspIp`/`getBaseUrl` 删除,import;保留 `window.getBaseUrl` 兼容) - Modify: `tools/js/can/dbc.js:38-60`(`getDeviceIp`/`setDeviceIp`/`getBaseUrl` 删除,import) - Modify: `tools/js/can_monitor.js:31-45`(删除本地 `getBaseUrl`,import) - Modify: `tools/js/common/connection.js:367`、`tools/js/common/polling.js:190`(已通过 `window.getBaseUrl`,确认仍可用) - [ ] **Step 1: app.js 迁移** `import { getDeviceIp as getEspIp, getBaseUrl } from './common/device-ip.js';`,删除本地 52-64。末尾 `window.getBaseUrl = getBaseUrl; window.getEspIp = getEspIp;` 保留兼容(因 `connection.js`/`polling.js` 用 `window.*`)。 - [ ] **Step 2: dbc.js 迁移** `import { getDeviceIp, setDeviceIp, getBaseUrl } from '../common/device-ip.js';`,删除 38-60。注意:`dbc.js` 原 `getDeviceIp` 读 `el.toolIp`,迁移后需用 `getDeviceIp({ inputId: 'toolIp' })` 调用,或确认 `el.toolIp` 在调用时已缓存。 - [ ] **Step 3: can_monitor.js 迁移** 删除 31-45,import。`getBaseUrl` 调用点改为 `getBaseUrl({ inputId: 'monIp' })` 或 `{ inputId: 'toolIp' }`(按原文优先级)。 - [ ] **Step 4: 冒烟** `tools/can.html` 与 `tools/index.html` 各加载一次,确认连接的目标 IP 正确(默认 192.168.31.127),切换 IP 后 localStorage 持久化生效。 - [ ] **Step 5: 提交** ```bash git add tools/js/app.js tools/js/can/dbc.js tools/js/can_monitor.js git commit -m "refactor(js): 迁移设备 IP/BaseUrl 至 common/device-ip.js" ``` --- ### Task 16b: 消除 `bytesToHex`/`formatTime` 重复实现 **Files:** - Modify: `tools/js/can/dbc.js:79-86`(删除本地 `bytesToHex`,统一用 `common/utils.js` 的) - Modify: `tools/js/can/send.js:23-29`(删除本地 `formatTime`,改用参数化 `common/utils.js` 版) - [ ] **Step 1: 统一 bytesToHex** 先确认 `common/utils.js:51-60` 的 `bytesToHex` 输出小写,而 `can/dbc.js:79-86` 用 `padHex` 输出大写。需检查 `dbc.js` 的 `bytesToHex` 调用点(`can/send.js`、`can/signals.js`)对大小写的依赖。 - 若调用点只用于显示且显示要求大写:在 `common/utils.js` 给 `bytesToHex` 加 `{ upper: true }` 选项: ```js function bytesToHex(bytes, opts) { var hex = Array.prototype.map.call(new Uint8Array(bytes), function (b) { var h = b.toString(16).padStart(2, '0'); return opts && opts.upper ? h.toUpperCase() : h; }).join(''); return hex; } ``` 然后 `dbc.js` 调用 `bytesToHex(bytes, { upper: true })`。 - 删除 `dbc.js:79-86` 本地定义与 `padHex`(若 `padHex` 无其他调用点)。 - [ ] **Step 2: 统一 formatTime** 在 `common/utils.js:17-23` 的 `formatTime` 加 `opts.millis` 选项(默认 true): ```js function formatTime(date, opts) { var d = date instanceof Date ? date : new Date(date); var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; var base = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); if (opts && opts.millis === false) return base; return base + '.' + ('00' + d.getMilliseconds()).slice(-3); } ``` `can/send.js:23-29` 删除本地定义,调用改为 `formatTime(date, { millis: false })`。 - [ ] **Step 3: 冒烟** CAN 页发送一帧,历史记录时间显示 `HH:MM:SS`(无毫秒);hex 预览/字节条显示大写。 - [ ] **Step 4: 提交** ```bash git add tools/js/common/utils.js tools/js/can/dbc.js tools/js/can/send.js git commit -m "refactor(js): 统一 bytesToHex/formatTime 至 common/utils.js" ``` --- ### Task 17: 提取 `buildOtaBody` 消除 esp32/gd32 OTA body 重复组装 **Files:** - Create: `tools/js/common/ota-body.js` - Modify: `tools/js/ota/upload-esp32.js:219-242` - [ ] **Step 1: 创建 ota-body.js** ```js // tools/js/common/ota-body.js function buildOtaBody(arrayBuffer, headerObj) { var encoder = new TextEncoder(); var headerBytes = encoder.encode(JSON.stringify(headerObj)); var payload = new Uint8Array(arrayBuffer); var combined = new Uint8Array(headerBytes.length + payload.length); combined.set(headerBytes, 0); combined.set(payload, headerBytes.length); return combined.buffer; } export { buildOtaBody }; ``` - [ ] **Step 2: 替换 upload-esp32.js:219-242** ESP32 分支:`buildOtaBody(payload, { type: 'esp32', total_size: payload.byteLength })` GD32 分支:`buildOtaBody(payload, { total_size: payload.byteLength })` - [ ] **Step 3: 冒烟** WiFi OTA 上传 ESP32 与 GD32 固件各一次,服务端正常解析。 - [ ] **Step 4: 提交** ```bash git add tools/js/common/ota-body.js tools/js/ota/upload-esp32.js git commit -m "refactor(js): 提取 buildOtaBody 消除 esp32/gd32 body 重复" ``` --- ### Task 18: 统一 SHA verify panel UI(BLE 与 OTA 共用同一套 DOM id) **Files:** - Create: `tools/js/common/verify-panel.js` - Modify: `tools/js/ble/verify.js:3-26`(改为薄包装或 re-export) - Modify: `tools/js/ota/upload-esp32.js:200-207,486-489,529-540,557-564,448-452,470-475`(内联 DOM 写改用共享 helper) - [ ] **Step 1: 创建 verify-panel.js** ```js // tools/js/common/verify-panel.js function showVerifyInfo(frontSha, devSha, opts) { opts = opts || {}; var panel = document.getElementById('stageVerifyInfo'); if (panel) panel.hidden = false; var f = document.getElementById('sviFrontSha'); var d = document.getElementById('sviDeviceSha'); var r = document.getElementById('sviResult'); if (f) f.textContent = frontSha || ''; if (d) d.textContent = devSha || ''; if (r) { r.textContent = opts.text || ''; r.style.color = opts.ok ? '#2ecc71' : (opts.err ? '#e74c3c' : ''); } } function hideVerifyInfo() { var panel = document.getElementById('stageVerifyInfo'); if (panel) panel.hidden = true; } function resetVerifyInfo() { ['sviFrontSha', 'sviDeviceSha', 'sviResult'].forEach(function (id) { var el = document.getElementById(id); if (el) { el.textContent = ''; el.style.color = ''; } }); } export { showVerifyInfo, hideVerifyInfo, resetVerifyInfo }; ``` > ⚠️ 实现前需 Read `tools/js/ble/verify.js:3-26` 与 `ota/upload-esp32.js` 上述行段,确认精确 DOM id 与样式字段,本 step 为骨架。 - [ ] **Step 2: ble/verify.js 改为 re-export** `export { showVerifyInfo as showBleVerifyInfo, hideVerifyInfo as hideBleVerifyInfo } from '../common/verify-panel.js';`(若 `ble/verify.js` 有 BLE 专属逻辑则保留包装)。 - [ ] **Step 3: ota/upload-esp32.js 内联 DOM 写改用 helper** `:200-207` reset → `resetVerifyInfo()`;`:486-489` show → `showVerifyInfo(...)`;`:529-540` 比对成功 → `showVerifyInfo(front, dev, { text: '匹配', ok: true })`;`:557-564` 失败 → `showVerifyInfo(front, dev, { text: '不匹配', err: true })`。 - [ ] **Step 4: 冒烟** BLE OTA 与 WiFi OTA 各跑一次,验证 SHA 面板显示与改前一致。 - [ ] **Step 5: 提交** ```bash git add tools/js/common/verify-panel.js tools/js/ble/verify.js tools/js/ota/upload-esp32.js git commit -m "refactor(js): 统一 SHA verify panel UI 至 common/verify-panel.js" ``` --- ## 阶段 D:JS 过长函数拆分 ### Task 19: 拆分 `ble/gatt.js:handleNotify` (262 行) **Files:** - Modify: `tools/js/ble/gatt.js:182-444` - [ ] **Step 1: 识别子段** 按通知类型拆出三个本地函数: - `handleAckNotify(buf)` — ACK 解析 + state + progress + waiter 解析 - `handleFinishNotify(buf)` — FINISH 解析 + 阶段推进 + 结果等待 - `handleSha256Notify(buf)` — SHA256 解析 + verify panel + 比较 - [ ] **Step 2: 重写 handleNotify 为 dispatcher** ```js function handleNotify(event) { var buf = new Uint8Array(event.target.value.buffer); if (buf.length < 1) return; switch (buf[0]) { case BLE_OTA_NOTIFY_ACK: return handleAckNotify(buf); case BLE_OTA_NOTIFY_FINISH: return handleFinishNotify(buf); case BLE_OTA_NOTIFY_SHA256: return handleSha256Notify(buf); default: bleLog('未知通知 0x' + buf[0].toString(16)); } } ``` - [ ] **Step 3: 冒烟** BLE OTA 全流程,观察 ACK/FINISH/SHA256 三类通知处理正常。 - [ ] **Step 4: 提交** ```bash git add tools/js/ble/gatt.js git commit -m "refactor(ble): 拆分 handleNotify 为 ack/finish/sha256 三个 handler" ``` --- ### Task 20: 拆分 `ble/transfer.js:uploadFirmware` (367 行) **Files:** - Modify: `tools/js/ble/transfer.js:107-474` - [ ] **Step 1: 提取子函数** - `refreshFileBeforeUpload()` — 文件重读逻辑 (109-120) - `sendWindow(windowStart, endSeq, fileBytes, payloadSize, cfg)` — 单窗口发送 (219-302) - `retryWindow(resumeSeq, endSeq, ...)` — 重传 (304-345) — 与 sendWindow 共享 per-packet 体 - `buildEsp32Trigger(fileSha)` — ESP32 触发包 (362-396) - `buildGd32Trigger(fileSha, sessionId, cfg)` — GD32 触发包 (400-441) - [ ] **Step 2: 重写 uploadFirmware 为编排** 保留外层窗口循环、retry 决策、stage 推进、结果等待;per-packet 体提到 `sendPacket(seq, fileBytes, payloadSize, cfg)` 共享。 - [ ] **Step 3: 冒烟** BLE OTA 上传 ESP32 与 GD32 固件各一次,观察窗口推进/重传/触发/结果等待正常。 - [ ] **Step 4: 提交** ```bash git add tools/js/ble/transfer.js git commit -m "refactor(ble): 拆分 uploadFirmware 为窗口/触发子函数" ``` --- ### Task 21: 拆分 `ota/upload-esp32.js:startUpload` (299 行) **Files:** - Modify: `tools/js/ota/upload-esp32.js:56-355` - [ ] **Step 1: 提取子函数** - `probeOtaMutex(chip)` — 4 次重试的 mutex 探测 - `probeStaleSession(chip)` — 旧会话探测 - `buildUploadBody(chip, payload)` — 用 Task 17 的 `buildOtaBody` - `wireXhrHandlers(xhr, chip, payload)` — 5 个 XHR handler 绑定(progress/load/error/abort/timeout) - [ ] **Step 2: 重写 startUpload 为编排** - [ ] **Step 3: 冒烟** WiFi OTA 上传 ESP32 与 GD32 各一次。 - [ ] **Step 4: 提交** ```bash git add tools/js/ota/upload-esp32.js git commit -m "refactor(ota): 拆分 startUpload 为 mutex/body/xhr 子函数" ``` --- ### Task 22: 拆分 `common/polling.js:pollStatus` (122 行) 与 `ota/stages.js:applyGd32OtaSessionUi` (151 行) **Files:** - Modify: `tools/js/common/polling.js:182-303` - Modify: `tools/js/ota/stages.js:182-333` - [ ] **Step 1: pollStatus catch 拆分** 把 228-290 的 7 分支 if/else 拆出 `handlePollRecovery`/`handlePollReboot`/`handlePollSessionFailure`/`handlePollDeviceBusy`/`handlePollDisconnected`,`pollStatus` 的 catch 调用之。 - [ ] **Step 2: applyGd32OtaSessionUi 拆分** 按 `otaSessionState` 分支拆出 `applyGd32Failed`/`applyGd32Relaying`/`applyGd32Storing`/`applyGd32Done`。 - [ ] **Step 3: 冒烟** 模拟 OTA 失败/转发中/存储中/完成四态(可手动改 `window.otaSessionState`),观察 UI 切换正常;pollStatus 在断连/重启/失败下行为正常。 - [ ] **Step 4: 提交** ```bash git add tools/js/common/polling.js tools/js/ota/stages.js git commit -m "refactor(js): 拆分 pollStatus 与 applyGd32OtaSessionUi" ``` --- ## 阶段 E:死代码/一次性 helper 清理 ### Task 23: C++ 侧死代码与内联 **Files:** - Modify: `main/server/ble_ota.cpp:346-349`(内联 `BleOta::isConnected()` 或保留——评估后决定) - Modify: `main/server/ble_ota_host.cpp:228-236`(0xFB handler 内联队列 drain 改用 `drainOtaDataQueue()`) - Modify: `main/include/app_config.h:126`(确认 `BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9` 是否死代码) - [ ] **Step 1: 0xFB handler 改用 drainOtaDataQueue** `ble_ota_host.cpp:228-236` 的内联 drain 循环替换为 `drainOtaDataQueue();`。 - [ ] **Step 2: 检查 BLE_OTA_CTRL_VERIFY_SHA256** `grep -rn "0xF9\|BLE_OTA_CTRL_VERIFY_SHA256" main/ tools/`。若全无引用,删除 `app_config.h:126` 该行。 - [ ] **Step 3: 评估 BleOta::isConnected** 若调用点仅 2 处且均为简单条件判断,内联;否则保留。本 step 可降级为 no-op。 - [ ] **Step 4: 验证编译 + 冒烟** - [ ] **Step 5: 提交** ```bash git add main/server/ble_ota_host.cpp main/include/app_config.h main/server/ble_ota.cpp git commit -m "refactor(ble_ota): 清理 0xFB 内联 drain 与死常量" ``` --- ### Task 24: 消除 `polling.js` 与 `app.js` 的重复 IP-change 监听 **Files:** - Modify: `tools/js/common/polling.js:372-387` - Modify: `tools/js/app.js:236-249` - [ ] **Step 1: 确认重复** 读两边代码,确认同一 `#espIp` `change` 事件被绑定两次(一次 `app.js` 初始化时,一次 `polling.js` setTimeout 500ms 后)。 - [ ] **Step 2: 删除 polling.js 中的重复绑定** 保留 `app.js:236-249` 版本(更早绑定),删除 `polling.js:376-386` 的 IP change 绑定;保留 `polling.js:373-375` 的 upload button label 更新(若无重复)。 - [ ] **Step 3: 冒烟** `tools/index.html` 修改 IP 输入,确认只触发一次保存 + 重置失败计数 + 重启轮询。 - [ ] **Step 4: 提交** ```bash git add tools/js/common/polling.js git commit -m "refactor(js): 删除 polling.js 重复的 IP change 监听" ``` --- ### Task 25: JS 杂项死代码清理 **Files:** - Modify: `tools/js/can/messages.js:27`(`el.importDbcBtn.hidden = false` 无操作) - Modify: `tools/js/can_monitor.js:22-23`(重复的 toast ID 缓存,若与 `dbc.js` 的 `el` 共享) - Modify: `tools/js/ble/state.js:99-105`(`hexToBytes` 单调用点评估内联) - [ ] **Step 1: 删除 messages.js:27 无操作行** - [ ] **Step 2: can_monitor.js toast ID 评估** 确认 `can_monitor.js` 是否与 `can/dbc.js` 共享 `el`。若共享,删除 `:22-23` 重复 toast ID。若独立保留。 - [ ] **Step 3: 评估 hexToBytes 内联** `ble/state.js:99-105` 仅 `ble/transfer.js:408` 调用。若 `cfg.ctrlHex` 改为预编码常量字节数组(在 `ble/constants.js` 的 TARGETS 里),可删除 `hexToBytes`。本 step 可降级为 no-op。 - [ ] **Step 4: 冒烟** `tools/can.html` 加载无报错;CAN 监控 toast 正常。 - [ ] **Step 5: 提交** ```bash git add tools/js/can/messages.js tools/js/can_monitor.js tools/js/ble/state.js tools/js/ble/constants.js tools/js/ble/transfer.js git commit -m "refactor(js): 清理无操作行与重复 toast 缓存" ``` --- ## 自检清单(Self-Review) ### 1. Spec 覆盖 用户请求:「扫描每个函数,评估是否可以精简,主要关注复用关系」。覆盖维度: - ✅ 复用:hex_utils、progress、stage-ui、sha、fetch-json、device-ip、gatt-helpers、verify-panel、buildOtaBody、triggerGd32RelayCommon、shouldSendThrottledAck、bytesToHex/formatTime 统一 - ✅ 精简:拆分 ble_ota_ctrl_access、ota_upload_handler、handleNotify、uploadFirmware、startUpload、pollStatus、applyGd32OtaSessionUi - ✅ 死代码:F9 常量、重复 IP 监听、无操作 hidden、重复 toast 缓存 ### 2. Placeholder 扫描 - Task 4 Step 2、Task 7 Step 1、Task 11 Step 1、Task 18 Step 1 含「以原文为准」骨架——已显式标注 ⚠️ 并要求先 Read 原文。这是必要的谨慎(避免凭记忆改写节流条件/DOM id),非占位。 - Task 23 Step 3、Task 25 Step 3 标注「可降级为 no-op」——这是显式的可选清理,非占位。 ### 3. 类型/命名一致性 - `setProgress(percent, text)` 跨 BLE/OTA 一致。 - `getStageStatusBar`/`getStageStatusText` 在 ble/stages.js 与 common/stage-ui.js 一致。 - `compareShaHex(a, b)` 在 common/sha.js 与调用点一致。 - `buildOtaBody(arrayBuffer, headerObj)` 在 common/ota-body.js 与调用点一致。 - `triggerGd32RelayCommon` 在 C++ 侧命名与 `triggerGd32Relay` 区分清晰。 --- ## Execution Handoff **Plan complete and saved to `.zcode/plans/2026-07-18-function-simplify-reuse.md`. Two execution options:** **1. Subagent-Driven (recommended)** - 每个 task 派一个新 subagent,task 间做两阶段 review,迭代快。 **2. Inline Execution** - 在当前 session 用 executing-plans 批量执行 + checkpoint review。 **Which approach?**