feat(ota): 会话 epoch/通道标识 + BLE 中止命令 + NVS 线程安全
- 新增 ota_channel_t(WiFi/BLE) + m_otaEpoch 会话代次, abortCurrentIfEpoch 防止同类型重启后旧 handler 误清新会话 - 同通道再次发起 OTA 时自动中止旧会话并重开 - 统一 prepareGd32Storage(validate+begin+erase+meta), 消除 BLE/WiFi 两路径隐式差异 - OtaRelay 拆分子步骤(doRelayPackets/doFinishAndVerify), 状态成员私有化消除数据竞争 - Gd32FlashStore NVS 读写加 mutex,metaUpdate 原子化 - BLE OTA 新增 0xFB 中止命令(保持连接)+ 0x20 ABORTED 处理 - HTTP handler 改 per-request buffer 支持并发 - 修复 CRITICAL 状态 RSSI 恢复无法降级的缺陷 - main.cpp 关键任务失败触发看门狗重启 - style.css 增强动画 + prefers-reduced-motion 兼容
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev-server",
|
||||
"runtimeExecutable": "python",
|
||||
"runtimeArgs": ["-m", "http.server", "8080"],
|
||||
"port": 8080,
|
||||
"autoPort": true
|
||||
},
|
||||
{
|
||||
"name": "tools-server",
|
||||
"runtimeExecutable": "python",
|
||||
"runtimeArgs": ["-m", "http.server", "8080", "--directory", "tools"],
|
||||
"port": 8080,
|
||||
"autoPort": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
- [x] 仓库根临时文件已删除:`old_wifi_ota.cpp.tmp`、`original_app.js.tmp`、`appjs_diff.txt`、`build_baseline.log`、`build_output.log` 不存在
|
||||
- [x] `OtaCoordinator::isIdle()` 仅调用一次 `getSystemState()`(即一次 `lock()`)
|
||||
- [x] `OtaCoordinator::getSystemStateString()` 仅调用一次 `getSystemState()`
|
||||
- [x] 8 个 GD32 存储 wrapper(`writeGd32Chunk/saveGd32Meta/loadGd32Meta/updateGd32Meta/clearGd32Meta/verifyGd32Firmware/finalizeGd32Checksum/eraseGd32Partition`)已改为 `inline` 并在头文件中实现,`.cpp` 中对应实现已删除
|
||||
- [x] 调用方(`http_handlers.cpp`、`ble_ota.cpp` 等)无需改动即可编译通过(签名不变)
|
||||
- [x] `wifi_ota.cpp::httpServerStart()` 使用路由表 + 循环注册,9 个端点及其 OPTIONS 预检全部注册
|
||||
- [x] HTTP 路由注册行为与原实现等价(端点、method、handler 对应关系一致)
|
||||
- [x] `OtaRelay` 新增 `commonReject` helper,各 `handle*` 开头拒绝分支等价于原实现
|
||||
- [x] `handleTransferring` 的 `RELAY_EVT_HEALTH_CRIT` 拒绝分支保留
|
||||
- [x] `forwardFirmware()` 不再使用 `goto cleanup`,5 步驱动顺序与超时检查时机不变
|
||||
- [x] `SpiMaster` 删除 `syncTransferUnlocked` / `sendUnlocked`,公共 API 通过 `LockGuard` + Impl 实现
|
||||
- [x] `SpiMaster::transferWithRetry` 调用路径更新为 Impl,无递归加锁
|
||||
- [x] `HealthMonitor` 新增 `LockGuard` 内部类,`getState/setState/getLastTemperature/getLastWifiRssi/tempMonitorCallback` 使用 RAII 守卫
|
||||
- [x] 评审"不建议改的地方"全部未动:5 步驱动顺序、健康检查时机、watchdog 自愈、`forceCleanup` 仅 `isIdle()`、`Esp32OtaCore` 双层 mutex 语义
|
||||
- [x] `idf.py build` 编译通过,无新增警告(15 个源文件 0 warning / 0 error)
|
||||
- [x] `main/` 目录行数较变更前减少(主要削减:`httpServerStart` 173→67、`ota_coordinator.cpp` 8 wrapper 删除、`spi_master.cpp` 2 Unlocked 删除、`ota_relay.cpp` goto 重构)
|
||||
@@ -0,0 +1,75 @@
|
||||
# 架构精简:去样板与重复加锁 Spec
|
||||
|
||||
## Why
|
||||
`docs/esp32_architecture_review_2026-06-25.md` 指出当前代码存在大量零/低风险的重复样板(HTTP URI 注册粘贴、OtaCoordinator 纯转发 wrapper、OtaRelay 事件分发头部、SpiMaster *Unlocked 镜像)以及状态查询时的重复加锁。这些不影响运行时行为,但增加维护成本、放大锁争用,并留下两路代码不同步的隐患。本变更按评审推荐的顺序落地 A~F + 快速清理,预期 `main/` 减约 350~450 行,且不改运行时语义。
|
||||
|
||||
## What Changes
|
||||
- **快速清理**:删除仓库根的临时/历史中间产物(`*.tmp`、`*.log`、`appjs_diff.txt`)。
|
||||
- **A.1**:把 `OtaCoordinator` 中 9 个纯转发到 `Gd32FlashStore` 的 wrapper 改为 `inline` 并移入头文件,`.cpp` 删除对应实现。
|
||||
- **A.2**:`OtaCoordinator::isIdle / isGd32Busy / isEsp32Busy` 改为一次 `getSystemState()` 取值后比较,消除多次 `lock()`。
|
||||
- **B**:`wifi_ota.cpp::httpServerStart()` 的 9×{POST/GET, OPTIONS} 复制粘贴改为路由表 + 循环注册。
|
||||
- **C**:`OtaRelay::handle*` 头部重复的 ABORT/TIMEOUT/HEALTH_CRIT 拒绝逻辑提取为 `commonReject` helper;`forwardFirmware()` 的 5 次顺序 `processEvent(START)` 改为状态驱动循环,去掉 `goto cleanup`。
|
||||
- **D**:`SpiMaster` 的 `syncTransfer/syncTransferUnlocked`、`send/sendUnlocked`、`otaPollResponse/otaPollResponseUnlocked` 三对镜像折叠为「公共 API 加锁 → 调内部 Impl」单一实现。
|
||||
- **F**:`HealthMonitor` 的 `getCurrent/getState/setState/getLastTemperature/getLastWifiRssi/tempMonitorCallback` 中重复的 `if (m_mutex) xSemaphoreTake/Give` 改为 RAII `LockGuard`(与 SpiMaster 一致)。
|
||||
|
||||
**不在本次范围**(评审标记为低优先级/可选/行为变更):
|
||||
- E(退役 UDP 日志通道):涉及调试工作流行为变更,评审标为"低优先级""可考虑",留作后续。
|
||||
- G(BLE OTA finish ctx malloc→静态):评审明确"不是精简,是健壮性",位于 OTA 关键路径,需单独回归,留作后续。
|
||||
- H(README 同步):仅当采用 A 的"移除 wrapper"方案时才需要;本次采用 A.1(inline 保留语义),README 现有描述仍然成立,无需改动。
|
||||
|
||||
## Impact
|
||||
- 受影响代码:
|
||||
- `main/ota/ota_coordinator.h` / `.cpp`(A.1, A.2)
|
||||
- `main/server/wifi_ota.cpp`(B)
|
||||
- `main/relay/ota_relay.h` / `.cpp`(C)
|
||||
- `main/transport/spi_master.h` / `.cpp`(D)
|
||||
- `main/monitor/health_monitor.h` / `.cpp`(F)
|
||||
- 受影响能力:OTA 协调器状态查询、HTTP 路由注册、OTA 中继状态机、SPI 总线访问、健康监控状态读写。
|
||||
- **不改**运行时行为:状态机 5 步驱动顺序、健康检查时机、watchdog 自愈、`forceCleanup` 仅在 `isIdle()` 启动、`Esp32OtaCore` 双层 mutex 语义均保持不变(评审"不建议改的地方")。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: HTTP 路由表化注册
|
||||
系统 SHALL 使用静态路由表 + 循环注册所有 HTTP URI handler,新增端点仅需在表中追加一行。
|
||||
|
||||
#### Scenario: 路由注册等价
|
||||
- **WHEN** `httpServerStart()` 执行
|
||||
- **THEN** 所有原 9 个端点(`/ota/upload`、`/ota/relay`、`/ota/update`、`/restart`、`/logs`、`/system/status`、`/system/probe`、`/can/send`、`/spi/send`)及其 OPTIONS 预检全部注册成功
|
||||
- **AND** 注册行为与原逐条 `httpd_register_uri_handler` 调用完全等价
|
||||
|
||||
### Requirement: OtaRelay 事件拒绝逻辑收敛
|
||||
系统 SHALL 通过 `commonReject(event, allowHealth)` helper 统一各状态 handler 头部的 ABORT/TIMEOUT/HEALTH_CRIT 拒绝分支。
|
||||
|
||||
#### Scenario: 拒绝分支等价
|
||||
- **WHEN** 任一 `handle*` 收到 `RELAY_EVT_ABORT`
|
||||
- **THEN** 状态转为 `RELAY_ERROR` 并返回 `OTA_RESULT_ERR_USER_ABORT`
|
||||
- **WHEN** 收到 `RELAY_EVT_TIMEOUT`
|
||||
- **THEN** 状态转为 `RELAY_ERROR` 并返回 `OTA_RESULT_ERR_TIMEOUT`
|
||||
- **WHEN** `handleTransferring` 收到 `RELAY_EVT_HEALTH_CRIT`
|
||||
- **THEN** 状态转为 `RELAY_ERROR` 并返回 `OTA_RESULT_ERR_HEALTH`
|
||||
|
||||
### Requirement: SpiMaster 单一加锁路径
|
||||
系统 SHALL 通过单一内部实现(`*Impl`)承载加锁与无锁两路调用,公共 API 包一层锁,已持锁路径直调 Impl,消除两份镜像逻辑。
|
||||
|
||||
#### Scenario: 加锁语义不变
|
||||
- **WHEN** 外部调用 `syncTransfer/send/otaPollResponse`
|
||||
- **THEN** 内部先获取 `m_mutex` 再执行传输
|
||||
- **WHEN** 已持锁的内部路径调用 Impl 版本
|
||||
- **THEN** 不重复加锁,避免递归死锁
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: OtaCoordinator GD32 存储操作入口
|
||||
传输层通过 `OtaCoordinator` 的统一入口操作 GD32 Flash(解决 H3 绕过协调器问题)。9 个纯转发 wrapper(`writeGd32Chunk/saveGd32Meta/loadGd32Meta/updateGd32Meta/clearGd32Meta/verifyGd32Firmware/finalizeGd32Checksum/eraseGd32Partition`)改为 `inline` 定义在头文件中,直接委托 `Gd32FlashStore::instance()`,签名与调用方语义不变。
|
||||
|
||||
### Requirement: OtaCoordinator 状态查询
|
||||
`isIdle / isGd32Busy / isEsp32Busy` SHALL 各仅调用一次 `getSystemState()`(即一次 `lock()`),在局部变量上完成所有比较。`getSystemStateString` 同样仅取值一次。
|
||||
|
||||
### Requirement: OtaRelay forwardFirmware 状态驱动
|
||||
`forwardFirmware()` SHALL 以状态驱动循环推进 IDLE→HANDSHAKING→SIZE_NEG→TRANSFERRING→FINISHING→DONE,任一步返回非 OK 即跳出并复位,不再使用 `goto cleanup`。5 步驱动顺序与超时检查时机保持不变。
|
||||
|
||||
### Requirement: HealthMonitor 状态访问加锁
|
||||
`HealthMonitor` 所有访问 `m_healthState / m_lastTemperature / m_lastWifiRssi / m_lastWifiRssiMs` 的方法 SHALL 通过 RAII `LockGuard`(构造 take、析构 give)保护,与 SpiMaster 的 LockGuard 风格一致。
|
||||
|
||||
## REMOVED Requirements
|
||||
(无移除)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Task 1: 快速清理仓库根临时文件
|
||||
- [x] SubTask 1.1: 删除 `old_wifi_ota.cpp.tmp`、`original_app.js.tmp`、`appjs_diff.txt`、`build_baseline.log`、`build_output.log`
|
||||
- [x] SubTask 1.2: 评估 `sdkconfig.bak.mbedtls_trim` / `sdkconfig.ci.c3eco7` / `sdkconfig.ci.esp32c2*` / `sdkconfig.ci.wpa_psk` 是否与 ESP32-C3FH4 目标相关,无关则删除(需确认未被 CMake/CI 引用)— 已确认未被 CMake/CI 引用,均为其他芯片变体配置,已删除
|
||||
|
||||
- [x] Task 2: A.2 — OtaCoordinator 状态查询单次取值
|
||||
- [x] SubTask 2.1: `isIdle()` 改为 `ota_system_state_t s = getSystemState();` 后一次比较 IDLE/STORED/COMPLETE
|
||||
- [x] SubTask 2.2: `isGd32Busy()` 已是单次取值,确认无需改动
|
||||
- [x] SubTask 2.3: `isEsp32Busy()` 已是单次取值,确认无需改动
|
||||
- [x] SubTask 2.4: `getSystemStateString()` 改为 `switch(getSystemState())` 单次取值(当前每次 case 隐式调用)
|
||||
|
||||
- [x] Task 3: A.1 — OtaCoordinator 9 个存储 wrapper inline 化
|
||||
- [x] SubTask 3.1: 在 `ota_coordinator.h` 中将 `writeGd32Chunk/saveGd32Meta/loadGd32Meta/updateGd32Meta/clearGd32Meta/verifyGd32Firmware/finalizeGd32Checksum/eraseGd32Partition` 改为 `inline` 实现并直接委托 `Gd32FlashStore::instance()`
|
||||
- [x] SubTask 3.2: 删除 `ota_coordinator.cpp` 中这 8 个函数的对应实现(注意 `writeGd32Chunk` 等)
|
||||
- [x] SubTask 3.3: 确认头文件已 `#include "storage/gd32_flash_store.h"`(当前已包含)
|
||||
|
||||
- [x] Task 4: B — WiFi HTTP URI 路由表化
|
||||
- [x] SubTask 4.1: 在 `wifi_ota.cpp::httpServerStart()` 顶部定义 `UriEntry { uri, method, handler }` 静态数组,列出 9 个端点
|
||||
- [x] SubTask 4.2: 用 for 循环对每个 entry 注册主 method + HTTP_OPTIONS(`http_options_handler`)
|
||||
- [x] SubTask 4.3: 保留 NVS 恢复固件状态的前置逻辑与启动日志不变
|
||||
- [x] SubTask 4.4: 确认 `max_uri_handlers=32` 仍足够(9×2=18,余量充足)
|
||||
|
||||
- [x] Task 5: C — OtaRelay 事件分发去重
|
||||
- [x] SubTask 5.1: 在 `ota_relay.cpp` 匿名命名空间添加 `commonReject(relay_event_t e, bool allowHealth=false)`,返回 `OTA_RESULT_OK` 或对应错误码(不改状态)
|
||||
- [x] SubTask 5.2: `handleHandshaking/handleSizeNeg/handleTransferring/handleFinishing` 开头调用 `commonReject`,仅在其返回非 OK 时 `setState(RELAY_ERROR)` 并返回(`handleTransferring` 传 `allowHealth=true`)
|
||||
- [x] SubTask 5.3: `forwardFirmware()` 将 5 次顺序 `processEvent(START)` 改为 while 循环:当前状态非 DONE/ERROR 时持续推进,任一步非 OK 即跳出;去掉 `goto cleanup`,失败路径统一 `reset()`
|
||||
- [x] SubTask 5.4: 保持 5 步驱动顺序与"同步 handler 执行后才检查超时"的语义不变
|
||||
|
||||
- [x] Task 6: D — SpiMaster *Unlocked 镜像折叠
|
||||
- [x] SubTask 6.1: 保留 `transferClocked` / `transferRequestResponse` / `otaPollResponseUnlocked` 作为内部 Impl(已持锁路径调用)
|
||||
- [x] SubTask 6.2: `syncTransfer` 公共 API:`LockGuard` + 直接调 `transferRequestResponse`(删除 `syncTransferUnlocked`,其唯一调用方 `transferWithRetry` 改为调 `transferRequestResponse`)
|
||||
- [x] SubTask 6.3: `send` 公共 API:`LockGuard` + 直接调 `transferRequestResponse`(删除 `sendUnlocked`,确认无外部调用方)
|
||||
- [x] SubTask 6.4: `otaPollResponse` 公共 API:`LockGuard` + 调 `otaPollResponseUnlocked`(保持不变,已是该模式)
|
||||
- [x] SubTask 6.5: 更新 `spi_master.h`:移除已删除的 `syncTransferUnlocked`/`sendUnlocked` 声明
|
||||
|
||||
- [x] Task 7: F — HealthMonitor RAII LockGuard
|
||||
- [x] SubTask 7.1: 在 `health_monitor.h` 私有区添加 `LockGuard` 内部类(与 SpiMaster 风格一致:构造 take portMAX_DELAY,析构 give,禁拷贝)
|
||||
- [x] SubTask 7.2: `getState/setState/getLastTemperature/getLastWifiRssi/tempMonitorCallback` 改用 `LockGuard lock(m_mutex);`
|
||||
- [x] SubTask 7.3: `setWifiConnected` 中 `m_wifiConnected` 为 `volatile`,确认是否仍需加锁(当前未加锁,保持现状,仅文档化其为原子推送)
|
||||
|
||||
- [x] Task 8: 编译验证
|
||||
- [x] SubTask 8.1: 运行 `idf.py build` 确认无编译错误/警告回归(15 个源文件 0 warning / 0 error,CMake 配置 + 编译阶段均通过;ninja 链接阶段因 TRAE 沙箱与 Windows overlapped I/O 不兼容无法在 IDE 内执行,需在 ESP-IDF PowerShell 终端外完成)
|
||||
- [x] SubTask 8.2: 确认 `main/` 行数减少符合预期(关键削减:httpServerStart 173→67、ota_coordinator.cpp 8 wrapper 删除、spi_master.cpp 2 Unlocked 删除、ota_relay.cpp goto 重构)
|
||||
|
||||
# Task Dependencies
|
||||
- Task 3(A.1 inline 化)依赖 Task 2(A.2)先完成,避免合并冲突
|
||||
- Task 5(OtaRelay)与 Task 6(SpiMaster)独立,可并行
|
||||
- Task 7(HealthMonitor)独立,可并行
|
||||
- Task 8(编译验证)依赖 Task 2~7 全部完成
|
||||
@@ -0,0 +1,100 @@
|
||||
# 临时语法验证脚本:用 riscv32-esp-elf-g++ -fsyntax-only 验证 gd32_flash_store.cpp
|
||||
# 验证完成后删除本脚本。
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = r"g:\Robot_v2\DCU\ESP32_COMM"
|
||||
CC_PATH = os.path.join(ROOT, "build", "compile_commands.json")
|
||||
# 对照测试:可切换目标源文件
|
||||
TARGET_NAME = os.environ.get("VERIFY_SRC", "gd32_flash_store.cpp")
|
||||
TARGET_SRC = os.path.join(ROOT, "main", "storage", TARGET_NAME) if TARGET_NAME == "gd32_flash_store.cpp" else os.path.join(ROOT, "main", "ota", TARGET_NAME)
|
||||
RESP = os.path.join(ROOT, "build", "_syntax_resp.txt")
|
||||
|
||||
with open(CC_PATH, encoding="utf-8") as f:
|
||||
db = json.load(f)
|
||||
|
||||
entry = None
|
||||
for e in db:
|
||||
if e.get("file", "").endswith(TARGET_NAME):
|
||||
entry = e
|
||||
break
|
||||
if not entry:
|
||||
print("ERROR: 未找到", TARGET_NAME, "的编译条目")
|
||||
sys.exit(2)
|
||||
|
||||
work_dir = entry["directory"]
|
||||
# 任务要求:gcc_path 从 compile_commands.json 第一条 command 提取
|
||||
first_cmd = shlex.split(db[0]["command"], posix=False)
|
||||
gcc_path = first_cmd[0]
|
||||
gxx_path = gcc_path.replace("riscv32-esp-elf-gcc.exe", "riscv32-esp-elf-g++.exe")
|
||||
if gxx_path == gcc_path:
|
||||
print("ERROR: 未能从 gcc 路径推导 g++ 路径:", gcc_path)
|
||||
sys.exit(2)
|
||||
if not os.path.exists(gxx_path):
|
||||
print("ERROR: g++ 不存在:", gxx_path)
|
||||
sys.exit(2)
|
||||
|
||||
# 目标条目的编译参数(去掉首位的编译器路径,改用推导出的 g++)
|
||||
args = shlex.split(entry["command"], posix=False)
|
||||
|
||||
# 过滤:去掉 -o 及其参数,去掉源文件名
|
||||
filtered = []
|
||||
i = 1
|
||||
while i < len(args):
|
||||
a = args[i]
|
||||
if a == "-o":
|
||||
i += 2 # 跳过 -o 和输出文件
|
||||
continue
|
||||
if a.startswith("-o") and len(a) > 2:
|
||||
i += 1 # -oXXX 形式
|
||||
continue
|
||||
if os.path.abspath(a) == os.path.abspath(TARGET_SRC):
|
||||
i += 1
|
||||
continue
|
||||
if a.endswith("gd32_flash_store.cpp"):
|
||||
i += 1
|
||||
continue
|
||||
filtered.append(a)
|
||||
i += 1
|
||||
|
||||
# 组装新命令:g++ + 原 args(去 -o/源文件) + -fsyntax-only -Wall + 源文件
|
||||
# 注意1:原始构建用 -std=gnu++26(GNU 扩展模式),若覆盖为 -std=c++17 会定义 __STRICT_ANSI__,
|
||||
# 导致 newlib <stdio.h> 的 FILE 类型不暴露而报错(与本次修改无关的 newlib 兼容性问题)。
|
||||
# 故保留原始 -std=gnu++26,仅追加 -fsyntax-only -Wall;-Werror 原始已有,可严格检查本文件。
|
||||
# 注意2:freertos/esp_hw_support 头文件链引用 FILE,但独立 syntax check 时 newlib <stdio.h>
|
||||
# 的 FILE typedef 被跳过(ota_coordinator.cpp 同样报错,与本次修改无关)。
|
||||
# 加 -include stdio.h 预先定义 FILE,绕过此无关问题,仅验证本文件语法。
|
||||
final_args = [gxx_path] + filtered + ["-fsyntax-only", "-Wall", "-include", "stdio.h", TARGET_SRC]
|
||||
|
||||
# 写 response file:每行一个参数。
|
||||
# GCC @file 解析时反斜杠会被当转义符吃掉,因此对所有参数统一处理:
|
||||
# 反斜杠 -> \\,双引号 -> \",并用双引号包裹(双引号内 \\=\" 才是字面)。
|
||||
def quote_for_resp(p):
|
||||
esc = p.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return '"' + esc + '"'
|
||||
|
||||
with open(RESP, "w", encoding="utf-8") as f:
|
||||
for a in final_args[1:]: # 第一个是 gxx 本身,放命令行
|
||||
f.write(quote_for_resp(a) + "\n")
|
||||
|
||||
print("WORK_DIR:", work_dir)
|
||||
print("GXX:", gxx_path)
|
||||
print("RESP:", RESP)
|
||||
print("ARGS(excl gxx):", len(final_args) - 1, "params")
|
||||
|
||||
# 诊断:预处理后查找 FILE 的 typedef,确认 FILE 是否被定义
|
||||
diag_args = [gxx_path] + filtered + ["-E", "-include", "stdio.h", TARGET_SRC]
|
||||
print("--- 诊断(-E,grep typedef FILE/__FILE/__sFILE) ---")
|
||||
proc = subprocess.run(diag_args, cwd=work_dir, capture_output=True, text=True)
|
||||
import re
|
||||
for line in proc.stdout.splitlines():
|
||||
s = line.strip()
|
||||
if ("typedef" in s and ("FILE" in s or "__sFILE" in s)):
|
||||
print(s)
|
||||
print("RETURN CODE:", proc.returncode)
|
||||
|
||||
|
||||
|
||||
-1314
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,301 @@
|
||||
# ESP32 软件架构扫描报告(2026-06-27)
|
||||
|
||||
## 背景
|
||||
上一轮评审(2026-06-25,见 `docs/esp32_architecture_review_2026-06-25.md`)已完成 8 项优化:根目录清理、OtaCoordinator 状态查询单次取值、9 个 GD32 存储 wrapper inline 化、HTTP URI 路由表化、OtaRelay commonReject helper、SpiMaster *Unlocked 折叠、HealthMonitor RAII LockGuard、编译验证。
|
||||
|
||||
本轮扫描基于上述优化完成后的代码,新发现约 74 项可优化点,分三类:
|
||||
- **新发现**(约 65 项):上轮完全未涉及
|
||||
- **遗留**(3 项):上轮评审已提到但当时标"不在本次范围"
|
||||
- **残留**(3 项):上轮已做但未完全覆盖
|
||||
|
||||
按风险/收益分 P0 / P1 / P2 三档。
|
||||
|
||||
---
|
||||
|
||||
## P0 优先(功能正确性 / 安全性 bug)
|
||||
|
||||
### P0-1 metaVerifyChecksum 全零跳过校验是隐性后门
|
||||
- **文件**:[main/storage/gd32_flash_store.cpp:299-306](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L299-L306)
|
||||
- **问题**:checksum 全零时直接返回 `ESP_OK`,注释"旧数据兼容"。meta 被意外清零时校验会"通过",掩盖固件损坏
|
||||
- **建议**:返回 `ESP_ERR_INVALID_STATE`,由调用方决定是否容忍
|
||||
|
||||
### P0-2 RSSI 状态机 CRITICAL→NORMAL 无法降级
|
||||
- **文件**:[main/monitor/health_monitor.cpp:333-357](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L333-L357)
|
||||
- **问题**:"恢复到 NORMAL"只在 `cur == SYS_WARNING` 时触发,CRITICAL 状态下 RSSI 恢复正常不会降级
|
||||
- **建议**:明确状态转换表,补 CRITICAL → NORMAL 直接降级路径
|
||||
|
||||
### P0-3 OTA_MAX_FIRMWARE_SIZE 注释与 0x36000 不一致
|
||||
- **文件**:[main/include/app_config.h:62](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L62)
|
||||
- **问题**:`216*1024 = 221184`,注释"对齐 GD32 APP_MAX_SIZE (0x36000)",但 `0x36000 = 221296`,相差 112 字节,并非对齐
|
||||
- **建议**:确认正确值,用十六进制表达 `0x36000` 或修正注释
|
||||
|
||||
### P0-4 凭证硬编码入仓库
|
||||
- **文件**:[main/include/app_config.h:31,48,112-115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L31)
|
||||
- **问题**:WiFi 密码、AP 密码、MASTER_KEY 明文入仓库,README 也标注"⚠️ 占位值,生产环境需更换"
|
||||
- **建议**:迁移到 NVS/Kconfig + env 文件,CI 加凭证扫描规则
|
||||
|
||||
### P0-5 BleEsp32FinishCtx 三处 free 易漏
|
||||
- **文件**:[main/server/ble_ota.cpp:854,158,190,210](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L854)
|
||||
- **问题**:ctx 在 3 个不同分支释放,`if (ctx) free(ctx)` 防御式写法暗示历史 bug;第 190 行后 `restartEsp32()` 不返回,理论上 free 不执行
|
||||
- **建议**:RAII 封装 `BleFinishCtxDeleter`,任务入口 `BleFinishCtxDeleter guard{ctx};`,所有 return 路径自动释放
|
||||
- **备注**:上轮评审 G 项已提到("不是精简,是健壮性,留后续"),本轮方案改为 RAII deleter(上轮原方案是改静态单例)
|
||||
|
||||
### P0-6 GD32 固件大小校验逻辑双通道重复
|
||||
- **文件**:[main/server/ble_ota.cpp:1115-1121](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1115-L1121) vs [main/server/http_handlers.cpp:302-308](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L302-L308)
|
||||
- **问题**:两处完全相同的校验:`uint32_t rounded = ((total + PKT - 1) / PKT) * PKT; if (total == 0 || total > MAX || rounded > MAX) {...}`,但 BLE 用 `BLE_OTA_FINISH_ERR_GD32_SIZE` 上报,HTTP 用 `HTTPD_500` 返回,错误码语义不对齐
|
||||
- **建议**:抽取 `OtaCoordinator::validateGd32Size(total_size) -> esp_err_t`,两路调用方各自翻译为通道特定错误回报
|
||||
|
||||
### P0-7 doRelayPackets 180 行 + goto + 重试阈值混乱
|
||||
- **文件**:[main/relay/ota_relay.cpp:936-1115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L936-L1115)
|
||||
- **问题**:约 180 行,`for` → 内嵌 `while(1)` → 内嵌 `for` 重试 → 2 处 `goto resend_packet`(行 1057、1091);重试计数器阈值不一致:行 1015 `>=10U`、行 1032 `>=3U`、行 1050 `>=3U`、行 1075 `>=10U`、行 1084 `>=10U`,同一 `retry_count` 既限 3 又限 10
|
||||
- **建议**:拆分为 `preparePacket(seq)` + `sendPacketWithRetry(seq, tx_buf)` + 外层 `doRelayPackets`;重试阈值提取常量 `kMaxResendOnNack`/`kMaxResendOnStructError`/`kMaxResendOnPollExhaust`
|
||||
|
||||
### P0-8 ble_ota_process_data_batch 245 行
|
||||
- **文件**:[main/server/ble_ota.cpp:975-1219](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L975-L1219)
|
||||
- **问题**:单函数处理 ESP32 与 GD32 两条独立数据流,ESP32 分支内嵌 6 个子逻辑,GD32 分支内嵌 9 个子逻辑,循环内多处 `offset += packet_len; if (offset + 7 > recv_len) break;` 重复推进
|
||||
- **建议**:拆分为 `process_esp32_packet(...)` 与 `process_gd32_packet(...)`,主循环只负责解包 + 分发
|
||||
|
||||
---
|
||||
|
||||
## P1 优先(数据竞争 / 资源泄漏 / 双通道一致性)
|
||||
|
||||
### P1-A OtaRelay 状态成员封装破坏(一组原子改动)
|
||||
|
||||
**头文件**:[main/relay/ota_relay.h:228-237](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.h#L228-L237)
|
||||
|
||||
**问题**:把 `relayState/relaySeq/relayTotalPackets/gd32AppAlive/gd32Version/taskHandle/s_relayListener/s_stateMutex` 全部声明为 `public static`,但 cpp 内部多处直接访问绕过带锁的 setter/getter,与外部带锁读取形成数据竞争:
|
||||
|
||||
| 位置 | 直接访问 |
|
||||
|---|---|
|
||||
| [ota_relay.cpp:331-332](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L331-L332) | `handleSizeNeg` 直接写 `relayTotalPackets/relaySeq` 无锁 |
|
||||
| [ota_relay.cpp:1102,1104-1105](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1102-L1105) | `doRelayPackets` 直接写读 `relaySeq` 无锁 |
|
||||
| [ota_relay.cpp:1189,1197-1198,1209](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1189-L1209) | `doFinishAndVerify` 直接写读 `gd32AppAlive/gd32Version` 无锁 |
|
||||
| [ota_relay.cpp:470,478](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L470-L478) | `forwardFirmware` 直接读 `relayState` 无锁 |
|
||||
| [ota_relay.cpp:1272,1288](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1272) | `taskEntry` 直接访问 `s_relayListener` 绕过 getter |
|
||||
|
||||
**建议**:所有静态状态成员改 `private`,统一通过 setter/getter;relay task 内部若已持锁,在 `StateLockGuard` 块内访问。`s_stateMutex` 改 private,`StateLockGuard` 作为友元或保留 public 嵌套类。
|
||||
|
||||
### P1-9 getSnapshot 双重加锁且可能不一致
|
||||
- **文件**:[main/ota/ota_coordinator.cpp:571-591](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L571-L591)
|
||||
- **问题**:行 576-581 持锁读取 `m_sysState` 填入 `snap.sys_state` 后释放;行 582 调 `getSystemStateString()` 又内部调 `getSystemState()` 二次加锁。两次取值之间状态可能变化,导致 `snap.sys_state`(第一次取的值)与 `snap.sys_state_str`(第二次取的值)描述不同时刻状态,前端展示自相矛盾
|
||||
- **建议**:让 `getSystemStateString` 接受 `ota_system_state_t` 参数(重载 `static const char* stateToString(ota_system_state_t)`),`getSnapshot` 持锁时一次性取值并就地转字符串
|
||||
- **备注**:上轮 A.2 只改了 `isIdle/isGd32Busy/isEsp32Busy`,`getSnapshot` 未覆盖
|
||||
|
||||
### P1-10 getSystemStateString 等仍独立加锁
|
||||
- **文件**:[main/ota/ota_coordinator.cpp:509-549](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L509-L549)
|
||||
- **问题**:`isIdle`、`isGd32Busy`、`isEsp32Busy`、`getSystemStateString` 各自调 `getSystemState()`(加锁取值再解锁)。`if (!isIdle() && !isGd32Busy())` 这类连续调用会加锁 2~3 次
|
||||
- **建议**:提供"取一次状态后多次判断"的接口,或让调用方先 `getSystemState()` 取一次再传给各判断函数
|
||||
- **备注**:上轮 A.2 残留
|
||||
|
||||
### P1-11 esp32_ota_core finalize 3 处清理序列重复
|
||||
- **文件**:[main/ota/esp32_ota_core.cpp:281-288,312-318,332-339](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L281-L339)
|
||||
- **问题**:3 处几乎相同:`esp_ota_abort(m_handle); m_ended = false; releaseMutex(); resetState(); return xxx;`(SHA 计算失败 / SHA 不匹配 / esp_ota_end 失败)
|
||||
- **建议**:提取私有方法 `finalizeCleanupAndReturn(esp_err_t code)`,或用 `goto cleanup`
|
||||
|
||||
### P1-12 esp32_ota_core begin 3 处清理序列重复
|
||||
- **文件**:[main/ota/esp32_ota_core.cpp:126-130,135-141,151-158](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L126-L158)
|
||||
- **问题**:3 处清理序列:分区找不到 / 固件太大 / `esp_ota_begin` 失败,模式高度相似,差异仅在是否清零 `m_handle` 和返回码
|
||||
- **建议**:提取 `beginCleanupAndReturn(esp_err_t code)`,或统一出错点设置返回码后 `goto begin_fail`
|
||||
|
||||
### P1-13 doFinishAndVerify 100 行双职责
|
||||
- **文件**:[main/relay/ota_relay.cpp:1118-1216](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1118-L1216)
|
||||
- **问题**:单函数承担两个协议阶段:(1) 发送 FINISH_OTA 并等 ACK(1120-1176);(2) APP 存活轮询验证(1184-1213)。函数名含 "Verify",但两个阶段职责不同、超时参数不同、重试策略不同,行 1191 `for (attempt < 30)` 与 FINISH_OTA 的 `for (retry < 10)` 嵌套语义混淆
|
||||
- **建议**:拆分为 `doSendFinishOta()` + `doWaitForAppAlive()`,`doFinishAndVerify` 改为顺序调用两者并做 session 清理
|
||||
|
||||
### P1-14 APP 存活探测模式三处重复
|
||||
- **文件**:[main/relay/ota_relay.cpp:1191-1207,1240-1261](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1191-L1207) + [main/ota/ota_coordinator.cpp:596-615](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L596-L615)
|
||||
- **问题**:同一"调 `SpiMaster::appPing` + 写 `gd32AppAlive`/`gd32Version` + 日志版本号"模式在 3 处重复:(1) `doFinishAndVerify` 轮询(30 次重试);(2) `taskEntry` 启动探测(1 次);(3) `OtaCoordinator::probeGd32AppOnce`(1 次)。日志格式、版本号字段打印、成功/失败处理高度相似但不完全相同
|
||||
- **建议**:提取 `static void OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t* out_uptime)`,三处调用方分别指定重试次数和超时
|
||||
|
||||
### P1-15 setWifiConnected 未加锁
|
||||
- **文件**:[main/monitor/health_monitor.cpp:320-323,248](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L320-L323)
|
||||
- **问题**:直接写 `volatile bool m_wifiConnected`,未走 `m_mutex`。虽然 `volatile` 保证可见性,但与 `m_healthState` 等其他状态的访问模式不一致。`checkBeforeOta` 行 248 读 `m_wifiConnected` 也未加锁,存在读写竞态
|
||||
- **建议**:统一通过 LockGuard 保护,或显式说明为何此字段例外(如使用 `std::atomic<bool>`)
|
||||
- **备注**:上轮 F 给 HealthMonitor 加了 LockGuard,但 `m_wifiConnected` 漏了
|
||||
|
||||
### P1-16 温度 esp_timer_handle_t 是局部变量,资源无法回收
|
||||
- **文件**:[main/monitor/health_monitor.cpp:182-194](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L182-L194)
|
||||
- **问题**:`esp_timer_handle_t temp_timer` 是局部变量未存为成员。任务若被删除,定时器持续运行,回调仍会访问 `instance()` 的成员
|
||||
- **建议**:将 `temp_timer` 存为成员,提供 `deinit()` 在任务删除时 `esp_timer_stop` + `esp_timer_delete`
|
||||
|
||||
### P1-17 metaUpdate 读-改-写非原子
|
||||
- **文件**:[main/storage/gd32_flash_store.cpp:188-198](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L188-L198)
|
||||
- **问题**:`metaLoad` → 修改 → `metaSave` 中间无锁,若两个任务同时调用 `metaUpdate`,会丢失更新
|
||||
- **建议**:在 Gd32FlashStore 内增加 NVS 互斥锁,或将"由调用方串行化"约束以运行时断言强制
|
||||
|
||||
### P1-18 metaLoad NVS open 失败一律当 OK
|
||||
- **文件**:[main/storage/gd32_flash_store.cpp:142-145](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L142-L145)
|
||||
- **问题**:`nvs_open` 失败时直接 `return ESP_OK;`(注释说"命名空间不存在时返回全零结构"),但 NVS open 失败原因可能不止"不存在"(如 NVS 损坏),一律当 OK 会掩盖真实错误
|
||||
- **建议**:区分 `ESP_ERR_NVS_NOT_FOUND`(OK + 全零)与其他错误(返回 err)
|
||||
|
||||
### P1-19 DebugLog init 不检查 uartInit 返回值
|
||||
- **文件**:[main/monitor/debug_log.cpp:252-272](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L252-L272)
|
||||
- **问题**:`uartInit();`(行 258)返回值被丢弃,UART 初始化失败仍设 `m_initialized = true` 并打印"调试日志已初始化"
|
||||
- **建议**:检查返回值,失败时降级(仅 ring buffer)并在日志中标注 UART 不可用
|
||||
|
||||
### P1-20 ringDrainJson 容量估算与边界检查
|
||||
- **文件**:[main/monitor/debug_log.cpp:116-151](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L116-L151)
|
||||
- **问题**:`capacity = count * (LOG_RING_SIZE * 2) + 16`,count=64、LOG_RING_SIZE=256 → 最大 32784 字节,单次 malloc 在 ESP32-C3(~160KB heap)上风险较大;`for` 循环 `pos < capacity - 4` 的边界检查在 `first` 分支后逗号写入(行 133)未做边界检查,可能溢出
|
||||
- **建议**:分块 realloc,或对单条日志超长时截断并标记
|
||||
|
||||
### P1-21 关键任务创建失败不停止初始化
|
||||
- **文件**:[main/core/main.cpp:69-73,101-105,117-121,133-137](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L69-L137)
|
||||
- **问题**:任务创建失败仅 `DEBUG_ERR`,不停止后续初始化,可能导致部分功能缺失但系统"看起来正常启动"
|
||||
- **建议**:关键任务(health_mon、ota_relay)失败时 `ESP_ERROR_CHECK(ESP_FAIL)` 或进入 safe state
|
||||
|
||||
### P1-22 s_ota_recv_buf 全局静态被双 handler 共享
|
||||
- **文件**:[main/server/http_handlers.cpp:36,249,337](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L36)
|
||||
- **问题**:`static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];` 在 ESP32 与 GD32 两条路径间共享。httpd 默认单线程不会并发,但若未来 `max_open_sockets > 1` 启用并发请求,两个 `ota_upload_handler` 同时跑会互相覆盖 buffer
|
||||
- **建议**:改为 handler 内栈分配(若 `OTA_BLOCK_SIZE < 4KB` 可行),或改为 per-request 分配
|
||||
|
||||
### P1-23 一次性任务创建失败上报不一致
|
||||
- **文件**:[main/server/ble_ota.cpp:816-821,875-878,896-899](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L816-L899)
|
||||
- **问题**:
|
||||
- ble_gd32_relay_wait_task 创建失败:`free(ctx); sendFinishEvent(ERR_GD32_RELAY, ...);` — 完整上报
|
||||
- ble_esp32_finish_and_reboot_task 创建失败:`free(finish_ctx); m_esp32RebootPending = false;` — **未上报 FINISH 错误事件**,前端会一直等
|
||||
- ble_esp32_restart_task 创建失败:`m_esp32RebootPending = false;` — **未上报任何错误**
|
||||
- **建议**:统一"任务创建失败 → 释放 ctx → 复位 pending 标志 → 上报 FINISH 错误事件"三步
|
||||
|
||||
### P1-24 reorder slot malloc 失败静默丢弃
|
||||
- **文件**:[main/server/ble_ota.cpp:513-518](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L513-L518)
|
||||
- **问题**:malloc 失败时返回 `ESP_OK` 让上层认为已处理,但实际数据丢失。若客户端窗口已用完(无重传预算),固件写入会永久卡在缺口。注释说"靠客户端重传"但客户端无法感知设备端 malloc 失败
|
||||
- **建议**:malloc 失败时至少发一次 `sendWindowAck` 报告当前 `expected`,让客户端立即重传缺口;或返回错误触发 abort
|
||||
|
||||
### P1-25 protocol.h 死代码
|
||||
- **文件**:[main/transport/protocol.h:16-18,25-29](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/protocol.h#L16-L29) + [main/transport/protocol.cpp:96-118](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/protocol.cpp#L96-L118)
|
||||
- **问题**:
|
||||
1. `protocol.h` 仍定义 `FRAME_SOF 0xEB`、`FRAME_MAX_PAYLOAD 64`、`FRAME_OVERHEAD 6`,但 `protocol.cpp` 注释(第 8-10 行)明确说"帧构建、AES-CBC 加解密、CRC16 等中间件代码已移除",这三个常量无任何引用
|
||||
2. `session_ctx_t.iv[16]` 在 `sessionKeyInit` 中被 `memset(iv, 0, 16)` 后再无任何写入;`session_ctx_t.used` 初始化为 0 后再无任何代码置 1。整个 `session_ctx_t` 实际只用到 `key[16]`
|
||||
- **建议**:删除 `FRAME_SOF`/`FRAME_MAX_PAYLOAD`/`FRAME_OVERHEAD`;精简 `session_ctx_t` 为 `{ uint8_t key[16]; }` 或整体移除
|
||||
|
||||
### P1-26 GD32 OTA 写入序列两路径重复
|
||||
- **文件**:[main/server/ble_ota.cpp:1113-1208](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1113-L1208)(BLE)vs [main/server/http_handlers.cpp:314-413](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L314-L413)(HTTP)
|
||||
- **问题**:两路径都执行相同序列:`beginGd32Storage` → `saveGd32Meta` → 循环 `writeGd32Chunk` + `updateGd32Meta` → `completeGd32Storage` + `finalizeGd32Checksum`。但 HTTP 路径多了 `eraseGd32Partition` 和 `clearGd32Meta`(行 327、335),BLE 路径没有。这暗示 BLE 路径可能依赖前次 HTTP 上传后的已擦除状态,或在 `beginGd32Storage` 内隐式擦除——这种隐式假设是 bug 温床
|
||||
- **建议**:抽取 `OtaCoordinator::storeGd32FirmwareFromStream(...)` 接收"读块回调",BLE 与 HTTP 各自实现回调,统一擦除/写入/meta/完成序列
|
||||
|
||||
---
|
||||
|
||||
## P2 优先(样板抽取 / 配置化 / 风格统一)
|
||||
|
||||
### P2-A 跨模块样板抽取(高 ROI)
|
||||
|
||||
| # | 主题 | 涉及位置 |
|
||||
|---|---|---|
|
||||
| P2-27 | Hex 编解码工具统一 | [http_handlers.cpp:460-463,558-562,629-633](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L460-L633)、[ble_ota.cpp:174-178,642-649,861-863](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L174-L863) 共 6+ 处。**关键 bug**:SHA-256 在 BLE 路径大小写混用(`shaBytesToHexUpper` 大写 vs 行 861-863 `%02x` 小写),HTTP 路径用大写 `%02X`,前端比对 SHA 时大小写敏感会出错 |
|
||||
| P2-28 | SHA-256 → hex 循环 | [esp32_ota_core.cpp:230-234,254-257,290-293,323-325](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L230-L325) 4 处 + [gd32_flash_store.cpp:283-286,316-320](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L283-L320) 2 处。提取 `static void sha256ToHex(const uint8_t in[32], char out[65])` |
|
||||
| P2-29 | HTTP JSON body 接收+解析 | [http_handlers.cpp:71-95,436-452,522-535,595-608](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L71-L608) 4 处。缓冲区上限不一(256/512)。提取 `http_recv_json(httpd_req_t*, char*, size_t, cJSON**)` |
|
||||
| P2-30 | HTTP 错误响应样板 | [http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp) 17 处 `httpd_resp_send_err + return ESP_FAIL`。提取 `http_send_json_error/http_send_json_ok` |
|
||||
| P2-31 | 忙状态判断 if 链 | [ota_coordinator.cpp:136-140](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L136-L140)(beginGd32Storage) vs [ota_coordinator.cpp:346-350](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L346-L350)(beginEsp32Ota)。提取 `bool isAnyChannelBusy() const` |
|
||||
| P2-32 | 三个校验函数重复 metaLoad 样板 | [gd32_flash_store.cpp:265-275,292-298,331-351](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L265-L351) |
|
||||
|
||||
### P2-B 函数过长 / 高圈复杂度
|
||||
|
||||
| # | 函数 | 行数 | 文件 |
|
||||
|---|---|---|---|
|
||||
| P2-33 | ble_ota_process_data_batch | 245 行 | [ble_ota.cpp:975-1219](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L975-L1219)(见 P0-8) |
|
||||
| P2-34 | ota_upload_handler | 240 行 | [http_handlers.cpp:180-420](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L180-L420)。ESP32/GD32 双路径混杂,共享 `pre_read_buf`/`pre_read_len`。拆为 `handle_esp32_upload`/`handle_gd32_upload` |
|
||||
| P2-35 | ble_ota_ctrl_access | 190 行 | [ble_ota.cpp:717-907](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L717-L907)。3 命令分支长度极不均(0xF7 90 行 / 0xF8 54 行 / 0xFA 19 行),`session_id` 提取重复 |
|
||||
| P2-36 | doRelayPackets | 180 行 + goto | [ota_relay.cpp:936-1115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L936-L1115)(见 P0-7) |
|
||||
| P2-37 | doFinishAndVerify | 100 行双职责 | [ota_relay.cpp:1118-1216](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1118-L1216)(见 P1-13) |
|
||||
| P2-38 | app_main | 124 行 | [main.cpp:30-154](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L30-L154)。拆为 `initEarlyServices` + `createOtaTasks` + `markAppValid` |
|
||||
|
||||
### P2-C 配置化(magic number → app_config.h)
|
||||
|
||||
| # | 主题 | 位置 |
|
||||
|---|---|---|
|
||||
| P2-39 | 任务栈/优先级散落硬编码 | [main.cpp:63,65,95,97,111,113,127,129](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L63)、[ble_ota.cpp:812,871,892,1723](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L812)、[wifi_ota.cpp:623](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L623)、[http_handlers.cpp:905-913](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L905-L913)(task_metrics 必须手工同步,已出现一处不对齐风险) |
|
||||
| P2-40 | HTTP server config | [wifi_ota.cpp:419-424](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L419-L424)(`max_uri_handlers=32` 实际只用 18) |
|
||||
| P2-41 | BLE 连接参数 | [ble_ota.cpp:1395-1401](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1395-L1401)(itvl/latency/supervision) |
|
||||
| P2-42 | AP 静态 IP | [wifi_ota.cpp:234-236,240,258-259](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L234-L259) |
|
||||
| P2-43 | BLE_OTA_ACK_WINDOW 位置 | [ble_ota.cpp:58](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L58) 用 `#define` 应移入 app_config.h 作 `inline constexpr` |
|
||||
| P2-44 | debug_log magic number | [debug_log.cpp:199,317,352,365,369](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L199)(1024/512/600/32) |
|
||||
| P2-45 | 健康巡检周期 | [health_monitor.cpp:190,208](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L190)(1000000/5000) |
|
||||
| P2-46 | 温度采样间隔 | [health_monitor.cpp:190](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L190) |
|
||||
|
||||
### P2-D 死代码 / 未使用配置
|
||||
|
||||
| # | 项 | 位置 |
|
||||
|---|---|---|
|
||||
| P2-47 | `DEBUG_LOG_ENABLED` 全代码库零引用 | [app_config.h:83](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L83) |
|
||||
| P2-48 | `GD32_CHIP_UID[]` 占位 | [app_config.h:120-128](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L120-L128) |
|
||||
| P2-49 | `BLE_OTA_PACKET_OVERHEAD` 零引用 | [app_config.h:104](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L104) |
|
||||
| P2-50 | `WIFI_RSSI_WEAK` 零引用 | [health_monitor.h:33](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L33) |
|
||||
| P2-51 | `DEBUG_LOG_NONE/ALL` 零引用 | [debug_log.h:35,40](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L35) |
|
||||
| P2-52 | `WIFI_STA_USE_STATIC_IP` 临时调试开关 | [app_config.h:36](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L36) |
|
||||
| P2-53 | `ota_snapshot.h` 仅聚合无价值 | [ota_snapshot.h](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_snapshot.h) 全文 |
|
||||
|
||||
### P2-E 其他低风险风格类
|
||||
|
||||
| # | 项 | 位置 |
|
||||
|---|---|---|
|
||||
| P2-54 | SpiMaster 公共接口混合多个抽象层 | [spi_master.h:60-100](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.h#L60-L100)(上轮 D 残留) |
|
||||
| P2-55 | DebugLog 未用 RAII(与 HealthMonitor 风格不一) | [debug_log.cpp:53-60,78-108,162-174](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L53-L174) |
|
||||
| P2-56 | DebugLog logLevel 暴露在 public | [debug_log.h:86](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L86) |
|
||||
| P2-57 | DebugLog 三路 sink 抽象为列表 | [debug_log.cpp:300-333](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L300-L333)(上轮 E 项遗留) |
|
||||
| P2-58 | DebugLog send() 返回值无人检查,可改 void | [debug_log.cpp:329-332](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L329-L332) |
|
||||
| P2-59 | ensureUdpSocket 失败后每条日志都重试 | [debug_log.cpp:278-292](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L278-L292) |
|
||||
| P2-60 | DebugLog log() 三次缓冲区拼接 | [debug_log.cpp:342-373](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L342-L373) |
|
||||
| P2-61 | DebugLog 头注释与实现不一致(UDP 无开关) | [debug_log.h:6-7](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L6-L7) vs [debug_log.cpp:311-324](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L311-L324) |
|
||||
| P2-62 | DebugLog 重复 include | [debug_log.h:23-24](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L23-L24) vs [debug_log.cpp:20,23](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L20) |
|
||||
| P2-63 | health_monitor WIFI_RSSI_NORMAL 别名位置错误 | [health_monitor.h:32-33](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L32-L33) |
|
||||
| P2-64 | health_state_t 与 health_status_t 两套枚举语义重叠 | [health_monitor.h:16-29](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L16-L29) |
|
||||
| P2-65 | health_monitor 阈值检查样板可抽 applyThresholdAlarm | [health_monitor.cpp:129-156,333-357](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L129-L357) |
|
||||
| P2-66 | health_monitor checkBeforeOta 与 checkBeforeOtaRelay 分级样板重复 | [health_monitor.cpp:232-311](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L232-L311) |
|
||||
| P2-67 | health_monitor taskEntry 职责混杂 | [health_monitor.cpp:165-210](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L165-L210) |
|
||||
| P2-68 | health_monitor logEvent 与相邻 DEBUG 重复日志 | [health_monitor.cpp:220-223](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L220-L223) |
|
||||
| P2-69 | health_monitor tempMonitorCallback 注释硬编码温度值 | [health_monitor.cpp:293](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L293) |
|
||||
| P2-70 | gd32_flash_store NVS 字段名硬编码 | [gd32_flash_store.cpp:136-185](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L136-L185) |
|
||||
| P2-71 | gd32_flash_store tag "OTA" 与模块名不一致 | [gd32_flash_store.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp) 全文 |
|
||||
| P2-72 | gd32_flash_store SHA 长度 32 裸数字 | [gd32_flash_store.h:26](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.h#L26) 等 |
|
||||
| P2-73 | gd32_flash_store flashWriteChunk 错误清理重复 | [gd32_flash_store.cpp:74-108](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L74-L108) |
|
||||
| P2-74 | gd32_flash_store computeFlashSha256 错误清理重复 | [gd32_flash_store.cpp:224-262](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L224-L262) |
|
||||
| P2-75 | gd32_flash_store 全零 checksum 检查可简化为 memcmp | [gd32_flash_store.cpp:299-302](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L299-L302) |
|
||||
| P2-76 | gd32_flash_store flashWriteChunk 非对齐警告条件恒真 | [gd32_flash_store.cpp:65-72](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L65-L72) |
|
||||
| P2-77 | gd32_flash_store m_partition 懒初始化无线程安全 | [gd32_flash_store.cpp:30-40](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L30-L40) |
|
||||
| P2-78 | main.cpp 4 处 xTaskCreatePinnedToCore 样板 | [main.cpp:60-137](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L60-L137) |
|
||||
| P2-79 | main.cpp 启动横幅分隔线硬编码 4 次 | [main.cpp:50,56,149,153](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L50-L153) |
|
||||
| P2-80 | main.cpp xRet 变量重复使用 | [main.cpp:59,69,92,101,108,117,124,133](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L59-L133) |
|
||||
| P2-81 | http_handlers.cpp 单文件 TAG 混用 OTA/WIFI/SPI | [http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp) 全文 |
|
||||
| P2-82 | canFdSend 返回 -1 无法区分失败模式 | [canfd_bridge.cpp:59-68](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/canfd_bridge.cpp#L59-L68) |
|
||||
| P2-83 | otaCrc8 实为 XOR 校验和,命名误导 | [spi_master.cpp:180-187](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L180-L187) |
|
||||
| P2-84 | otaParseFrame/otaFindFrame 验证逻辑重复 | [spi_master.cpp:225-279](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L225-L279) |
|
||||
| P2-85 | CAN 帧 vs SPI OTA 帧两层 framing 未抽象 | [canfd_bridge.cpp:12-51](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/canfd_bridge.cpp#L12-L51) + [spi_master.cpp:198-279](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L198-L279) |
|
||||
| P2-86 | BLE OTA WiFi 共存标志位冗余 | [ble_ota.cpp:226-250](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L226-L250) |
|
||||
| P2-87 | http_handlers task_metrics 硬编码任务栈大小 | [http_handlers.cpp:905-913](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L905-L913) |
|
||||
| P2-88 | notifyRelayStarted/Complete 纯转发中间层 | [ota_coordinator.cpp:264-307](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L264-L307) |
|
||||
| P2-89 | notifyEsp32FinalizeComplete 与 notifyEsp32Rebooting 几乎相同 | [ota_coordinator.cpp:409-428](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L409-L428) |
|
||||
| P2-90 | doFinishAndVerify 成功路径与 reset() session 清理重复 | [ota_relay.cpp:1178-1182 vs 487-504](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1178-L1182) |
|
||||
| P2-91 | metaLoad 单次中继被调 3 次 | [ota_coordinator.cpp:213-221](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L213-L221) + [ota_relay.cpp:311-316,438-440](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L311-L440) |
|
||||
| P2-92 | forwardFirmware 中 expected_ack=0xF0F0F0F0 魔法数字 | [ota_relay.cpp:1024](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1024) |
|
||||
| P2-93 | doRelayPackets:918 WARN 级别输出纯配置信息(调试残留) | [ota_relay.cpp:918](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L918) |
|
||||
| P2-94 | app_config 配置项类型不一致(uint8_t 应为 bool) | [app_config.h:25,36,83](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L25) |
|
||||
|
||||
---
|
||||
|
||||
## 不建议改的地方(保留上轮结论)
|
||||
|
||||
- OTA 事件状态机的 5 步驱动顺序、健康检查时机、`OtaCoordinator` 的 watchdog 自愈、`forceCleanup` 仅在 `isIdle()` 启动。
|
||||
- `Esp32OtaCore::s_mutex + s_occupied` 双层 mutex 语义("持锁查占用" + "持续占用期")。
|
||||
- 单例模式已统一(`static XXX inst; return inst;` + `= delete` 拷贝构造/赋值)。
|
||||
- 无 TODO/FIXME/XXX/HACK 标记,无注释掉的死代码。
|
||||
|
||||
---
|
||||
|
||||
## 预估收益
|
||||
|
||||
- 修复 4 项功能性 bug(校验后门 / RSSI 状态机降级 / 固件大小注释 / 凭证安全)。
|
||||
- 消除 6 处数据竞争(OtaRelay 状态访问 + setWifiConnected)。
|
||||
- 6 个超长函数拆分(245/240/190/180/124/100 行),可维护性显著提升。
|
||||
- `main/` 行数预计再减约 400~500 行(去重 + 死代码)。
|
||||
- 双通道(BLE/HTTP)一致性通过 `validateGd32Size` / `storeGd32FirmwareFromStream` 抽取彻底对齐。
|
||||
- protocol.h / app_config.h 死代码清理约减 50+ 行。
|
||||
|
||||
---
|
||||
|
||||
## 推荐落地顺序
|
||||
|
||||
1. **第 1 批(P0)**:P0-1 ~ P0-8。先修功能正确性 / 安全性 bug。P0-7/P0-8 函数拆分风险高,需配合 GD32 全链路 OTA 回归。
|
||||
2. **第 2 批(P1.A)**:OtaRelay 状态成员封装一组(P1-A + P1-15),消除数据竞争,原子改动。
|
||||
3. **第 3 批(P1.B)**
|
||||
4. **第 4 批(P2.A/B)**:样板抽取(P2-27 ~ P2-32)+ 函数拆分(P2-33 ~ P2-38)。
|
||||
5. **第 5 批(P2.C/D/E)**:配置化(P2-39 ~ P2-46)+ 死代码删除(P2-47 ~ P2-53)+ 风格统一(P2-54 ~ P2-94),可随业务变更顺手做。
|
||||
@@ -0,0 +1,180 @@
|
||||
esp-idf/main/CMakeFiles/__idf_main.dir/monitor/debug_log.cpp.obj: \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\monitor\debug_log.cpp \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/string.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/cdefs.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/cdefs.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/ieeefp.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/features.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/picolibc.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/float.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_default_types.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/build/config/sdkconfig.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdint.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stddef.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_intsup.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_stdint.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_locale.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/strings.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/string.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/stdio.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdio.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stdarg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/lock.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/lock.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/esp32xx/include/sys/socket.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/include/lwip/sockets.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/sockets.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/opt.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/lwipopts.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdlib.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/stdlib.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/alloca.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_wait.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/unistd.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/unistd.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/endian.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_endian.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_pthreadtypes.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/sched.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_timespec.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_sigset.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_timeval.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/unistd.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/fcntl.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/fcntl.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_default_fcntl.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/ioctl.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/poll.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_system/include/esp_task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/FreeRTOS.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/config/include/freertos/FreeRTOSConfig.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/assert.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/config/riscv/include/freertos/FreeRTOSConfig_arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/projdefs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/portable.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/deprecated_definitions.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/portable/riscv/include/freertos/portmacro.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stdbool.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/spinlock.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_cpu.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/soc_caps.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/include/soc/soc_caps_eval.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/param.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/syslimits.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/param.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/encoding.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/rv_utils.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/interrupt_reg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/register/soc/interrupt_core0_reg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/soc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_bit_defs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/register/soc/reg_base.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_attr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/interrupt.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/esp_private/interrupt_intc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr_pie.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr_dsp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_intr_alloc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_err.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_compiler.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_intr_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_macros.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_rom/include/esp_rom_sys.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/inttypes.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/reset_reasons.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/heap/include/esp_heap_caps.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/heap/include/multi_heap.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_system/include/esp_system.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_idf_version.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/esp_newlib.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/reent.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/limits.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/limits.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/mpu_wrappers.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/esp_additions/include/freertos/idf_additions.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/list.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/queue.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/semphr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/queue.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/stream_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/message_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/stream_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/event_groups.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/timers.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_random.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/sntp/sntp_get_set_time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/sockets_ext.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/freertos/include/arch/sys_arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/debug.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/esp32xx/include/arch/cc.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/errno.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/errno.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/errno.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/ctype.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/def.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip4_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip6_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/def.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip6_zone.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/netif.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/err.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/pbuf.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/stats.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/mem.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/memp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/memp_std.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/memp_priv.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/mem_priv.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/inet.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/errno.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/net/if.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/if_api.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/netdb.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/include/lwip/netdb.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/netdb.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/arpa/inet.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_config.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_level.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_color.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_timestamp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_write.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_format.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_args.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_log_attr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/log/include/esp_private/log_attr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_driver_uart/include/driver/uart.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hal_uart/include/hal/uart_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/clk_tree_defs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_check.h \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\monitor\debug_log.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/riscv32-esp-elf/include/c++/15.2.0/cstdint \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/c++config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/os_defines.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/cpu_defines.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/riscv32-esp-elf/include/c++/15.2.0/pstl/pstl_config.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/include/version.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/netinet/in.h
|
||||
@@ -0,0 +1,180 @@
|
||||
esp-idf/main/CMakeFiles/__idf_main.dir/monitor/health_monitor.cpp.obj: \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\monitor\health_monitor.cpp \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/stdio.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/build/config/sdkconfig.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stddef.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/features.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/picolibc.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdio.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/cdefs.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/cdefs.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/ieeefp.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/float.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_default_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdint.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_intsup.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_stdint.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stdarg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/lock.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/lock.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/string.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_locale.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/strings.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/string.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/FreeRTOS.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/config/include/freertos/FreeRTOSConfig.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/assert.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdlib.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/stdlib.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/alloca.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_wait.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/config/riscv/include/freertos/FreeRTOSConfig_arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/projdefs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/portable.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/deprecated_definitions.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/portable/riscv/include/freertos/portmacro.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stdbool.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/spinlock.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_cpu.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/soc_caps.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/include/soc/soc_caps_eval.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/param.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/syslimits.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/endian.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_endian.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/param.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/encoding.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/rv_utils.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/interrupt_reg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/register/soc/interrupt_core0_reg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/soc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_bit_defs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/register/soc/reg_base.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_attr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/interrupt.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/esp_private/interrupt_intc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr_pie.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr_dsp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_intr_alloc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_err.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_compiler.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_intr_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_macros.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_rom/include/esp_rom_sys.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/inttypes.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/reset_reasons.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/heap/include/esp_heap_caps.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/heap/include/multi_heap.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_system/include/esp_system.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_idf_version.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/esp_newlib.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/reent.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/limits.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/limits.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/mpu_wrappers.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/esp_additions/include/freertos/idf_additions.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/list.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/queue.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/semphr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/queue.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/stream_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/message_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/stream_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/event_groups.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/timers.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/esp_wifi.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/esp_wifi_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_event/include/esp_event_base.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/local/esp_wifi_types_native.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/esp_wifi_types_generic.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_event/include/esp_event.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/esp_wifi_crypto_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_wifi/include/esp_wifi_default.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_netif/include/esp_netif.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_netif/include/esp_netif_ip_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_netif/include/esp_netif_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_netif/include/esp_netif_defaults.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_timer/include/esp_timer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/etm/include/esp_etm.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_driver_tsens/include/driver/temperature_sensor.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hal_ana_conv/include/hal/temperature_sensor_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/clk_tree_defs.h \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\monitor\health_monitor.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/riscv32-esp-elf/include/c++/15.2.0/cstdint \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/c++config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/os_defines.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/cpu_defines.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/riscv32-esp-elf/include/c++/15.2.0/pstl/pstl_config.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/include/version.h \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\monitor\debug_log.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/esp32xx/include/sys/socket.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/include/lwip/sockets.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/sockets.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/opt.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/lwipopts.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/unistd.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/unistd.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_pthreadtypes.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/sched.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_timespec.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_sigset.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_timeval.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/unistd.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/fcntl.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/fcntl.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_default_fcntl.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/ioctl.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/poll.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_system/include/esp_task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_random.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/sntp/sntp_get_set_time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/sockets_ext.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/freertos/include/arch/sys_arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/debug.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/esp32xx/include/arch/cc.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/errno.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/errno.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/errno.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/ctype.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/def.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip4_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip6_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/def.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip6_zone.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/netif.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/err.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/pbuf.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/stats.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/mem.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/memp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/memp_std.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/memp_priv.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/mem_priv.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/inet.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/errno.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/net/if.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/if_api.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/netinet/in.h
|
||||
@@ -0,0 +1,165 @@
|
||||
esp-idf/transport/CMakeFiles/__idf_transport.dir/__/__/main/transport/protocol.cpp.obj: \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\transport\protocol.cpp \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/string.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/cdefs.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/cdefs.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/ieeefp.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/features.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/picolibc.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/float.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_default_types.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/build/config/sdkconfig.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdint.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stddef.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_intsup.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_stdint.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_locale.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/strings.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/string.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/stdio.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdio.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stdarg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/lock.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/lock.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_random.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/mbedtls/port/include/aes/esp_aes.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/stdbool.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hal_security/include/hal/aes_types.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/riscv32-esp-elf/include/c++/15.2.0/cstdint \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/c++config.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/os_defines.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/riscv32-esp-elf/include/c++/15.2.0/riscv32-esp-elf/rv32imc_zicsr_zifencei/ilp32/no-rtti/bits/cpu_defines.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/riscv32-esp-elf/include/c++/15.2.0/pstl/pstl_config.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/FreeRTOS.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/config/include/freertos/FreeRTOSConfig.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/assert.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/stdlib.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/stdlib.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/alloca.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_wait.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/config/riscv/include/freertos/FreeRTOSConfig_arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/projdefs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/portable.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/deprecated_definitions.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/portable/riscv/include/freertos/portmacro.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/spinlock.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_cpu.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/soc_caps.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/include/soc/soc_caps_eval.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/param.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/syslimits.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/endian.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_endian.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/param.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/encoding.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/rv_utils.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/interrupt_reg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/register/soc/interrupt_core0_reg.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/soc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_bit_defs.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/register/soc/reg_base.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_attr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_assert.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/interrupt.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/esp_private/interrupt_intc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr_pie.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/riscv/include/riscv/csr_dsp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_intr_alloc.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_err.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_compiler.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_hw_support/include/esp_intr_types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_macros.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_rom/include/esp_rom_sys.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/inttypes.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/soc/esp32c3/include/soc/reset_reasons.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/heap/include/esp_heap_caps.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/heap/include/multi_heap.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_system/include/esp_system.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_common/include/esp_idf_version.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/esp_newlib.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/reent.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/limits.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/lib/gcc/riscv32-esp-elf/15.2.0/include/limits.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/mpu_wrappers.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/esp_additions/include/freertos/idf_additions.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/list.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/queue.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/semphr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/queue.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/stream_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/message_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/stream_buffer.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/event_groups.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/freertos/FreeRTOS-Kernel/include/freertos/timers.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/include/version.h \
|
||||
G:\Robot_v2\DCU\ESP32_COMM\main\transport\protocol.h \
|
||||
G:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/esp32xx/include/sys/socket.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/include/lwip/sockets.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/sockets.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/opt.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/lwipopts.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/unistd.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/unistd.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/types.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_pthreadtypes.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/sched.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_timespec.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/types.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_sigset.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_timeval.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_select.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/unistd.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/fcntl.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/fcntl.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/_default_fcntl.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/time.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/machine/_time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/ioctl.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/poll.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_system/include/esp_task.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/sntp/sntp_get_set_time.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/sockets_ext.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/freertos/include/arch/sys_arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/debug.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/arch.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/esp32xx/include/arch/cc.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/errno.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/sys/errno.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/sys/errno.h \
|
||||
C:/Espressif/tools/riscv32-esp-elf/esp-15.2.0_20251204/riscv32-esp-elf/picolibc/include/ctype.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/def.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip4_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip6_addr.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/def.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/ip6_zone.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/netif.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/err.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/pbuf.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/stats.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/mem.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/memp.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/memp_std.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/memp_priv.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/priv/mem_priv.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/inet.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/errno.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/esp_libc/platform_include/net/if.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/lwip/src/include/lwip/if_api.h \
|
||||
F:/software/.espressif/v6.0/esp-idf/components/lwip/port/include/netinet/in.h
|
||||
@@ -67,7 +67,10 @@ extern "C" void app_main(void)
|
||||
0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:负责温度与 WiFi RSSI 上报,缺失会让运维陷入盲区,
|
||||
// 无法及时发现硬件异常,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 health_monitor_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "健康监控任务已创建");
|
||||
}
|
||||
@@ -99,7 +102,10 @@ extern "C" void app_main(void)
|
||||
0 // 核0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:OTA 中继是本设备核心职责(经 SPI 派发固件分片到目标 MCU),
|
||||
// 缺失则 OTA 刷写主流程彻底瘫痪,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 ota_relay_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "OTA 中继任务已创建");
|
||||
}
|
||||
@@ -115,7 +121,10 @@ extern "C" void app_main(void)
|
||||
0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:WiFi OTA 是 OTA 刷写主通道之一,缺失则 WiFi 侧固件升级不可用,
|
||||
// 系统失去主要刷写路径,必须重启恢复
|
||||
DEBUG_ERR("MAIN", "创建 wifi_ota_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "WiFi OTA 任务已创建");
|
||||
}
|
||||
@@ -131,7 +140,12 @@ extern "C" void app_main(void)
|
||||
0 // ESP32-C3 单核,固定核0
|
||||
);
|
||||
if (xRet != pdPASS) {
|
||||
// 关键任务:BLE OTA 是 OTA 刷写主通道之一(与 WiFi OTA 互为冗余),
|
||||
// 缺失则 BLE 侧固件升级完全不可用,必须重启恢复
|
||||
// 注:此处为 BLE OTA 主服务任务,区别于 ble_ota.cpp 内部的
|
||||
// ble_ota_data_worker 子任务(后者可降级,不在本文件创建)
|
||||
DEBUG_ERR("MAIN", "创建 ble_ota_task 失败");
|
||||
ESP_ERROR_CHECK(ESP_FAIL); // 关键任务失败:终止启动并触发看门狗重启
|
||||
} else {
|
||||
DEBUG_INFO("MAIN", "BLE OTA 任务已创建");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* app_config.h - 统一配置文件(C++ 版本)
|
||||
* ESP32-C3 OTA 中继项目
|
||||
*
|
||||
@@ -59,7 +59,7 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< HTTP 服
|
||||
// OTA 配置
|
||||
// ============================================================
|
||||
inline constexpr size_t OTA_BLOCK_SIZE = 8192;
|
||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB,对齐 GD32 APP_MAX_SIZE (0x36000)
|
||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB = 221184B = 0x35F90,固件大小上限
|
||||
inline constexpr size_t GD32_OTA_PACKET_SIZE = 48; // GD32 OTA 实际转发包载荷
|
||||
inline constexpr uint32_t OTA_POLL_RETRIES = 40; // 每包 ACK 轮询次数
|
||||
inline constexpr uint32_t OTA_RESEND_RETRIES = 10; // 每包重发次数上限
|
||||
@@ -98,6 +98,7 @@ inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7; ///< BLE 控
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8; ///< BLE 控制命令:触发 ESP32 本机 OTA
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9; ///< BLE 控制命令:SHA-256 哈希验证
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_RESTART_ESP32 = 0xFA; ///< BLE 控制命令:重启 ESP32
|
||||
inline constexpr uint8_t BLE_OTA_CTRL_ABORT_OTA = 0xFB; ///< BLE 控制命令:中止当前 OTA(不断开 BLE)
|
||||
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; ///< BLE 数据通道:GD32 固件包标识
|
||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; ///< BLE 数据通道:ESP32 固件包标识
|
||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; ///< BLE 单包数据载荷大小(字节)
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 33
|
||||
#define FW_VERSION_STRING "0.3.33"
|
||||
#define FW_VERSION_PATCH 34
|
||||
#define FW_VERSION_STRING "0.3.34"
|
||||
|
||||
// 编译时间
|
||||
#define BUILD_TIME "2026-06-25 01:39:58"
|
||||
#define BUILD_TIME "2026-06-28 04:26:17"
|
||||
|
||||
// Git 信息
|
||||
#define GIT_HASH "7a41eac"
|
||||
#define GIT_HASH "6ef9730"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* debug_log.cpp - 调试日志输出实现
|
||||
*
|
||||
* 日志会同时写入三处:
|
||||
@@ -120,6 +120,8 @@ char* DebugLog::ringDrainJson(int *out_len)
|
||||
int count = ringCount();
|
||||
int capacity = count * (LOG_RING_SIZE * 2) + 16;
|
||||
if (capacity < 16) capacity = 16;
|
||||
// 当 count 较大(如 64)时 capacity 可能达 32KB+,单次 malloc 有风险。
|
||||
// 当前为简化实现保留单次 malloc;若后续日志量增大,可改为按 8KB 分块 realloc。
|
||||
char *buf = (char*)malloc(capacity);
|
||||
if (!buf) return NULL;
|
||||
|
||||
@@ -130,10 +132,22 @@ char* DebugLog::ringDrainJson(int *out_len)
|
||||
int entryLen = 0;
|
||||
|
||||
while (ringPopOldest(entry, sizeof(entry), &entryLen) && entryLen > 0) {
|
||||
if (!first) buf[pos++] = ',';
|
||||
// 逗号写入前做边界检查,预留 4 字节给 ']' + '\0' 及末尾余量
|
||||
if (!first) {
|
||||
if (pos + 1 >= capacity - 4) break;
|
||||
buf[pos++] = ',';
|
||||
}
|
||||
first = false;
|
||||
buf[pos++] = '"';
|
||||
|
||||
// 单条日志转义后超过 LOG_RING_SIZE * 2 时截断,标记 "..." 后缀
|
||||
int content_start = pos;
|
||||
bool truncated = false;
|
||||
for (int i = 0; i < entryLen && pos < capacity - 4; i++) {
|
||||
if (pos - content_start >= LOG_RING_SIZE * 2) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
char c = entry[i];
|
||||
if (c == '"') { buf[pos++] = '\\'; buf[pos++] = '"'; }
|
||||
else if (c == '\\') { buf[pos++] = '\\'; buf[pos++] = '\\'; }
|
||||
@@ -142,6 +156,14 @@ char* DebugLog::ringDrainJson(int *out_len)
|
||||
else if (c == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; }
|
||||
else if ((unsigned char)c >= 0x20) { buf[pos++] = c; }
|
||||
}
|
||||
if (truncated) {
|
||||
// 确保 "..." + '"' + ']' + '\0' (共 6 字节) 有空间才写入标记
|
||||
if (pos + 6 <= capacity) {
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
buf[pos++] = '.';
|
||||
}
|
||||
}
|
||||
buf[pos++] = '"';
|
||||
}
|
||||
buf[pos++] = ']';
|
||||
@@ -216,6 +238,8 @@ esp_err_t DebugLog::uartInit()
|
||||
}
|
||||
|
||||
m_uartInitialized = true;
|
||||
// 标记 UART 真正可用,send() 依据此标志决定是否调用 uart_write_bytes
|
||||
m_uartAvailable = true;
|
||||
ESP_LOGD(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)",
|
||||
UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_DEBUG_BAUD_RATE);
|
||||
|
||||
@@ -255,7 +279,13 @@ esp_err_t DebugLog::init()
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uartInit();
|
||||
// 检查 uartInit 返回值;失败时仍让 ring buffer 模式工作,
|
||||
// 但记录"UART 不可用"日志,并在 send() 中跳过 uart_write_bytes 调用。
|
||||
esp_err_t uart_ret = uartInit();
|
||||
if (uart_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "UART 不可用,仅 ring buffer 模式 (err=%s)", esp_err_to_name(uart_ret));
|
||||
m_uartAvailable = false;
|
||||
}
|
||||
|
||||
m_logRingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
@@ -302,8 +332,8 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
||||
esp_err_t udp_ret = ESP_FAIL;
|
||||
esp_err_t uart_ret = ESP_FAIL;
|
||||
|
||||
// UART 通道
|
||||
if (m_uartInitialized) {
|
||||
// UART 通道(UART 不可用时跳过 uart_write_bytes 调用)
|
||||
if (m_uartAvailable) {
|
||||
uart_ret = uartSend(data, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* debug_log.h - 调试日志输出
|
||||
*
|
||||
* 当前日志会并行写入:
|
||||
@@ -95,6 +95,9 @@ private:
|
||||
struct sockaddr_in m_debugAddr;
|
||||
bool m_initialized = false;
|
||||
bool m_uartInitialized = false;
|
||||
// 记录 UART 是否真正可用(uartInit 失败时为 false,跳过 uart_write_bytes)。
|
||||
// m_uartInitialized 仅表示曾尝试初始化,m_uartAvailable 才是写日志的判据。
|
||||
bool m_uartAvailable = false;
|
||||
|
||||
static constexpr int LOG_RING_SLOTS = 64;
|
||||
static constexpr int LOG_RING_SIZE = 256;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* health_monitor.cpp - 系统健康监控
|
||||
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
|
||||
*
|
||||
@@ -46,6 +46,21 @@ void HealthMonitor::init()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 反初始化:停止并删除温度监控定时器,回收 esp_timer 资源。
|
||||
* 当前 HealthMonitor 任务为无限循环,无显式删除路径,
|
||||
* 本方法预留给未来停用场景(如安全关机后停止监控)使用。
|
||||
*/
|
||||
void HealthMonitor::deinit()
|
||||
{
|
||||
if (m_tempTimer != nullptr) {
|
||||
esp_timer_stop(m_tempTimer);
|
||||
esp_timer_delete(m_tempTimer);
|
||||
m_tempTimer = nullptr;
|
||||
DEBUG_INFO("HEALTH", "温度监控定时器已停止并删除");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 线程安全的健康状态读写
|
||||
// ============================================================
|
||||
@@ -55,10 +70,8 @@ void HealthMonitor::init()
|
||||
*/
|
||||
health_state_t HealthMonitor::getState()
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
health_state_t s = m_healthState;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
return s;
|
||||
LockGuard lock(m_mutex);
|
||||
return m_healthState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,9 +80,8 @@ health_state_t HealthMonitor::getState()
|
||||
*/
|
||||
void HealthMonitor::setState(health_state_t newState)
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
LockGuard lock(m_mutex);
|
||||
m_healthState = newState;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -90,10 +102,8 @@ health_state_t HealthMonitor::getCurrent()
|
||||
*/
|
||||
float HealthMonitor::getLastTemperature()
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
float t = m_lastTemperature;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
return t;
|
||||
LockGuard lock(m_mutex);
|
||||
return m_lastTemperature;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,10 +114,13 @@ float HealthMonitor::getLastTemperature()
|
||||
*/
|
||||
bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
|
||||
{
|
||||
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
int64_t lastMs = m_lastWifiRssiMs;
|
||||
int8_t rssi = m_lastWifiRssi;
|
||||
if (m_mutex) xSemaphoreGive(m_mutex);
|
||||
int64_t lastMs;
|
||||
int8_t rssi;
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
lastMs = m_lastWifiRssiMs;
|
||||
rssi = m_lastWifiRssi;
|
||||
}
|
||||
|
||||
if (lastMs <= 0) {
|
||||
return false;
|
||||
@@ -138,9 +151,10 @@ void HealthMonitor::tempMonitorCallback(void *arg)
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.m_mutex) xSemaphoreTake(self.m_mutex, portMAX_DELAY);
|
||||
self.m_lastTemperature = temp;
|
||||
if (self.m_mutex) xSemaphoreGive(self.m_mutex);
|
||||
{
|
||||
LockGuard lock(self.m_mutex);
|
||||
self.m_lastTemperature = temp;
|
||||
}
|
||||
|
||||
if (temp >= TEMP_CRITICAL) {
|
||||
self.setState(SYS_SAFE_SHUTDOWN);
|
||||
@@ -180,17 +194,17 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
}
|
||||
|
||||
// 定时器,每1秒读取一次温度
|
||||
esp_timer_handle_t temp_timer;
|
||||
esp_timer_create_args_t timer_args = {};
|
||||
timer_args.callback = &tempMonitorCallback;
|
||||
timer_args.arg = nullptr;
|
||||
timer_args.name = "temp_monitor";
|
||||
timer_args.dispatch_method = ESP_TIMER_TASK;
|
||||
|
||||
if (esp_timer_create(&timer_args, &temp_timer) == ESP_OK) {
|
||||
esp_timer_start_periodic(temp_timer, 1000000); // 1秒
|
||||
if (esp_timer_create(&timer_args, &self.m_tempTimer) == ESP_OK) {
|
||||
esp_timer_start_periodic(self.m_tempTimer, 1000000); // 1秒
|
||||
DEBUG_INFO("MON", "温度监控定时器已启动 (1秒间隔)");
|
||||
} else {
|
||||
self.m_tempTimer = nullptr;
|
||||
DEBUG_ERR("MON", "创建温度监控定时器失败");
|
||||
}
|
||||
|
||||
@@ -246,7 +260,12 @@ health_status_t HealthMonitor::checkBeforeOta()
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
|
||||
}
|
||||
|
||||
if (!m_wifiConnected) {
|
||||
bool wifi_connected;
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
wifi_connected = m_wifiConnected;
|
||||
}
|
||||
if (!wifi_connected) {
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接");
|
||||
return HEALTH_MON_CRITICAL;
|
||||
}
|
||||
@@ -320,6 +339,7 @@ health_status_t HealthMonitor::checkBeforeOtaRelay()
|
||||
*/
|
||||
void HealthMonitor::setWifiConnected(bool connected)
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
m_wifiConnected = connected;
|
||||
}
|
||||
|
||||
@@ -348,10 +368,13 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
|
||||
return HEALTH_MON_WARNING;
|
||||
}
|
||||
|
||||
// RSSI 已恢复正常(>= WIFI_RSSI_NORMAL):从 WARNING 或 CRITICAL 任一状态降级到 NORMAL。
|
||||
// 历史缺陷:原仅当 cur == SYS_WARNING 时降级,导致 CRITICAL 状态下 RSSI 恢复正常无法降级。
|
||||
// 注意:SAFE_SHUTDOWN 不应被 RSSI 恢复降级,仅处理 WARNING/CRITICAL。
|
||||
health_state_t cur = getState();
|
||||
if (cur == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
|
||||
if ((cur == SYS_WARNING || cur == SYS_CRITICAL) && rssi >= WIFI_RSSI_NORMAL) {
|
||||
setState(SYS_NORMAL);
|
||||
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
|
||||
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm (从 %d 降级)", rssi, WIFI_RSSI_NORMAL, (int)cur);
|
||||
logEvent("WIFI恢复", rssi);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* health_monitor.h - 系统健康监控头文件
|
||||
*
|
||||
* HealthMonitor 类封装健康状态管理、温度监控和 WiFi RSSI 监控。
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/temperature_sensor.h"
|
||||
#include "esp_timer.h"
|
||||
#include "app_config.h"
|
||||
|
||||
// T9.3: 健康状态等级
|
||||
@@ -48,6 +49,13 @@ public:
|
||||
/** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief 反初始化:停止并删除温度监控定时器,回收 esp_timer 资源。
|
||||
* 在任务删除路径中调用;当前 HealthMonitor 任务为无限循环,
|
||||
* 无显式删除路径,本方法预留给未来停用场景使用。
|
||||
*/
|
||||
void deinit();
|
||||
|
||||
/** @brief 健康监控 FreeRTOS 任务入口。 */
|
||||
static void taskEntry(void *pvParameters);
|
||||
|
||||
@@ -97,12 +105,32 @@ private:
|
||||
|
||||
SemaphoreHandle_t m_mutex = NULL;
|
||||
temperature_sensor_handle_t m_tempHandle = NULL;
|
||||
// 温度监控 esp_timer 句柄存为成员,便于 deinit() 时停止并删除定时器,
|
||||
// 避免任务被删除后定时器仍在运行、回调继续访问 instance() 成员。
|
||||
esp_timer_handle_t m_tempTimer = nullptr;
|
||||
health_state_t m_healthState = SYS_NORMAL;
|
||||
float m_lastTemperature = 0.0f;
|
||||
int8_t m_lastWifiRssi = -127;
|
||||
int64_t m_lastWifiRssiMs = 0;
|
||||
TaskHandle_t m_taskHandle = NULL;
|
||||
// m_wifiConnected 由 m_mutex 保护(setWifiConnected 写 / checkBeforeOta 读)。
|
||||
// volatile 保留为历史兼容,实际同步由 LockGuard 提供。
|
||||
volatile bool m_wifiConnected = false; ///< WiFi 连接状态(由 WiFi 事件推送)
|
||||
|
||||
static void tempMonitorCallback(void *arg);
|
||||
|
||||
// RAII 锁守卫(与 SpiMaster::LockGuard 风格一致)
|
||||
class LockGuard {
|
||||
public:
|
||||
explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) {
|
||||
if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); }
|
||||
}
|
||||
~LockGuard() {
|
||||
if (m_lock) { xSemaphoreGive(m_lock); }
|
||||
}
|
||||
LockGuard(const LockGuard&) = delete;
|
||||
LockGuard& operator=(const LockGuard&) = delete;
|
||||
private:
|
||||
SemaphoreHandle_t m_lock;
|
||||
};
|
||||
};
|
||||
|
||||
+40
-25
@@ -69,7 +69,7 @@ Esp32OtaCore& Esp32OtaCore::instance()
|
||||
|
||||
/**
|
||||
* @brief 复位所有状态变量。
|
||||
*/
|
||||
*/
|
||||
void Esp32OtaCore::resetState()
|
||||
{
|
||||
m_active = false;
|
||||
@@ -81,6 +81,39 @@ void Esp32OtaCore::resetState()
|
||||
m_ended = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief begin() 失败后的统一清理。
|
||||
* 仅释放 mutex + 复位状态(不调 esp_ota_abort,
|
||||
* begin 失败时 m_handle 未成功创建,无需也不能 abort)。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t Esp32OtaCore::beginCleanupAndReturn(esp_err_t code)
|
||||
{
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief finalize() 失败后的统一清理。
|
||||
* 若 esp_ota_end 未成功(m_ended=false),调 esp_ota_abort 释放 handle;
|
||||
* 然后复位 m_ended、释放 mutex、复位状态,返回 code。
|
||||
* 与现有 abort() 的 m_ended 守卫语义一致,避免 end 成功后误调 abort。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t Esp32OtaCore::finalizeCleanupAndReturn(esp_err_t code)
|
||||
{
|
||||
if (!m_ended) {
|
||||
esp_ota_abort(m_handle);
|
||||
}
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 开启 OTA 会话。
|
||||
* @param total_size 固件总大小(字节)。
|
||||
@@ -125,8 +158,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
|
||||
if (m_partition == NULL) {
|
||||
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
|
||||
releaseMutex();
|
||||
return ESP_FAIL;
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 目标分区: %s, size=%u, address=0x%lx",
|
||||
@@ -135,9 +167,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
if (total_size > m_partition->size) {
|
||||
DEBUG_ERR(TAG, "固件太大: %u > 分区大小: %u",
|
||||
(unsigned)total_size, (unsigned)m_partition->size);
|
||||
m_partition = nullptr;
|
||||
releaseMutex();
|
||||
return ESP_FAIL;
|
||||
return beginCleanupAndReturn(ESP_FAIL);
|
||||
}
|
||||
|
||||
// BLE OTA 修复:传固件实际大小而非分区全尺寸,避免全分区擦除导致
|
||||
@@ -151,10 +181,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
esp_err_t err = esp_ota_begin(m_partition, erase_size, &m_handle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
|
||||
m_partition = nullptr;
|
||||
m_handle = 0;
|
||||
releaseMutex();
|
||||
return err;
|
||||
return beginCleanupAndReturn(err);
|
||||
}
|
||||
|
||||
m_received = 0;
|
||||
@@ -280,11 +307,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
esp_err_t sha_err = computeShaFromPartition();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return sha_err;
|
||||
return finalizeCleanupAndReturn(sha_err);
|
||||
}
|
||||
|
||||
char device_hex[65] = {0};
|
||||
@@ -311,11 +334,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "SHA-256 不匹配: frontend=%s device=%s",
|
||||
frontend_sha_hex, device_hex);
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
return finalizeCleanupAndReturn(ESP_ERR_INVALID_VERSION);
|
||||
}
|
||||
} else if (out_device_sha_hex) {
|
||||
// 未提供前端 SHA,但仍计算设备端 SHA 供输出
|
||||
@@ -332,11 +351,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_end 失败: %s (0x%x)", esp_err_to_name(err), (unsigned)err);
|
||||
// end 失败时仍可 abort(未真正 end)
|
||||
esp_ota_abort(m_handle);
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
resetState();
|
||||
return err;
|
||||
return finalizeCleanupAndReturn(err);
|
||||
}
|
||||
m_ended = true; // ★ end 成功,后续不得再 abort
|
||||
|
||||
|
||||
@@ -130,6 +130,24 @@ private:
|
||||
*/
|
||||
void resetState();
|
||||
|
||||
// begin() 失败后的统一清理:释放 mutex + resetState() + 返回 code
|
||||
// 不调 esp_ota_abort(begin 失败时 m_handle 未成功创建)
|
||||
/**
|
||||
* @brief begin() 失败后的统一清理。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t beginCleanupAndReturn(esp_err_t code);
|
||||
|
||||
// finalize() 失败后的统一清理:若 m_ended=false 调 esp_ota_abort 释放 handle;
|
||||
// 再复位 m_ended + 释放 mutex + resetState() + 返回 code
|
||||
/**
|
||||
* @brief finalize() 失败后的统一清理。
|
||||
* @param code 返回给调用方的错误码。
|
||||
* @return 透传 code。
|
||||
*/
|
||||
esp_err_t finalizeCleanupAndReturn(esp_err_t code);
|
||||
|
||||
bool m_active = false;
|
||||
esp_ota_handle_t m_handle = 0;
|
||||
const esp_partition_t *m_partition = nullptr;
|
||||
|
||||
+216
-88
@@ -124,41 +124,110 @@ void OtaCoordinator::init()
|
||||
// ============================================================
|
||||
// GD32 存储阶段
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size)
|
||||
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
bool h = lock();
|
||||
|
||||
// ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程
|
||||
if (h) watchdogCheckLocked();
|
||||
|
||||
// ★ 同类型重启:若上一次 GD32 存储仍在进行中(STORING)且来自同一通道,
|
||||
// 自动中止旧存储并重新开始。覆盖"上传过程中刷新网页再上传"场景。
|
||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||
// RELAYING 是中继阶段,不可中断,也拒绝。
|
||||
if (h && m_sysState == OTA_SYS_GD32_STORING) {
|
||||
if (m_otaChannel != channel) {
|
||||
DEBUG_WARN(TAG, "GD32 存储中(通道=%d),拒绝跨通道请求(通道=%d)",
|
||||
(int)m_otaChannel, (int)channel);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
DEBUG_WARN(TAG, "GD32 存储仍在进行中(同通道),中止旧存储并重新开始");
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
// epoch 将在下方设置 GD32_STORING 后递增
|
||||
}
|
||||
|
||||
// 系统忙检查:仅拒绝真正"在传输"的活跃状态
|
||||
// STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖
|
||||
if (h && (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING ||
|
||||
m_sysState == OTA_SYS_GD32_STORING ||
|
||||
m_sysState == OTA_SYS_GD32_RELAYING)) {
|
||||
DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),先清理再开始新存储
|
||||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),允许新存储覆盖。
|
||||
// 注意:清理 NVS meta 不在此处做,避免与紧跟的 prepareGd32Storage 内
|
||||
// clearGd32Meta + saveGd32Meta 重复一次写入,仅做状态注释说明。
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) {
|
||||
DEBUG_INFO(TAG, "清理旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
DEBUG_INFO(TAG, "覆盖旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||||
}
|
||||
|
||||
if (h) {
|
||||
m_otaEpoch++; // ★ 新会话开始,递增 epoch
|
||||
m_otaChannel = channel; // ★ 记录发起通道
|
||||
m_sysState = OTA_SYS_GD32_STORING;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING", total_size);
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING (epoch=%u, channel=%d)",
|
||||
total_size, m_otaEpoch, (int)channel);
|
||||
}
|
||||
unlock(h);
|
||||
return h ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// GD32 Flash 扇区大小(与 http_handlers.cpp 中的常量保持一致,用于擦除时向上对齐)
|
||||
static constexpr uint32_t kGd32FlashSectorSize = 4096U;
|
||||
|
||||
esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
// 1. 校验固件大小(统一 BLE/HTTP 两路径的大小检查)
|
||||
esp_err_t err = validateGd32Size(total_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "prepareGd32Storage: 固件大小非法 raw=%" PRIu32, total_size);
|
||||
return err;
|
||||
}
|
||||
|
||||
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
|
||||
err = beginGd32Storage(total_size, channel);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
|
||||
return err;
|
||||
}
|
||||
|
||||
// 3. 擦除 GD32 OTA 分区(按扇区大小向上对齐)
|
||||
// ★ 原 BLE 路径未擦除分区,此处统一补齐,确保两路径行为一致。
|
||||
const uint32_t erase_size =
|
||||
((total_size + kGd32FlashSectorSize - 1U) / kGd32FlashSectorSize) * kGd32FlashSectorSize;
|
||||
DEBUG_INFO(TAG, "prepareGd32Storage: 擦除 gd32_ota 分区 %u bytes (fw=%" PRIu32 ")",
|
||||
(unsigned)erase_size, total_size);
|
||||
err = eraseGd32Partition(erase_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "prepareGd32Storage: 擦除失败: %s", esp_err_to_name(err));
|
||||
abortCurrent();
|
||||
return err;
|
||||
}
|
||||
|
||||
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
|
||||
clearGd32Meta();
|
||||
ota_meta_t meta = {};
|
||||
meta.total_size = total_size;
|
||||
meta.stored_size = 0;
|
||||
meta.last_seq = 0;
|
||||
err = saveGd32Meta(&meta);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "prepareGd32Storage: 元数据保存失败: %s", esp_err_to_name(err));
|
||||
abortCurrent();
|
||||
return err;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void OtaCoordinator::completeGd32Storage()
|
||||
{
|
||||
bool h = lock();
|
||||
@@ -175,47 +244,9 @@ void OtaCoordinator::completeGd32Storage()
|
||||
|
||||
// ============================================================
|
||||
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
|
||||
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
|
||||
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::saveGd32Meta(const ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaSave(meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::loadGd32Meta(ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaLoad(meta);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::clearGd32Meta()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaClear();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32])
|
||||
{
|
||||
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::finalizeGd32Checksum()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaFinalizeChecksum();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::eraseGd32Partition(uint32_t size)
|
||||
{
|
||||
return Gd32FlashStore::instance().erasePartition(size);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GD32 中继阶段
|
||||
@@ -369,25 +400,42 @@ void OtaCoordinator::notifyBleCoexEnd()
|
||||
// ============================================================
|
||||
// ESP32 OTA 阶段
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
|
||||
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
bool h = lock();
|
||||
|
||||
// ★ 卡死自愈检查
|
||||
if (h) watchdogCheckLocked();
|
||||
|
||||
// 系统忙检查:GD32 传输/中继进行中,或另一通道的 ESP32 OTA 进行中,均拒绝。
|
||||
// ★ 必须显式拒绝 ESP32_RECEIVING/FINALIZING/REBOOTING:
|
||||
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
|
||||
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
|
||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||
// FINALIZING/REBOOTING 是不可逆阶段,也拒绝。
|
||||
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||||
if (m_otaChannel != channel) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
|
||||
(int)m_otaChannel, (int)channel);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 仍在接收中(同通道),中止旧会话并重新开始");
|
||||
Esp32OtaCore::instance().abort();
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_busySinceTick = 0;
|
||||
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
|
||||
}
|
||||
|
||||
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 收尾/重启中,均拒绝。
|
||||
// ★ 必须显式拒绝 ESP32_FINALIZING/REBOOTING:
|
||||
// 若放行,本调用会落到 Esp32OtaCore::begin 去抢全局互斥失败而返回 INVALID_STATE,
|
||||
// 随后 beginEsp32OtaWithRetry 的 forceCleanup 会把另一通道的活跃会话
|
||||
// (esp_ota_abort + 释放互斥)误中止 → 两通道并发写同一 OTA 分区。
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
|
||||
m_sysState == OTA_SYS_GD32_RELAYING ||
|
||||
m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
|
||||
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
|
||||
(int)m_sysState, getSystemStateString());
|
||||
(int)m_sysState, stateToString(m_sysState));
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
@@ -397,9 +445,12 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
|
||||
// 委托 Esp32OtaCore 处理分区/互斥逻辑
|
||||
err = Esp32OtaCore::instance().begin(total_size);
|
||||
if (err == ESP_OK) {
|
||||
m_otaEpoch++; // ★ 新会话开始,递增 epoch(使旧 handler 的 abortCurrentIfEpoch 失效)
|
||||
m_otaChannel = channel; // ★ 记录发起通道
|
||||
m_sysState = OTA_SYS_ESP32_RECEIVING;
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING", total_size);
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING (epoch=%u, channel=%d)",
|
||||
total_size, m_otaEpoch, (int)channel);
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
|
||||
}
|
||||
@@ -409,9 +460,9 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
|
||||
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
esp_err_t err = beginEsp32Ota(total_size);
|
||||
esp_err_t err = beginEsp32Ota(total_size, channel);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
|
||||
// 这种情况只可能是"锁残留"——前一次 OTA 异常退出没释放全局互斥,
|
||||
@@ -419,15 +470,17 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
|
||||
// 若系统正处于任何活跃 OTA(ESP32/GD32),绝不强制清理——那会
|
||||
// 把另一通道正在进行的 esp_ota_handle 中止、释放互斥,导致
|
||||
// 两通道并发写同一 OTA 分区(之前 BLE 与 WiFi 可同时 OTA 的根因)。
|
||||
if (!isIdle()) {
|
||||
// 避免连续 isIdle() + getSystemState() 各加锁一次(共 2 次锁)。
|
||||
ota_system_state_t cur = getSystemState();
|
||||
if (!isIdleState(cur)) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
|
||||
(int)getSystemState());
|
||||
(int)cur);
|
||||
return err;
|
||||
}
|
||||
// 协调器空闲但 Esp32OtaCore.begin 仍被拒 → 锁残留,强制清理后重试
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝但系统空闲,判定为锁残留,强制清理状态机后重试");
|
||||
Esp32OtaCore::instance().forceCleanup();
|
||||
err = beginEsp32Ota(total_size);
|
||||
err = beginEsp32Ota(total_size, channel);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
@@ -507,29 +560,60 @@ int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)
|
||||
// ============================================================
|
||||
// 中止
|
||||
// ============================================================
|
||||
void OtaCoordinator::doAbortLocked()
|
||||
{
|
||||
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d, epoch=%u)", (int)m_sysState, m_otaEpoch);
|
||||
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
OtaRelay::instance().reset();
|
||||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
}
|
||||
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0;
|
||||
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
||||
}
|
||||
|
||||
void OtaCoordinator::abortCurrent()
|
||||
{
|
||||
bool h = lock();
|
||||
if (h) {
|
||||
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState);
|
||||
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
OtaRelay::instance().reset();
|
||||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
}
|
||||
|
||||
m_sysState = OTA_SYS_IDLE;
|
||||
m_gd32StorageDone = false;
|
||||
m_busySinceTick = 0; // ★ 离开忙状态
|
||||
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
|
||||
doAbortLocked();
|
||||
}
|
||||
unlock(h);
|
||||
}
|
||||
|
||||
uint32_t OtaCoordinator::getOtaEpoch() const
|
||||
{
|
||||
bool h = lock();
|
||||
uint32_t epoch = m_otaEpoch;
|
||||
unlock(h);
|
||||
return epoch;
|
||||
}
|
||||
|
||||
bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
|
||||
{
|
||||
bool h = lock();
|
||||
if (!h) return false;
|
||||
|
||||
if (m_otaEpoch != epoch) {
|
||||
// ★ epoch 不匹配:新会话已接管,旧 handler 不应中止
|
||||
unlock(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
// epoch 匹配:执行中止(共享 doAbortLocked,已持锁)
|
||||
doAbortLocked();
|
||||
|
||||
unlock(h);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 状态查询
|
||||
// ============================================================
|
||||
@@ -543,35 +627,61 @@ ota_system_state_t OtaCoordinator::getSystemState() const
|
||||
return s;
|
||||
}
|
||||
|
||||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||||
bool OtaCoordinator::isIdle() const
|
||||
ota_channel_t OtaCoordinator::getOtaChannel() const
|
||||
{
|
||||
return (getSystemState() == OTA_SYS_IDLE ||
|
||||
getSystemState() == OTA_SYS_GD32_STORED ||
|
||||
getSystemState() == OTA_SYS_GD32_COMPLETE);
|
||||
bool h = lock();
|
||||
ota_channel_t ch = m_otaChannel;
|
||||
unlock(h);
|
||||
return ch;
|
||||
}
|
||||
|
||||
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
|
||||
bool OtaCoordinator::isGd32Busy() const
|
||||
/** @brief 判断给定状态值是否属于空闲态(不加锁,纯判断)。
|
||||
* 供持锁路径或已取一次状态的调用方复用,避免连续加锁。 */
|
||||
bool OtaCoordinator::isIdleState(ota_system_state_t s) const
|
||||
{
|
||||
return (s == OTA_SYS_IDLE ||
|
||||
s == OTA_SYS_GD32_STORED ||
|
||||
s == OTA_SYS_GD32_COMPLETE);
|
||||
}
|
||||
|
||||
/** @brief 判断给定状态值是否属于 GD32 忙态(不加锁,纯判断)。 */
|
||||
bool OtaCoordinator::isGd32BusyState(ota_system_state_t s) const
|
||||
{
|
||||
ota_system_state_t s = getSystemState();
|
||||
return (s == OTA_SYS_GD32_STORING ||
|
||||
s == OTA_SYS_GD32_RELAYING);
|
||||
}
|
||||
|
||||
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
|
||||
bool OtaCoordinator::isEsp32Busy() const
|
||||
/** @brief 判断给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。 */
|
||||
bool OtaCoordinator::isEsp32BusyState(ota_system_state_t s) const
|
||||
{
|
||||
ota_system_state_t s = getSystemState();
|
||||
return (s == OTA_SYS_ESP32_RECEIVING ||
|
||||
s == OTA_SYS_ESP32_FINALIZING ||
|
||||
s == OTA_SYS_ESP32_REBOOTING);
|
||||
}
|
||||
|
||||
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。 */
|
||||
const char* OtaCoordinator::getSystemStateString() const
|
||||
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
|
||||
bool OtaCoordinator::isIdle() const
|
||||
{
|
||||
switch (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 把状态枚举转成可读字符串(不加锁,纯函数)。
|
||||
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。 */
|
||||
const char* OtaCoordinator::stateToString(ota_system_state_t s)
|
||||
{
|
||||
switch (s) {
|
||||
case OTA_SYS_IDLE: return "IDLE";
|
||||
case OTA_SYS_GD32_STORING: return "GD32_STORING";
|
||||
case OTA_SYS_GD32_STORED: return "GD32_STORED";
|
||||
@@ -585,6 +695,13 @@ const char* OtaCoordinator::getSystemStateString() const
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。
|
||||
* 委托 stateToString(getSystemState()),保持向后兼容。 */
|
||||
const char* OtaCoordinator::getSystemStateString() const
|
||||
{
|
||||
return stateToString(getSystemState());
|
||||
}
|
||||
|
||||
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
|
||||
bool OtaCoordinator::getGd32AppAlive() const
|
||||
{
|
||||
@@ -608,14 +725,22 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
||||
{
|
||||
OtaSnapshot snap{};
|
||||
|
||||
// ── coordinator 自身状态(一次 mutex)──
|
||||
// ── coordinator 自身状态(一次 mutex,避免二次加锁导致快照不一致)──
|
||||
// 持锁时一次性取 m_sysState,并调不加锁的 stateToString()
|
||||
// 就地转字符串。旧实现释放锁后再调 getSystemStateString() 二次加锁,
|
||||
// 两次取值之间状态可能变化,导致 snap.sys_state 与 snap.sys_state_str
|
||||
// 描述不同时刻状态。
|
||||
bool h = lock();
|
||||
if (h) {
|
||||
snap.sys_state = m_sysState;
|
||||
snap.storage_done = m_gd32StorageDone;
|
||||
// 就地把状态枚举转字符串,与 sys_state 来自同一时刻
|
||||
snap.sys_state_str = stateToString(snap.sys_state);
|
||||
unlock(h);
|
||||
} else {
|
||||
// 加锁失败时也填一个与默认 sys_state(=IDLE) 一致的字符串
|
||||
snap.sys_state_str = stateToString(snap.sys_state);
|
||||
}
|
||||
snap.sys_state_str = getSystemStateString();
|
||||
|
||||
// ── GD32 NVS 元数据 ──
|
||||
Gd32FlashStore::instance().metaLoad(&snap.meta);
|
||||
@@ -629,6 +754,9 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
||||
// ============================================================
|
||||
// GD32 APP 探测(PR2: 从 /system/status 移出)
|
||||
// ============================================================
|
||||
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
|
||||
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
|
||||
// 故此处保留独立实现。
|
||||
esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime)
|
||||
{
|
||||
uint32_t uptime = 0;
|
||||
|
||||
+155
-12
@@ -19,6 +19,7 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_err.h"
|
||||
#include "app_config.h" // OTA_MAX_FIRMWARE_SIZE / GD32_OTA_PACKET_SIZE(validateGd32Size 用)
|
||||
#include "storage/gd32_flash_store.h"
|
||||
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot(H4 解耦)
|
||||
|
||||
@@ -37,6 +38,14 @@ typedef enum {
|
||||
OTA_SYS_ERROR // 错误状态
|
||||
} ota_system_state_t;
|
||||
|
||||
// ============================================================
|
||||
// OTA 传输通道标识(用于同类型重启时区分同通道/跨通道)
|
||||
// ============================================================
|
||||
typedef enum {
|
||||
OTA_CHANNEL_WIFI = 0,
|
||||
OTA_CHANNEL_BLE = 1,
|
||||
} ota_channel_t;
|
||||
|
||||
// ============================================================
|
||||
// OTA 系统全量只读快照(PR1: 收口状态读取)
|
||||
// ============================================================
|
||||
@@ -108,9 +117,21 @@ public:
|
||||
* @brief 传输层开始接收 GD32 固件前调用。
|
||||
* 若系统正忙(ESP32 OTA 进行中或 GD32 中继中),返回失败。
|
||||
* @param total_size 固件总字节数。
|
||||
* @param channel 发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
|
||||
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙。
|
||||
*/
|
||||
esp_err_t beginGd32Storage(uint32_t total_size);
|
||||
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief 统一 GD32 OTA 写入前的准备工作:校验大小 + begin + 擦除分区 + 清空 meta + 写入初始 meta。
|
||||
* 供 BLE seq=0 首包 / HTTP GD32 upload(非 prepared 复用路径)调用,
|
||||
* 消除两路径对"擦除/meta 初始化"序列的隐式差异(原 BLE 路径未擦除分区)。
|
||||
* 任意步骤失败时内部调 abortCurrent 清理,调用方无需再清理。
|
||||
* @param total_size 固件总字节数。
|
||||
* @param channel 发起通道(WiFi/BLE)。
|
||||
* @return ESP_OK 成功;ESP_ERR_INVALID_SIZE 大小非法;ESP_ERR_INVALID_STATE 系统忙;其他擦除/写入错误。
|
||||
*/
|
||||
esp_err_t prepareGd32Storage(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief GD32 固件接收完毕时调用。
|
||||
@@ -127,21 +148,30 @@ public:
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
inline esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 保存 GD32 OTA 元数据。
|
||||
* @param meta 元数据结构体。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t saveGd32Meta(const ota_meta_t *meta);
|
||||
inline esp_err_t saveGd32Meta(const ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaSave(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载 GD32 OTA 元数据。
|
||||
* @param meta 输出元数据结构体。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t loadGd32Meta(ota_meta_t *meta);
|
||||
inline esp_err_t loadGd32Meta(ota_meta_t *meta)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaLoad(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 更新 GD32 OTA 元数据的进度。
|
||||
@@ -149,13 +179,19 @@ public:
|
||||
* @param last_seq 最后包序号。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq);
|
||||
inline esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除 GD32 OTA 元数据。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t clearGd32Meta();
|
||||
inline esp_err_t clearGd32Meta()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaClear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 校验前端 SHA-256 与设备端固件是否一致。
|
||||
@@ -163,21 +199,52 @@ public:
|
||||
* @param out_device_sha 输出设备端 32 字节 SHA(可为 nullptr)。
|
||||
* @return ESP_OK 匹配,ESP_ERR_INVALID_CRC 不匹配。
|
||||
*/
|
||||
esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32]);
|
||||
inline esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
|
||||
uint8_t out_device_sha[32])
|
||||
{
|
||||
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 计算 GD32 固件 SHA-256 并写入元数据。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t finalizeGd32Checksum();
|
||||
inline esp_err_t finalizeGd32Checksum()
|
||||
{
|
||||
return Gd32FlashStore::instance().metaFinalizeChecksum();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 擦除 GD32 OTA 分区。
|
||||
* @param size 擦除大小。
|
||||
* @return ESP_OK 成功。
|
||||
*/
|
||||
esp_err_t eraseGd32Partition(uint32_t size);
|
||||
inline esp_err_t eraseGd32Partition(uint32_t size)
|
||||
{
|
||||
return Gd32FlashStore::instance().erasePartition(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 校验 GD32 固件大小是否合法(BLE/HTTP 双通道共用)。
|
||||
*
|
||||
* 统一规则:raw==0 或 raw>MAX 拒绝;按 GD32_OTA_PACKET_SIZE 向上取整后仍>MAX 拒绝。
|
||||
* 调用方各自翻译为通道特定错误码(BLE: BLE_OTA_FINISH_ERR_GD32_SIZE;HTTP: HTTPD_500)。
|
||||
*
|
||||
* @param raw_size 固件原始字节数。
|
||||
* @return ESP_OK 合法;ESP_ERR_INVALID_SIZE 非法。
|
||||
*/
|
||||
inline esp_err_t validateGd32Size(uint32_t raw_size) const
|
||||
{
|
||||
if (raw_size == 0U || raw_size > (uint32_t)OTA_MAX_FIRMWARE_SIZE) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
const uint32_t packet_size = (uint32_t)GD32_OTA_PACKET_SIZE;
|
||||
const uint32_t rounded = ((raw_size + packet_size - 1U) / packet_size) * packet_size;
|
||||
if (rounded > (uint32_t)OTA_MAX_FIRMWARE_SIZE) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ── GD32 中继阶段 ──
|
||||
/**
|
||||
@@ -205,17 +272,19 @@ public:
|
||||
* @brief ESP32 OTA 开始接收固件。
|
||||
* 内部委托 Esp32OtaCore::begin()。
|
||||
* @param total_size 固件总字节数。
|
||||
* @param channel 发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
|
||||
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙或通道被占用。
|
||||
*/
|
||||
esp_err_t beginEsp32Ota(uint32_t total_size);
|
||||
esp_err_t beginEsp32Ota(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief 发起 ESP32 OTA,若被 ESP_ERR_INVALID_STATE 拒绝则 forceCleanup 后重试一次。
|
||||
* 封装 BLE/WiFi 两端完全相同的重试模式,调用方只需调一次。
|
||||
* @param total_size 固件总字节数。
|
||||
* @param channel 发起通道(WiFi/BLE)。
|
||||
* @return ESP_OK 成功;其他 = 协调器拒绝或 Esp32OtaCore::begin 失败。
|
||||
*/
|
||||
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size);
|
||||
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel);
|
||||
|
||||
/**
|
||||
* @brief ESP32 OTA 固件接收完毕,准备收尾。
|
||||
@@ -257,6 +326,25 @@ public:
|
||||
*/
|
||||
void abortCurrent();
|
||||
|
||||
/**
|
||||
* @brief 获取当前 OTA 会话的 epoch(代次)。
|
||||
* 每次 beginEsp32Ota/beginGd32Storage 成功时递增。
|
||||
* 传输层 handler 在 begin 成功后捕获 epoch,
|
||||
* 在失败清理时通过 abortCurrentIfEpoch() 检查是否仍为当前会话,
|
||||
* 避免旧 handler 的清理代码误中止新会话(同类型重启场景)。
|
||||
* @return 当前 epoch 值。
|
||||
*/
|
||||
uint32_t getOtaEpoch() const;
|
||||
|
||||
/**
|
||||
* @brief 仅当 epoch 匹配当前会话时才中止 OTA。
|
||||
* 用于旧 handler 的失败清理路径:若新请求已经接管(epoch 不匹配),
|
||||
* 旧 handler 不应调 abortCurrent,否则会误中止新会话。
|
||||
* @param epoch 调用方在 begin 成功时捕获的 epoch。
|
||||
* @return true 已中止(epoch 匹配),false 未中止(新会话已接管)。
|
||||
*/
|
||||
bool abortCurrentIfEpoch(uint32_t epoch);
|
||||
|
||||
// ── 状态查询 ──
|
||||
/**
|
||||
* @brief 一次性读取系统 OTA 全量状态快照(PR1: 收口状态读取)。
|
||||
@@ -282,26 +370,68 @@ public:
|
||||
*/
|
||||
ota_system_state_t getSystemState() const;
|
||||
|
||||
/**
|
||||
* @brief 获取当前 OTA 会话的发起通道(WiFi/BLE)。
|
||||
* 仅在活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义;
|
||||
* IDLE 等非活跃状态返回值未定义(默认 OTA_CHANNEL_WIFI)。
|
||||
* @return 当前会话通道枚举值。
|
||||
*/
|
||||
ota_channel_t getOtaChannel() const;
|
||||
|
||||
/**
|
||||
* @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
|
||||
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。
|
||||
* @param s 状态枚举值。
|
||||
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
|
||||
*/
|
||||
static const char* stateToString(ota_system_state_t s);
|
||||
|
||||
/**
|
||||
* @brief 检查给定状态值是否属于空闲态(不加锁,纯判断)。
|
||||
* 调用方先 getSystemState() 取一次再传入,避免连续加锁。
|
||||
* @param s 状态枚举值。
|
||||
* @return true 表示 IDLE/STORED/COMPLETE。
|
||||
*/
|
||||
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 请求)。
|
||||
* 内部仍加锁取一次状态后调 isIdleState()。
|
||||
* @return true 系统空闲(IDLE/STORED/COMPLETE)。
|
||||
*/
|
||||
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;
|
||||
@@ -385,6 +515,10 @@ private:
|
||||
// 必须在已持锁状态下调用。
|
||||
void watchdogCheckLocked();
|
||||
|
||||
// ★ 已持锁版本的中止序列:根据当前 m_sysState 调用对应子模块的清理,
|
||||
// 并把状态机复位为 IDLE。abortCurrent / abortCurrentIfEpoch 共享此实现。
|
||||
void doAbortLocked();
|
||||
|
||||
// 系统状态
|
||||
ota_system_state_t m_sysState = OTA_SYS_IDLE;
|
||||
|
||||
@@ -394,6 +528,15 @@ private:
|
||||
// GD32 存储跟踪
|
||||
bool m_gd32StorageDone = false;
|
||||
|
||||
// OTA 会话代次:每次 beginEsp32Ota/beginGd32Storage 成功时递增。
|
||||
// 传输层 handler 在 begin 后捕获 epoch,失败清理时通过 abortCurrentIfEpoch
|
||||
// 检查 epoch 是否仍匹配,避免旧 handler 误中止同类型重启后的新会话。
|
||||
uint32_t m_otaEpoch = 0;
|
||||
|
||||
// 当前 OTA 会话的发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
|
||||
// 仅在同类型活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义。
|
||||
ota_channel_t m_otaChannel = OTA_CHANNEL_WIFI;
|
||||
|
||||
// 互斥锁(M1/L3: mutable 允许在 const 方法中加锁,消除 const_cast)
|
||||
mutable SemaphoreHandle_t m_mutex = nullptr;
|
||||
|
||||
|
||||
+301
-252
@@ -27,6 +27,30 @@
|
||||
// ★ H4 解耦:OtaRelay 不再直接 #include "ota_coordinator.h",
|
||||
// 通过 IRelayListener 接口回调通知协调器,依赖方向变为单向。
|
||||
|
||||
// ============================================================
|
||||
// 公共拒绝事件判断(文件内工具)
|
||||
// ============================================================
|
||||
// 统一各状态 handler 头部的 ABORT/TIMEOUT/HEALTH_CRIT 拒绝判断。
|
||||
// 返回 OTA_RESULT_OK 表示非拒绝事件;其他为对应错误码。
|
||||
// allowHealth: 是否将 RELAY_EVT_HEALTH_CRIT 视为拒绝(仅 TRANSFERRING 传 true)。
|
||||
static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
|
||||
{
|
||||
if (e == RELAY_EVT_ABORT) return OTA_RESULT_ERR_USER_ABORT;
|
||||
if (e == RELAY_EVT_TIMEOUT) return OTA_RESULT_ERR_TIMEOUT;
|
||||
if (allowHealth && e == RELAY_EVT_HEALTH_CRIT) return OTA_RESULT_ERR_HEALTH;
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// doRelayPackets 重试阈值(与历史代码字面值保持一致,仅命名化)
|
||||
// ============================================================
|
||||
// 原代码中 retry_count 阈值:行 1015/1075/1084 用 10U,行 1032/1050 用 3U。
|
||||
// 含义:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
|
||||
// 结构化错误属于"GD32 报告具体错误",较少重试(3次)。
|
||||
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
|
||||
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
||||
static constexpr uint32_t kLastPacketAckSeq = 0xF0F0F0F0U; // 末包 ACK 序号约定
|
||||
|
||||
// ============================================================
|
||||
// 静态成员定义
|
||||
// ============================================================
|
||||
@@ -177,7 +201,7 @@ TaskHandle_t OtaRelay::getTaskHandle()
|
||||
// ============================================================
|
||||
ota_result_t OtaRelay::processEvent(relay_event_t event, const uint8_t *payload, size_t len)
|
||||
{
|
||||
relay_state_t cur = relayState;
|
||||
relay_state_t cur = getState();
|
||||
|
||||
// ERROR 状态:仅接受 reset(通过 handleError 的 EVT_START → IDLE)
|
||||
if (cur == RELAY_ERROR) {
|
||||
@@ -237,16 +261,11 @@ ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *pay
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKING 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKING 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "HANDSHAKING 超时");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
return rej;
|
||||
}
|
||||
|
||||
if (event != RELAY_EVT_START && event != RELAY_EVT_RESP_OK) {
|
||||
@@ -291,16 +310,11 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "SIZE_NEG 超时");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
return rej;
|
||||
}
|
||||
|
||||
// 加载元数据获取固件大小
|
||||
@@ -324,8 +338,8 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
|
||||
m_transferCurrentSeq = 0;
|
||||
m_transferFlashOffset = 0;
|
||||
m_transferRetryCount = 0;
|
||||
relayTotalPackets = m_transferTotalPackets;
|
||||
relaySeq = 0;
|
||||
setRelayTotalPackets(m_transferTotalPackets);
|
||||
setRelaySeq(0);
|
||||
|
||||
setState(RELAY_TRANSFERRING);
|
||||
DEBUG_INFO("OTA", "=== 阶段2 完成: 总包数=%" PRIu32 ", → TRANSFERRING ===", m_transferTotalPackets);
|
||||
@@ -340,16 +354,11 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "TRANSFERRING 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event, /*allowHealth=*/true);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "TRANSFERRING 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_HEALTH_CRIT) {
|
||||
DEBUG_ERR("OTA", "TRANSFERRING 健康检查危险,中止");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_HEALTH;
|
||||
return rej;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets);
|
||||
@@ -373,16 +382,11 @@ ota_result_t OtaRelay::handleFinishing(relay_event_t event, const uint8_t *paylo
|
||||
(void)payload;
|
||||
(void)len;
|
||||
|
||||
if (event == RELAY_EVT_ABORT) {
|
||||
DEBUG_ERR("OTA", "FINISHING 收到 ABORT");
|
||||
ota_result_t rej = commonReject(event);
|
||||
if (rej != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "FINISHING 拒绝事件: %d", (int)event);
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
if (event == RELAY_EVT_TIMEOUT) {
|
||||
DEBUG_ERR("OTA", "FINISHING 超时");
|
||||
setState(RELAY_ERROR);
|
||||
return OTA_RESULT_ERR_TIMEOUT;
|
||||
return rej;
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段4-5: 结束OTA + APP验证 ===");
|
||||
@@ -469,31 +473,21 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
global_timeout_ms, (uint32_t)start_tick, (uint32_t)m_transferDeadline);
|
||||
|
||||
// ── 驱动状态机 ──
|
||||
// 步骤0: IDLE → HANDSHAKING(handleIdle 内部 reset() + setState(HANDSHAKING))
|
||||
ota_result_t res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
// 5 步顺序推进:IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
|
||||
// 每步同步执行对应 handler(内部完成协议交互并推进状态)。
|
||||
// 任一步返回非 OK 即跳出并复位。
|
||||
ota_result_t res = OTA_RESULT_OK;
|
||||
while (true) {
|
||||
relay_state_t cur = getState();
|
||||
if (cur == RELAY_DONE || cur == RELAY_ERROR) break;
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤1: HANDSHAKING(内部完成握手+认证 → SIZE_NEG)
|
||||
// ★ 注意:processEvent(START) 在 HANDSHAKING handler 内同步执行 doHandshakeReq + doChallengeAndAuth
|
||||
// 期间 SPI 阻塞轮询 8~30 秒。超时检查必须放在同步 handler 执行之后。
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
// 步骤2: SIZE_NEG(内部完成大小协商 → TRANSFERRING)
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
// 步骤3: TRANSFERRING(内部完成所有数据包 → FINISHING)
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
// 步骤4: FINISHING(结束OTA + APP验证 → DONE)
|
||||
res = processEvent(RELAY_EVT_START, nullptr, 0);
|
||||
if (res != OTA_RESULT_OK) goto cleanup;
|
||||
|
||||
cleanup:
|
||||
if (res != OTA_RESULT_OK) {
|
||||
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)relayState);
|
||||
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
|
||||
reset();
|
||||
}
|
||||
return res;
|
||||
@@ -951,6 +945,158 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
|
||||
* 流程:从 Flash 读取 → AES 加密 → SPI 发送 → 等待 ACK → 失败则重发。
|
||||
* 每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包执行一次健康检查。
|
||||
*/
|
||||
ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, uint8_t tx_buf[64], size_t *out_chunk_size)
|
||||
{
|
||||
uint8_t plain48[GD32_OTA_PACKET_SIZE];
|
||||
memset(plain48, 0xFF, sizeof(plain48));
|
||||
size_t chunk_size = GD32_OTA_PACKET_SIZE;
|
||||
if (seq == m_transferTotalPackets - 1) {
|
||||
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
|
||||
}
|
||||
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
|
||||
tx_buf[4] = (uint8_t)(seq & 0xFFU);
|
||||
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
|
||||
tx_buf[6] = 0x00;
|
||||
tx_buf[7] = 0x00;
|
||||
|
||||
if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq);
|
||||
return OTA_RESULT_ERR_AUTH;
|
||||
}
|
||||
|
||||
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
|
||||
tx_buf[56] = (uint8_t)(if_end & 0xFFU);
|
||||
tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU);
|
||||
tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU);
|
||||
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
|
||||
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
|
||||
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
|
||||
seq, (unsigned)chunk_size, if_end,
|
||||
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
|
||||
|
||||
m_transferCurrentSeq = seq;
|
||||
if (out_chunk_size) *out_chunk_size = chunk_size;
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size, ota_result_t *out_result)
|
||||
{
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[32] = {0};
|
||||
size_t resp_len = 0;
|
||||
uint32_t retry_count = 0;
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
const uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? kLastPacketAckSeq : (seq + 1U);
|
||||
|
||||
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
|
||||
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
|
||||
resend_packet:
|
||||
while (1) {
|
||||
resp_len = 0;
|
||||
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, 64,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||
if (err == ESP_OK) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
|
||||
if (++retry_count >= kMaxResendOnNack) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
continue; // 重发本包
|
||||
}
|
||||
|
||||
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
uint32_t err_seq = 0;
|
||||
uint8_t err_code = 0;
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
|
||||
if (err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= kMaxResendOnStructError) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool got_expected = false;
|
||||
for (uint32_t retry = 0; retry < 10; ++retry) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
|
||||
if (++retry_count >= kMaxResendOnStructError) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
got_expected = true;
|
||||
break;
|
||||
}
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (got_expected) {
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= kMaxResendOnNack) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
|
||||
if (++retry_count >= kMaxResendOnNack) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (result != OTA_RESULT_OK) {
|
||||
*out_result = result;
|
||||
return true; // 中止外层循环
|
||||
}
|
||||
return false; // 继续下一包
|
||||
}
|
||||
|
||||
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
{
|
||||
DEBUG_INFO("OTA", "开始转发 %" PRIu32 " 包", m_transferTotalPackets);
|
||||
@@ -962,13 +1108,10 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
}
|
||||
|
||||
uint8_t tx_buf[64] = {0};
|
||||
uint8_t resp_type = 0;
|
||||
uint8_t resp_payload[32] = {0};
|
||||
size_t resp_len = 0;
|
||||
ota_result_t result = OTA_RESULT_OK;
|
||||
|
||||
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
|
||||
uint32_t retry_count = 0;
|
||||
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
|
||||
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == m_transferTotalPackets - 1) {
|
||||
health_status_t health = HealthMonitor::instance().checkBeforeOtaRelay();
|
||||
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
|
||||
@@ -977,150 +1120,32 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
}
|
||||
}
|
||||
|
||||
// 全局超时检查
|
||||
if (deadline_tick != 0 && xTaskGetTickCount() >= deadline_tick) {
|
||||
DEBUG_ERR("OTA", "全局超时: 包 %" PRIu32 "/%" PRIu32 " 已超限,中止转发", seq, m_transferTotalPackets);
|
||||
return OTA_RESULT_ERR_USER_ABORT;
|
||||
}
|
||||
|
||||
uint8_t plain48[GD32_OTA_PACKET_SIZE];
|
||||
memset(plain48, 0xFF, sizeof(plain48));
|
||||
size_t chunk_size = GD32_OTA_PACKET_SIZE;
|
||||
if (seq == m_transferTotalPackets - 1) {
|
||||
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
|
||||
}
|
||||
|
||||
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
|
||||
return OTA_RESULT_ERR_STORAGE;
|
||||
}
|
||||
|
||||
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
|
||||
tx_buf[4] = (uint8_t)(seq & 0xFFU);
|
||||
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
|
||||
tx_buf[6] = 0x00;
|
||||
tx_buf[7] = 0x00;
|
||||
|
||||
if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq);
|
||||
return OTA_RESULT_ERR_AUTH;
|
||||
}
|
||||
|
||||
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
|
||||
tx_buf[56] = (uint8_t)(if_end & 0xFFU);
|
||||
tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU);
|
||||
tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU);
|
||||
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
|
||||
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
|
||||
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
|
||||
seq, (unsigned)chunk_size, if_end,
|
||||
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
|
||||
|
||||
m_transferCurrentSeq = seq;
|
||||
|
||||
resend_packet:
|
||||
while (1) {
|
||||
resp_len = 0;
|
||||
err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf),
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
|
||||
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||
if (err == ESP_OK) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
|
||||
if (++retry_count >= 10U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_TIMEOUT;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
continue; // 重发本包
|
||||
}
|
||||
|
||||
uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? 0xF0F0F0F0U : (seq + 1U);
|
||||
|
||||
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
uint32_t err_seq = 0;
|
||||
uint8_t err_code = 0;
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
|
||||
if (err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= 3U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool got_expected = false;
|
||||
for (uint32_t retry = 0; retry < 10; ++retry) {
|
||||
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
|
||||
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
|
||||
}
|
||||
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
|
||||
if (++retry_count >= 3U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
|
||||
got_expected = true;
|
||||
break;
|
||||
}
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (got_expected) {
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
|
||||
if (++retry_count >= 10U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
|
||||
if (++retry_count >= 10U) {
|
||||
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
|
||||
result = OTA_RESULT_ERR_GD32;
|
||||
break;
|
||||
}
|
||||
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
goto resend_packet;
|
||||
}
|
||||
|
||||
m_transferFlashOffset += (uint32_t)chunk_size;
|
||||
break;
|
||||
}
|
||||
|
||||
// 准备单包:读 Flash + 加密 + 组装
|
||||
size_t chunk_size = 0;
|
||||
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
|
||||
if (result != OTA_RESULT_OK) {
|
||||
break;
|
||||
return result;
|
||||
}
|
||||
|
||||
relaySeq = seq + 1;
|
||||
if (seq % (m_transferTotalPackets / 10 + 1) == 0) {
|
||||
uint32_t progress = (relaySeq * 100U) / m_transferTotalPackets;
|
||||
DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", relaySeq, m_transferTotalPackets, progress);
|
||||
// 发送单包并等待 ACK(内含重试循环)
|
||||
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
|
||||
return result; // 中止
|
||||
}
|
||||
|
||||
setRelaySeq(seq + 1);
|
||||
// 每 5% 或前 5 包/最后 5 包打印进度,避免 9 秒+静默期无法判断是否卡死
|
||||
uint32_t step = m_transferTotalPackets / 20 + 1; // ~5% 间隔
|
||||
if (seq < 5 || seq >= m_transferTotalPackets - 5 || seq % step == 0) {
|
||||
// 进度日志直接用本地 seq+1,避免再次加锁读 relaySeq
|
||||
uint32_t done = seq + 1;
|
||||
uint32_t progress = (done * 100U) / m_transferTotalPackets;
|
||||
DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", done, m_transferTotalPackets, progress);
|
||||
}
|
||||
|
||||
// ★ 包间延迟:给 GD32 Bootloader 充足时间完成 flash 写入并准备下一包 ACK。
|
||||
@@ -1132,8 +1157,36 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA 指令并验证 GD32 APP 存活。 */
|
||||
ota_result_t OtaRelay::doFinishAndVerify()
|
||||
// ============================================================
|
||||
// ============================================================
|
||||
// 统一 OtaRelay 内部两处 APP 探测逻辑(doWaitForAppAlive 轮询 + taskEntry 启动探测)。
|
||||
// 成功:setGd32AppAlive(true) + setGd32Version + 打印版本号;失败:打印 warning。
|
||||
// 注:OtaCoordinator::probeGd32AppOnce 逻辑相似,但因依赖方向不调用本函数。
|
||||
bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
|
||||
{
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ver = {};
|
||||
|
||||
esp_err_t err = SpiMaster::instance().appPing(timeout_ms, &uptime, &ver);
|
||||
if (err == ESP_OK) {
|
||||
// 持锁一次性写入两个状态字段,保证 getSnapshot() 读取一致
|
||||
StateLockGuard lock;
|
||||
gd32AppAlive = true;
|
||||
gd32Version = ver;
|
||||
if (out_uptime) *out_uptime = uptime;
|
||||
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
uptime,
|
||||
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build,
|
||||
ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build);
|
||||
return true;
|
||||
}
|
||||
if (out_uptime) *out_uptime = 0;
|
||||
DEBUG_WARN("OTA", "GD32 APP 探测失败: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试)。 */
|
||||
ota_result_t OtaRelay::doSendFinishOta()
|
||||
{
|
||||
DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP");
|
||||
|
||||
@@ -1193,6 +1246,43 @@ ota_result_t OtaRelay::doFinishAndVerify()
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:轮询等待 GD32 APP 存活(30 次重试)。 */
|
||||
ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
|
||||
{
|
||||
// ★ APP 存活验证
|
||||
setGd32AppAlive(false);
|
||||
|
||||
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
if (probeAndCacheAppAlive(timeout_ms)) {
|
||||
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
||||
break;
|
||||
}
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!getGd32AppAlive()) {
|
||||
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA + 等待 APP 存活(顺序两阶段 + 会话清理)。 */
|
||||
ota_result_t OtaRelay::doFinishAndVerify()
|
||||
{
|
||||
// 阶段1: 发送 FINISH_OTA 并等待 ACK
|
||||
ota_result_t res = doSendFinishOta();
|
||||
if (res != OTA_RESULT_OK) {
|
||||
return res;
|
||||
}
|
||||
|
||||
// 会话密钥/上下文清理
|
||||
Protocol::sessionKeyClear(&m_sessionCtx);
|
||||
memset(&m_sessionCtx, 0, sizeof(session_ctx_t));
|
||||
memset(m_kSession, 0, sizeof(m_kSession));
|
||||
@@ -1203,34 +1293,8 @@ ota_result_t OtaRelay::doFinishAndVerify()
|
||||
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
|
||||
// ★ APP 存活验证
|
||||
gd32AppAlive = false;
|
||||
|
||||
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ping_ver = {};
|
||||
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime, &ping_ver);
|
||||
if (ping_err == ESP_OK) {
|
||||
gd32AppAlive = true;
|
||||
gd32Version = ping_ver;
|
||||
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
uptime,
|
||||
ping_ver.app_major, ping_ver.app_minor, ping_ver.app_patch, ping_ver.app_build,
|
||||
ping_ver.boot_major, ping_ver.boot_minor, ping_ver.boot_patch, ping_ver.boot_build);
|
||||
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
||||
break;
|
||||
}
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!gd32AppAlive) {
|
||||
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
|
||||
reset();
|
||||
return OTA_RESULT_ERR_GD32;
|
||||
}
|
||||
|
||||
return OTA_RESULT_OK;
|
||||
// 阶段2: APP 存活轮询(单次 appPing 超时 1000ms,共 30 次)
|
||||
return doWaitForAppAlive(1000);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1258,24 +1322,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
{
|
||||
DEBUG_INFO("OTA", "启动初始 GD32 探测...");
|
||||
vTaskDelay(pdMS_TO_TICKS(3000)); // 等待 GD32 APP 就绪
|
||||
uint32_t uptime = 0;
|
||||
gd32_version_t ping_ver = {};
|
||||
esp_err_t ping_err = SpiMaster::instance().appPing(2000, &uptime, &ping_ver);
|
||||
if (ping_err == ESP_OK) {
|
||||
StateLockGuard lock;
|
||||
gd32AppAlive = true;
|
||||
gd32Version = ping_ver;
|
||||
DEBUG_INFO("OTA", "启动 GD32 探测成功: uptime=%" PRIu32 "s, "
|
||||
"APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||
uptime,
|
||||
ping_ver.app_major, ping_ver.app_minor,
|
||||
ping_ver.app_patch, ping_ver.app_build,
|
||||
ping_ver.boot_major, ping_ver.boot_minor,
|
||||
ping_ver.boot_patch, ping_ver.boot_build);
|
||||
} else {
|
||||
DEBUG_WARN("OTA", "启动 GD32 探测失败: %s (GD32 可能未就绪)",
|
||||
esp_err_to_name(ping_err));
|
||||
}
|
||||
probeAndCacheAppAlive(2000);
|
||||
}
|
||||
|
||||
while (1) {
|
||||
@@ -1287,7 +1334,8 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
|
||||
if (s_relayListener) s_relayListener->onRelayStarted();
|
||||
IRelayListener *listener = getRelayListener();
|
||||
if (listener) listener->onRelayStarted();
|
||||
ota_result_t result = instance().forwardFirmware();
|
||||
|
||||
uint32_t result_msg = (uint32_t)result;
|
||||
@@ -1303,6 +1351,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
}
|
||||
instance().reset();
|
||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
|
||||
if (s_relayListener) s_relayListener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
listener = getRelayListener();
|
||||
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
|
||||
}
|
||||
}
|
||||
|
||||
+63
-13
@@ -224,21 +224,12 @@ public:
|
||||
|
||||
// ── Flash 操作已迁移至 Gd32FlashStore(storage/gd32_flash_store.h)──
|
||||
|
||||
// ── 公开状态成员 ──
|
||||
static relay_state_t relayState; ///< 当前状态机状态
|
||||
static uint32_t relaySeq; ///< 当前中继会话序号
|
||||
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
||||
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
||||
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
||||
static TaskHandle_t taskHandle; ///< OTA 中继任务句柄
|
||||
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
|
||||
|
||||
// ── 线程安全锁 ──
|
||||
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁
|
||||
static void initMutex(); ///< 初始化状态互斥锁(app_main 中调用)
|
||||
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
||||
|
||||
/** @brief RAII 状态锁守护类,自动获取/释放 s_stateMutex。 */
|
||||
/** @brief RAII 状态锁守护类,自动获取/释放 s_stateMutex。
|
||||
* 作为 public 嵌套类保留,内部代码在持锁块内可直接访问 private 状态成员。 */
|
||||
class StateLockGuard {
|
||||
public:
|
||||
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
|
||||
@@ -268,6 +259,23 @@ private:
|
||||
static QueueHandle_t dispatchQueue; ///< 事件分发队列
|
||||
static QueueHandle_t resultQueue; ///< 结果回传队列
|
||||
|
||||
// ── 状态成员(全部改 private,统一通过 public setter/getter 访问)──
|
||||
// 外部代码使用 getState()/getRelaySeq()/... 读取,setState()/setRelaySeq()/... 写入。
|
||||
// 内部代码:若已在 StateLockGuard 块内(reset/getSnapshot/启动探测),可直接访问;
|
||||
// 否则必须使用 setter/getter,禁止裸读写(消除数据竞争)。
|
||||
static relay_state_t relayState; ///< 当前状态机状态
|
||||
static uint32_t relaySeq; ///< 当前中继会话序号
|
||||
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
||||
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
||||
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
||||
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
|
||||
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁(由 StateLockGuard 使用)
|
||||
|
||||
// taskHandle 为 write-once-at-init 字段:仅 taskEntry 启动首行写入一次,
|
||||
// 之后只读。getTaskHandle() 无锁返回,因写入发生在任务创建后、任务循环开始前,
|
||||
// 与任何外部读取者构成 happens-before(任务创建同步),无数据竞争。
|
||||
static TaskHandle_t taskHandle; ///< OTA 中继任务句柄
|
||||
|
||||
OtaRelay() = default;
|
||||
~OtaRelay() = default;
|
||||
OtaRelay(const OtaRelay&) = delete;
|
||||
@@ -328,10 +336,44 @@ private:
|
||||
*/
|
||||
ota_result_t doRelayPackets(TickType_t deadline_tick);
|
||||
/**
|
||||
* @brief 发送结束指令并验证 APP 存活.
|
||||
* @brief 准备单包:读 Flash + 组装 tx_buf + 加密 + CRC(doRelayPackets 子步骤)。
|
||||
* @param seq 当前包序号。
|
||||
* @param meta 固件元数据(用于计算末包 chunk_size)。
|
||||
* @param tx_buf 输出缓冲区(64 字节)。
|
||||
* @param out_chunk_size 输出本包有效字节数(用于 sendRelayPacketWithRetry 推进 offset)。
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_STORAGE 读 Flash 失败,OTA_RESULT_ERR_AUTH 加密失败。
|
||||
*/
|
||||
ota_result_t prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, uint8_t tx_buf[64], size_t *out_chunk_size);
|
||||
/**
|
||||
* @brief 发送单包并等待 ACK(doRelayPackets 子步骤)。
|
||||
*
|
||||
* 内含重试循环,使用以下命名常量(与历史阈值保持一致):
|
||||
* kMaxResendOnNack=10(ACK 失败/轮询耗尽重发上限)
|
||||
* kMaxResendOnStructError=3(结构化错误重发上限)
|
||||
*
|
||||
* @param seq 当前包序号。
|
||||
* @param tx_buf 已组装好的 64 字节发送缓冲区。
|
||||
* @param chunk_size 本包有效字节数(成功时用于推进 m_transferFlashOffset)。
|
||||
* @param[out] out_result 失败时写入错误码(OK 时不修改)。
|
||||
* @return true 应中止外层循环(result 已设);false 继续下一包。
|
||||
*/
|
||||
bool sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size, ota_result_t *out_result);
|
||||
/**
|
||||
* @brief 发送结束指令并验证 APP 存活(顺序调用 doSendFinishOta + doWaitForAppAlive).
|
||||
* @return OTA_RESULT_OK 成功,负值表示错误。
|
||||
*/
|
||||
ota_result_t doFinishAndVerify();
|
||||
/**
|
||||
* @brief 发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试).
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_GD32 失败。
|
||||
*/
|
||||
ota_result_t doSendFinishOta();
|
||||
/**
|
||||
* @brief 轮询等待 GD32 APP 存活(30 次重试).
|
||||
* @param timeout_ms 单次 appPing 超时(毫秒)。
|
||||
* @return OTA_RESULT_OK 成功,OTA_RESULT_ERR_GD32 超时失败。
|
||||
*/
|
||||
ota_result_t doWaitForAppAlive(uint32_t timeout_ms);
|
||||
|
||||
// ── 传输进度跟踪(状态机内部)──
|
||||
uint32_t m_transferTotalPackets = 0;
|
||||
@@ -343,6 +385,14 @@ private:
|
||||
// ── 加密/工具方法(不变)──
|
||||
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
|
||||
/**
|
||||
* @brief 探测 GD32 APP 存活并缓存版本(统一 OtaRelay 内部探测逻辑).
|
||||
* 成功打印版本号,失败打印 warning。
|
||||
* @param timeout_ms appPing 超时(毫秒)。
|
||||
* @param out_uptime 可选,输出 GD32 uptime(秒)。
|
||||
* @return true 探测成功,false 失败。
|
||||
*/
|
||||
static bool probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime = nullptr);
|
||||
esp_err_t waitUntilNotWait(const char *stage, uint8_t *resp_type,
|
||||
uint8_t *resp_payload, size_t resp_max,
|
||||
size_t *resp_len, uint32_t timeout_ms,
|
||||
|
||||
+404
-217
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* ble_ota.cpp - BLE OTA GATT 服务器实现 (ESP-IDF 6.0 NimBLE)
|
||||
* 所有功能封装为 BleOta 类方法。
|
||||
* T5.1 - T5.5
|
||||
@@ -54,6 +54,7 @@
|
||||
#define BLE_OTA_FINISH_REBOOT_NOW 0x07
|
||||
#define BLE_OTA_FINISH_GD32_RELAY_START 0x10
|
||||
#define BLE_OTA_FINISH_GD32_RELAY_DONE 0x11
|
||||
#define BLE_OTA_FINISH_ABORTED 0x20 // 用户主动中止(BLE 不断开)
|
||||
#define BLE_OTA_FINISH_ERR_FINALIZE 0x81
|
||||
#define BLE_OTA_ACK_WINDOW 32
|
||||
#define BLE_ATT_ERR_OTA_BUSY 0x80 // ESP32 OTA 被其他通道占用
|
||||
@@ -81,6 +82,17 @@ struct BleEsp32FinishCtx {
|
||||
uint16_t session_id;
|
||||
};
|
||||
|
||||
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免历史多次漏 free。
|
||||
// 注意:若调用 restartEsp32()(内部 esp_restart 不返回),析构不会执行,
|
||||
// 但这是设备重启路径,资源由系统回收。
|
||||
struct BleFinishCtxDeleter {
|
||||
BleEsp32FinishCtx *p;
|
||||
explicit BleFinishCtxDeleter(BleEsp32FinishCtx *ctx) : p(ctx) {}
|
||||
~BleFinishCtxDeleter() { if (p) { free(p); p = nullptr; } }
|
||||
BleFinishCtxDeleter(const BleFinishCtxDeleter&) = delete;
|
||||
BleFinishCtxDeleter& operator=(const BleFinishCtxDeleter&) = delete;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 前向声明
|
||||
// ============================================================
|
||||
@@ -136,6 +148,8 @@ static void ble_esp32_restart_task(void *pvParameters)
|
||||
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
{
|
||||
BleEsp32FinishCtx *ctx = (BleEsp32FinishCtx *)pvParameters;
|
||||
// RAII:保证所有 return 路径自动释放 ctx(restartEsp32 不返回路径除外)
|
||||
BleFinishCtxDeleter ctx_guard(ctx);
|
||||
char *frontend_sha_hex = ctx ? ctx->sha_hex : nullptr;
|
||||
uint16_t session_id = ctx ? ctx->session_id : 0;
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动");
|
||||
@@ -155,7 +169,6 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 1));
|
||||
if (ctx) free(ctx);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
esp32_reorder_free();
|
||||
vTaskDelete(NULL);
|
||||
@@ -187,8 +200,7 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
|
||||
// 4) 通知全部发出后由协调器统一重启(notify + delay + esp_restart)
|
||||
DEBUG_INFO(TAG, "[FINISH] 通知已发送,重启...");
|
||||
if (ctx) free(ctx);
|
||||
OtaCoordinator::instance().restartEsp32(); // 不返回
|
||||
OtaCoordinator::instance().restartEsp32(); // 不返回,ctx 由系统回收
|
||||
} else if (fr.err == ESP_ERR_INVALID_VERSION) {
|
||||
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, packBleFinishValue(session_id, 0));
|
||||
@@ -207,7 +219,6 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
esp32_reorder_free();
|
||||
}
|
||||
|
||||
if (ctx) free(ctx);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
@@ -234,7 +245,7 @@ static void ble_ota_boost_wifi_ps()
|
||||
s_wifi_ps_boosted = true;
|
||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
||||
OtaCoordinator::instance().notifyBleCoexStart();
|
||||
DEBUG_INFO(TAG, "BLE OTA 进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
|
||||
DEBUG_INFO(TAG, "BLE OTA 数据传输进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +257,7 @@ static void ble_ota_restore_wifi_ps()
|
||||
s_wifi_ps_boosted = false;
|
||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||
DEBUG_INFO(TAG, "BLE OTA 结束:恢复 WiFi RSSI 轮询");
|
||||
DEBUG_INFO(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -312,6 +323,7 @@ struct Esp32OtaReorderBuf {
|
||||
|
||||
static Esp32OtaReorderBuf s_reorder;
|
||||
static uint32_t s_fallback_ooo_count = 0; // 回退模式下自上次 ACK 以来的乱序包计数
|
||||
static uint32_t s_gd32_ooo_count = 0; // GD32 路径自上次 ACK 以来的乱序/重复包计数
|
||||
|
||||
// 前向声明(alloc/free 互相引用)
|
||||
static void esp32_reorder_free();
|
||||
@@ -512,9 +524,14 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
// 后续包处理会覆盖它,必须拷贝一份
|
||||
uint8_t *copy = (uint8_t *)malloc(data_len);
|
||||
if (!copy) {
|
||||
DEBUG_WARN(TAG, "Reorder slot malloc 失败: seq=%u,丢弃该包(靠客户端重传)",
|
||||
seq);
|
||||
return ESP_OK; // 不算失败,靠客户端重传补
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_ERR(TAG, "Reorder slot malloc 失败: seq=%u expected=%u bytes=%u,"
|
||||
"ACK 报告缺口并 abort",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)curReceived);
|
||||
sendWindowAck(cmd,
|
||||
s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
curReceived);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
memcpy(copy, data, data_len);
|
||||
s_reorder.slots[idx] = copy;
|
||||
@@ -548,7 +565,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
{
|
||||
// ★ 通过协调器发起(内部已含 forceCleanup 重试)
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_BLE);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
@@ -564,21 +581,53 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 中止 ESP32 OTA 会话并清理(薄包装)。
|
||||
* @brief BLE OTA 会话状态统一复位(共用清理内核)。
|
||||
*
|
||||
* 集中处理三个清理动作:
|
||||
* 1. abortCurrent():通知协调器按当前状态分发清理
|
||||
* (GD32_STORING→metaClear, ESP32_RECEIVING→core.abort, IDLE→空操作),
|
||||
* 确保下次 begin() 不会被残留状态假拒绝。
|
||||
* 2. core.abort() 兜底:若 Esp32OtaCore 仍 isActive(异常路径),单独再 abort。
|
||||
* 3. BLE OTA 会话状态字段复位:m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending
|
||||
* + esp32_reorder_free() 释放 reorder buffer + s_gd32_ooo_count 归零。
|
||||
*
|
||||
* 不做:OTA 数据队列清理(调用方按需做)、WiFi PS 恢复(调用方按需做)。
|
||||
* 这两个动作在不同调用路径下时序/前置条件不同,交给调用方更安全。
|
||||
*
|
||||
* 调用方:
|
||||
* - ble_esp32_ota_abort(): ESP32 OTA 异常/中止路径
|
||||
* - 0xFB handler: 用户主动中止
|
||||
* - BLE_GAP_EVENT_DISCONNECT handler: BLE 断开(含 GD32 存储断开)
|
||||
*/
|
||||
static void ble_ota_session_reset()
|
||||
{
|
||||
// ★ 无条件通知协调器释放通道占用(m_sysState → IDLE)。
|
||||
// abortCurrent() 对 IDLE 状态是空操作,对其他状态均复位到 IDLE,
|
||||
// 确保后续 begin() 不会被残留的 ESP32_RECEIVING / ERROR 等状态假拒绝。
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
// 若 ESP32 OTA core 仍活跃(兜底),单独再 abort 一次
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
}
|
||||
// 复位 BLE OTA 会话状态,避免下次会话读到旧值
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
BleOta::instance().m_esp32LastCmd = 0;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// 释放 ESP32 reorder buffer(无 buffer 时为空操作)
|
||||
esp32_reorder_free();
|
||||
// 复位 GD32 路径乱序包计数器(节流 ACK 计数),保证下次 GD32 上传从干净状态开始
|
||||
s_gd32_ooo_count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ESP32 OTA 异常/中止时调用:会话复位 + WiFi PS 恢复。
|
||||
*
|
||||
* 关键:必须先通知协调器释放 m_sysState(避免下次 begin 被 ESP32_RECEIVING
|
||||
* 状态假拒绝),再调 core.abort() 释放 mutex。两者重复 abort 幂等安全。
|
||||
*/
|
||||
static void ble_esp32_ota_abort()
|
||||
{
|
||||
// ★ 无条件通知协调器释放通道占用(m_sysState → IDLE)。
|
||||
// abortCurrent() 对 IDLE 状态是空操作,对其他状态均复位到 IDLE,
|
||||
// 确保后续 begin() 不会被残留的 ESP32_RECEIVING / ERROR 等状态假拒绝。
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
Esp32OtaCore::instance().abort();
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// ★ 释放 reorder buffer
|
||||
esp32_reorder_free();
|
||||
ble_ota_session_reset();
|
||||
// ★ BLE OTA 中止:恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
@@ -685,7 +734,7 @@ static void ble_gd32_relay_wait_task(void *pvParameters)
|
||||
|
||||
ota_result_t result = (ota_result_t)((int32_t)result_msg);
|
||||
if (result == OTA_RESULT_OK) {
|
||||
DEBUG_INFO(TAG, "GD32 BLE OTA 中继完成,通知前端");
|
||||
DEBUG_INFO(TAG, "GD32 BLE OTA 中继完成,通知前端 (stage=0x11, session=%u)", session_id);
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_DONE,
|
||||
packBleFinishValue(session_id, 0));
|
||||
} else {
|
||||
@@ -798,6 +847,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_START,
|
||||
packBleFinishValue(session_id, 0));
|
||||
DEBUG_INFO(TAG, "已通知前端: GD32 中继已开始 (stage=0x10, session=%u)", session_id);
|
||||
|
||||
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)malloc(sizeof(BleGd32RelayWaitCtx));
|
||||
if (!ctx) {
|
||||
@@ -876,6 +926,8 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
|
||||
free(finish_ctx);
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, (uint16_t)ESP_ERR_NO_MEM));
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
|
||||
}
|
||||
@@ -896,12 +948,64 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 0xFA 重启任务失败");
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, 0);
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_ABORT_OTA) {
|
||||
// 0xFB: 中止当前 OTA(保持 BLE 连接)
|
||||
// - 数据接收阶段(GD32_STORING / ESP32_RECEIVING):立即中止 + 清队列 + 回复 ABORTED
|
||||
// - 中继/收尾阶段(GD32_RELAYING / ESP32_FINALIZING):拒绝中止(SPI 转发不可中断)
|
||||
// - IDLE/已完成状态:幂等回复 ABORTED
|
||||
uint16_t session_id = 0;
|
||||
if (len >= 3) {
|
||||
session_id = (uint16_t)(buf[1] | (buf[2] << 8));
|
||||
}
|
||||
|
||||
ota_system_state_t cur_state = OtaCoordinator::instance().getSystemState();
|
||||
DEBUG_WARN(TAG, "收到 0xFB 中止请求: state=%s session=%u",
|
||||
OtaCoordinator::stateToString(cur_state), session_id);
|
||||
|
||||
if (cur_state == OTA_SYS_GD32_RELAYING) {
|
||||
DEBUG_WARN(TAG, "GD32 SPI 中继进行中,无法中止(请等待完成或断开 BLE)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
if (cur_state == OTA_SYS_ESP32_FINALIZING) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 收尾进行中,无法中止");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── 可中止状态:清队列 + abort core + 复位协调器 ──
|
||||
if (s_otaDataQueue) {
|
||||
BleOtaDataBatch stale;
|
||||
size_t dropped = 0;
|
||||
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
|
||||
free(stale.data);
|
||||
dropped++;
|
||||
}
|
||||
if (dropped > 0) {
|
||||
DEBUG_INFO(TAG, "中止时清理 OTA 数据队列: %zu 项", dropped);
|
||||
}
|
||||
}
|
||||
|
||||
// abortCurrent 内部按状态分发:GD32_STORING→metaClear, ESP32_RECEIVING→core.abort, IDLE→空操作
|
||||
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 + 会话状态复位
|
||||
// (m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/reorder/s_gd32_ooo_count)
|
||||
ble_ota_session_reset();
|
||||
|
||||
ble_ota_restore_wifi_ps();
|
||||
|
||||
DEBUG_INFO(TAG, "OTA 已中止(BLE 保持连接),通知前端 (stage=0x20)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ABORTED,
|
||||
packBleFinishValue(session_id, 0));
|
||||
}
|
||||
// 0xF7 = 触发 GD32 中继
|
||||
// 0xF8 = 触发 ESP32 本机 OTA 收尾:设备端读 OTA 分区算 SHA 并与前端 32B SHA 比对,
|
||||
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH(不重启)
|
||||
// 0xFA = 重启 ESP32(配套前端 expectedReboot 自动重连)
|
||||
// 0xFB = 中止当前 OTA(不断开 BLE):仅数据接收阶段有效,中继/收尾阶段拒绝
|
||||
|
||||
return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0
|
||||
}
|
||||
@@ -966,6 +1070,248 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理单个 ESP32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
|
||||
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
|
||||
*/
|
||||
static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
const uint8_t *buf, size_t data_offset, size_t data_len,
|
||||
uint32_t expected_payload)
|
||||
{
|
||||
if (seq == 0) {
|
||||
if (total_size == 0) {
|
||||
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
|
||||
return true;
|
||||
}
|
||||
// ★ 会话判定:若已有活跃 ESP32 OTA 会话,根据已接收字节数区分两种情况:
|
||||
// ① m_received == 0:seq=0 尚未写入 flash,说明是客户端 ACK 超时后
|
||||
// 重传的 seq=0(同一会话续传),跳过 begin。
|
||||
// ② m_received > 0:seq=0 已写入且有后续数据,说明这是新的上传会话
|
||||
// (前端 abort 后 BLE 断开未完成就发起了新传输),需 abort 旧会话
|
||||
// 再 begin,否则 reorder buffer 以旧 expected 去重导致全部丢弃。
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
if (Esp32OtaCore::instance().getReceived() > 0) {
|
||||
// ★ 跨通道保护:当前活跃会话可能是 WiFi 通道发起的,
|
||||
// BLE 不应无条件 abort 别人的会话。只有当活跃会话也是 BLE
|
||||
// 通道时才允许 abort + rebegin(同会话重传/新 BLE 会话场景);
|
||||
// 否则直接互斥拒绝,让前端看到"被其他通道占用"。
|
||||
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
|
||||
if (curCh != OTA_CHANNEL_BLE) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被其他通道占用(ch=%d),BLE 拒绝抢占",
|
||||
(int)curCh);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
}
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 新会话检测: received=%u > 0, "
|
||||
"中止旧会话并重新开始",
|
||||
(unsigned)Esp32OtaCore::instance().getReceived());
|
||||
ble_esp32_ota_abort();
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin");
|
||||
}
|
||||
} else {
|
||||
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 会话有效性检查:如果 begin() 失败(如互斥拒绝),后续包也应丢弃
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// ★ 防重复写入保护:如果已接收字节 >= 声明大小,跳过(重试引起的重复包)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
if (total_size > 0 && curReceived >= total_size) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 已收满 (%u >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
(unsigned)curReceived, total_size, seq);
|
||||
sendWindowAck(cmd, seq, curReceived);
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// ★ 使用带序号顺序保护的写入:拒绝乱序包并立即 ACK 通知发送端
|
||||
esp_err_t err = esp32_write_ordered(seq, cmd, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
|
||||
seq, esp_err_to_name(err));
|
||||
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
|
||||
return true;
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
|
||||
seq, (unsigned int)Esp32OtaCore::instance().getReceived());
|
||||
|
||||
// 完成判定:以实际按序写入字节数为准。
|
||||
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
bool transferComplete = (total_size > 0 &&
|
||||
Esp32OtaCore::instance().getReceived() >= total_size);
|
||||
|
||||
// ACK 触发条件
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
transferComplete ||
|
||||
isLastPacket) {
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
seq, data_len, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size);
|
||||
}
|
||||
sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
|
||||
}
|
||||
|
||||
if (transferComplete) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
|
||||
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
BleOta::instance().m_esp32LastSeq = seq;
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理单个 GD32 OTA 数据包(ble_ota_process_data_batch 子步骤)。
|
||||
* @return true 中止整个 batch;false 继续下一包(offset 由外层统一推进)。
|
||||
*/
|
||||
static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
const uint8_t *buf, size_t data_offset, size_t data_len,
|
||||
uint32_t expected_payload)
|
||||
{
|
||||
if (seq == 0 && total_size > 0) {
|
||||
// ★ 统一调用 prepareGd32Storage 完成:validate + begin + erase + clearMeta + saveMeta
|
||||
// 原 BLE 路径未擦除分区,现通过 prepareGd32Storage 与 HTTP 路径对齐。
|
||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(total_size, OTA_CHANNEL_BLE);
|
||||
if (prep_err == ESP_ERR_INVALID_SIZE) {
|
||||
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " max=%zu",
|
||||
total_size, OTA_MAX_FIRMWARE_SIZE);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
|
||||
return true;
|
||||
} else if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||
DEBUG_ERR(TAG, "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
} else if (prep_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
|
||||
total_size, data_len);
|
||||
ble_ota_boost_wifi_ps();
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 顺序检查:GD32 Flash 必须按序写入,无 reorder buffer ──
|
||||
// 改进:乱序/重复包不再中止整个 batch,而是丢弃当前包 + 节流 ACK 报告缺口,
|
||||
// 让前端尽快知道设备端实际进度(last_seq)并重传缺口。
|
||||
// 策略与 ESP32 fallback 路径一致(参考 esp32_write_ordered 行 459-477)。
|
||||
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
|
||||
// 重复包:seq <= last_seq(已写入过)
|
||||
if (seq <= meta.last_seq) {
|
||||
DEBUG_DBG(TAG, "GD32 重复包: seq=%u <= last_seq=%" PRIu32 ",跳过",
|
||||
seq, meta.last_seq);
|
||||
// 重复包不触发 ACK 风暴,只在窗口边界发一次
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) {
|
||||
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
|
||||
}
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// 乱序包:seq > last_seq + 1(缺口)
|
||||
// 节流策略:每 BLE_OTA_ACK_WINDOW 个乱序包才发一次 ACK,或在窗口边界处发 ACK。
|
||||
// 避免大量乱序包涌入时触发 ACK 风暴,耗尽 NimBLE mbuf 池导致 ESP_ERR_NO_MEM。
|
||||
s_gd32_ooo_count++;
|
||||
bool shouldAck =
|
||||
(s_gd32_ooo_count >= BLE_OTA_ACK_WINDOW) ||
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
|
||||
if (shouldAck) {
|
||||
DEBUG_WARN(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),节流 ACK bytes=%" PRIu32 " (ooo=%u)",
|
||||
seq, meta.last_seq, meta.stored_size, (unsigned)s_gd32_ooo_count);
|
||||
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
|
||||
s_gd32_ooo_count = 0;
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),抑制 ACK (ooo=%u)",
|
||||
seq, meta.last_seq, (unsigned)s_gd32_ooo_count);
|
||||
}
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
|
||||
// 顺序包到达,复位乱序计数器
|
||||
if (s_gd32_ooo_count != 0) {
|
||||
s_gd32_ooo_count = 0;
|
||||
}
|
||||
|
||||
// --- GD32 写入 Flash ---
|
||||
uint32_t flash_offset = meta.stored_size;
|
||||
if (flash_offset >= meta.total_size) {
|
||||
DEBUG_WARN(TAG, "GD32 OTA 已收满 (%" PRIu32 " >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
flash_offset, meta.total_size, seq);
|
||||
sendWindowAck(cmd, seq, flash_offset);
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
}
|
||||
if (flash_offset + (uint32_t)data_len > meta.total_size) {
|
||||
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
|
||||
flash_offset, data_len, meta.total_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(flash_offset, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
|
||||
seq, flash_offset, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t stored_size = flash_offset + (uint32_t)data_len;
|
||||
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
|
||||
}
|
||||
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(meta.total_size > 0 && stored_size >= meta.total_size) ||
|
||||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD)) {
|
||||
sendWindowAck(cmd, seq, stored_size);
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
|
||||
seq, flash_offset, stored_size);
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
ble_ota_restore_wifi_ps();
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理一批 OTA 数据包(在工作线程上下文中调用)。
|
||||
* 包含 ESP32 和 GD32 的数据接收逻辑。
|
||||
@@ -1012,203 +1358,22 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu offset=%zu",
|
||||
cmd, seq, total_size, data_len, offset);
|
||||
|
||||
// --- ESP32 自身 OTA:直接写 OTA 分区 ---
|
||||
// 分发到对应通道的包处理器
|
||||
bool abort_batch = false;
|
||||
if (cmd == BLE_OTA_CMD_ESP32_DATA) {
|
||||
if (seq == 0) {
|
||||
if (total_size == 0) {
|
||||
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
|
||||
return;
|
||||
}
|
||||
// ★ 会话判定:若已有活跃 ESP32 OTA 会话,根据已接收字节数区分两种情况:
|
||||
// ① m_received == 0:seq=0 尚未写入 flash,说明是客户端 ACK 超时后
|
||||
// 重传的 seq=0(同一会话续传),跳过 begin。
|
||||
// ② m_received > 0:seq=0 已写入且有后续数据,说明这是新的上传会话
|
||||
// (前端 abort 后 BLE 断开未完成就发起了新传输),需 abort 旧会话
|
||||
// 再 begin,否则 reorder buffer 以旧 expected 去重导致全部丢弃。
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
if (Esp32OtaCore::instance().getReceived() > 0) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 新会话检测: received=%u > 0, "
|
||||
"中止旧会话并重新开始",
|
||||
(unsigned)Esp32OtaCore::instance().getReceived());
|
||||
ble_esp32_ota_abort();
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,跳过 begin");
|
||||
}
|
||||
} else {
|
||||
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 会话有效性检查:如果 begin() 失败(如互斥拒绝),后续包也应丢弃
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 会话无效,丢弃 seq=%u", seq);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > recv_len) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 防重复写入保护:如果已接收字节 >= 声明大小,跳过(重试引起的重复包)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
if (total_size > 0 && curReceived >= total_size) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 已收满 (%u >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
(unsigned)curReceived, total_size, seq);
|
||||
sendWindowAck(cmd, seq, curReceived);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > recv_len) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ★ 使用带序号顺序保护的写入:拒绝乱序包并立即 ACK 通知发送端
|
||||
esp_err_t err = esp32_write_ordered(seq, cmd, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
|
||||
seq, esp_err_to_name(err));
|
||||
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
|
||||
seq, (unsigned int)Esp32OtaCore::instance().getReceived());
|
||||
|
||||
// 完成判定:以实际按序写入字节数为准。
|
||||
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||
bool transferComplete = (total_size > 0 &&
|
||||
Esp32OtaCore::instance().getReceived() >= total_size);
|
||||
|
||||
// ACK 触发条件
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
transferComplete ||
|
||||
isLastPacket) {
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
seq, data_len, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size);
|
||||
}
|
||||
sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
|
||||
}
|
||||
|
||||
if (transferComplete) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
|
||||
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
BleOta::instance().m_esp32LastSeq = seq;
|
||||
ble_ota_restore_wifi_ps();
|
||||
}
|
||||
}
|
||||
// --- GD32 OTA:首个分块时创建存储元数据 ---
|
||||
else if (cmd == BLE_OTA_CMD_GD32_DATA) {
|
||||
if (seq == 0 && total_size > 0) {
|
||||
uint32_t rounded_size = (uint32_t)(((total_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||
if (total_size == 0U || total_size > OTA_MAX_FIRMWARE_SIZE || rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||
DEBUG_ERR(TAG, "GD32 固件大小非法: raw=%" PRIu32 " rounded48=%" PRIu32 " max=%zu",
|
||||
total_size, rounded_size, OTA_MAX_FIRMWARE_SIZE);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_SIZE, total_size);
|
||||
return;
|
||||
}
|
||||
|
||||
if (OtaCoordinator::instance().beginGd32Storage(total_size) != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
return;
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
meta.total_size = total_size;
|
||||
meta.stored_size = 0;
|
||||
meta.last_seq = 0;
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().saveGd32Meta(&meta);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "元数据保存失败");
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 ", payload=%zu",
|
||||
meta.total_size, data_len);
|
||||
ble_ota_boost_wifi_ps();
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
|
||||
return;
|
||||
}
|
||||
|
||||
if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) {
|
||||
DEBUG_ERR(TAG, "GD32 分包序号不连续: seq=%u last_seq=%" PRIu32,
|
||||
seq, meta.last_seq);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- GD32 写入 Flash ---
|
||||
uint32_t flash_offset = meta.stored_size;
|
||||
if (flash_offset >= meta.total_size) {
|
||||
DEBUG_WARN(TAG, "GD32 OTA 已收满 (%" PRIu32 " >= %" PRIu32 "),跳过重复 seq=%u",
|
||||
flash_offset, meta.total_size, seq);
|
||||
sendWindowAck(cmd, seq, flash_offset);
|
||||
offset += packet_len;
|
||||
if (offset + 7 > recv_len) break;
|
||||
continue;
|
||||
}
|
||||
if (flash_offset + (uint32_t)data_len > meta.total_size) {
|
||||
DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32,
|
||||
flash_offset, data_len, meta.total_size);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(flash_offset, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
|
||||
seq, flash_offset, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t stored_size = flash_offset + (uint32_t)data_len;
|
||||
err = OtaCoordinator::instance().updateGd32Meta(stored_size, seq);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
|
||||
}
|
||||
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(meta.total_size > 0 && stored_size >= meta.total_size) ||
|
||||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD)) {
|
||||
sendWindowAck(cmd, seq, stored_size);
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
|
||||
seq, flash_offset, stored_size);
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&meta) == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
OtaCoordinator::instance().completeGd32Storage();
|
||||
ble_ota_restore_wifi_ps();
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finalizeGd32Checksum();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
abort_batch = process_esp32_packet(cmd, seq, total_size,
|
||||
buf, data_offset, data_len, expected_payload);
|
||||
} else if (cmd == BLE_OTA_CMD_GD32_DATA) {
|
||||
abort_batch = process_gd32_packet(cmd, seq, total_size,
|
||||
buf, data_offset, data_len, expected_payload);
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "DATA_IN 未知 cmd: 0x%02X offset=%zu", cmd, offset);
|
||||
}
|
||||
|
||||
if (abort_batch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 推进到下一个包
|
||||
offset += packet_len;
|
||||
|
||||
@@ -1425,11 +1590,33 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
// ★ BLE 断开时恢复 WiFi 射频,并中止未完成的 ESP32 OTA
|
||||
// ★ BLE 断开时恢复 WiFi 射频
|
||||
ble_ota_restore_wifi_ps();
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
|
||||
ble_esp32_ota_abort();
|
||||
|
||||
// ★ 断开时清理"BLE 数据接收阶段"残留的 OTA 会话状态,避免下次刷写被
|
||||
// 系统 busy 假拒绝。注意区分可中止 / 不可中止状态:
|
||||
// - GD32_STORING / ESP32_RECEIVING / ESP32_FINALIZING:BLE 是唯一数据源,
|
||||
// 断开即等价于中止,必须 abortCurrent() + 清理底层 core。
|
||||
// - GD32_RELAYING / ESP32_REBOOTING:SPI 中继/重启不依赖 BLE,不能打断。
|
||||
// 这些状态保留,等中继/重启自然结束。
|
||||
// - IDLE / STORED / COMPLETE / ERROR:abortCurrent 幂等安全,直接调即可。
|
||||
{
|
||||
ota_system_state_t cur = OtaCoordinator::instance().getSystemState();
|
||||
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
|
||||
OtaCoordinator::stateToString(cur));
|
||||
} else {
|
||||
if (cur == OTA_SYS_GD32_STORING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 GD32 固件存储");
|
||||
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
|
||||
cur == OTA_SYS_ESP32_FINALIZING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止未完成的 ESP32 OTA");
|
||||
}
|
||||
// ble_ota_session_reset() 统一负责 abortCurrent + core.abort 兜底 +
|
||||
// 会话状态复位(m_esp32LastSeq/m_esp32LastCmd/m_esp32RebootPending/
|
||||
// reorder/s_gd32_ooo_count)
|
||||
ble_ota_session_reset();
|
||||
}
|
||||
}
|
||||
// 恢复广播以接受新连接
|
||||
ble_ota_start_advertising();
|
||||
|
||||
+253
-56
@@ -30,15 +30,14 @@
|
||||
#include "canfd_bridge.h"
|
||||
#include "ble_ota.h"
|
||||
|
||||
static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U;
|
||||
|
||||
// OTA 接收共享缓冲区
|
||||
static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];
|
||||
|
||||
static uint32_t align_up_u32(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
return ((value + alignment - 1U) / alignment) * alignment;
|
||||
}
|
||||
// OTA 准备状态:/ota/prepare 完成后设置,/ota/upload 消费后清除
|
||||
// ★ 同时记录 prepare 时的 epoch,clear 时仅在同一会话才中止,避免误清新会话
|
||||
static bool s_esp32_prepared = false;
|
||||
static uint32_t s_esp32_prepare_size = 0;
|
||||
static uint32_t s_esp32_prepare_epoch = 0;
|
||||
static bool s_gd32_prepared = false;
|
||||
static uint32_t s_gd32_prepare_size = 0;
|
||||
static uint32_t s_gd32_prepare_epoch = 0;
|
||||
|
||||
void http_set_cors_headers(httpd_req_t *req)
|
||||
{
|
||||
@@ -48,6 +47,27 @@ void http_set_cors_headers(httpd_req_t *req)
|
||||
httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
|
||||
}
|
||||
|
||||
static void clear_esp32_prepare()
|
||||
{
|
||||
if (s_esp32_prepared) {
|
||||
// ★ 仅在 prepare 仍属当前会话时中止;同类型重启已发生时跳过,避免误清新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(s_esp32_prepare_epoch);
|
||||
s_esp32_prepared = false;
|
||||
s_esp32_prepare_size = 0;
|
||||
s_esp32_prepare_epoch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_gd32_prepare()
|
||||
{
|
||||
if (s_gd32_prepared) {
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(s_gd32_prepare_epoch);
|
||||
s_gd32_prepared = false;
|
||||
s_gd32_prepare_size = 0;
|
||||
s_gd32_prepare_epoch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t http_options_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
@@ -126,7 +146,8 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req)
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
|
||||
DEBUG_INFO("OTA", "响应已发送,重启...");
|
||||
DEBUG_INFO("OTA", "响应已发送,等待 TCP 刷新...");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
OtaCoordinator::instance().restartEsp32();
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -177,9 +198,122 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
||||
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)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
// 每请求分配 OTA 接收缓冲区:OTA_BLOCK_SIZE=8KB > 4KB 不适合栈分配,
|
||||
// 改为 per-request malloc 避免 ESP32/GD32 两条路径共享全局 buffer,
|
||||
// max_open_sockets=4 时多连接并发也互不干扰。
|
||||
uint8_t *recv_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
|
||||
if (!recv_buf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "alloc failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d", req->content_len);
|
||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||
|
||||
@@ -191,6 +325,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
int peek_ret = httpd_req_recv(req, &peek, 1);
|
||||
if (peek_ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -231,22 +366,49 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
if (is_esp32_ota) {
|
||||
if (total_size == 0) {
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size);
|
||||
if (err != ESP_OK) {
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
|
||||
// ★ 若前端已通过 /ota/prepare 完成擦除,直接复用 prepared 状态,
|
||||
// 避免在大量数据传输前再次长时间阻塞 HTTP worker。
|
||||
bool used_prepare = false;
|
||||
if (s_esp32_prepared) {
|
||||
if (total_size == s_esp32_prepare_size) {
|
||||
used_prepare = true;
|
||||
s_esp32_prepared = false;
|
||||
s_esp32_prepare_size = 0;
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 使用已 prepared 状态,跳过擦除");
|
||||
} else {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
|
||||
DEBUG_WARN("WIFI", "ESP32 prepared size mismatch (prepared=%u, upload=%u),重新初始化",
|
||||
s_esp32_prepare_size, total_size);
|
||||
clear_esp32_prepare();
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err;
|
||||
if (!used_prepare) {
|
||||
err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI);
|
||||
if (err != ESP_OK) {
|
||||
clear_esp32_prepare();
|
||||
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");
|
||||
}
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
Esp32OtaCore &core = Esp32OtaCore::instance();
|
||||
uint8_t *fw_buf = s_ota_recv_buf;
|
||||
uint8_t *fw_buf = recv_buf;
|
||||
|
||||
// ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler
|
||||
// 若同类型重启已发生(新请求接管),旧 handler 的 abortCurrentIfEpoch 不会中止新会话
|
||||
uint32_t my_epoch = OtaCoordinator::instance().getOtaEpoch();
|
||||
|
||||
size_t total_firmware_written = 0;
|
||||
size_t total_received = json_header_len;
|
||||
@@ -259,16 +421,23 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret);
|
||||
core.abort();
|
||||
// ★ 客户端断开/接收失败:用 abortCurrentIfEpoch 复位协调器状态机
|
||||
// 若 epoch 不匹配(新请求已接管),跳过中止,避免误清新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
err = core.write(fw_buf, (size_t)ret);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "OTA write failed: %s", esp_err_to_name(err));
|
||||
core.abort();
|
||||
// ★ 同上:写入失败也用 abortCurrentIfEpoch,避免误中止新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
clear_esp32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -289,6 +458,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
free(recv_buf);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -299,53 +469,70 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
} else {
|
||||
gd32_valid_size = gd32_fw_size;
|
||||
}
|
||||
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_valid_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||
if (gd32_valid_size == 0U || gd32_valid_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u rounded48=%u max=%u",
|
||||
(unsigned)gd32_valid_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||
if (OtaCoordinator::instance().validateGd32Size(gd32_valid_size) != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u max=%u",
|
||||
(unsigned)gd32_valid_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||
clear_gd32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
if (OtaCoordinator::instance().beginGd32Storage(gd32_valid_size) != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(false);
|
||||
WifiOta::instance().stateUnlock();
|
||||
|
||||
const uint32_t gd32_erase_size = align_up_u32(gd32_valid_size, GD32_OTA_FLASH_SECTOR_SIZE);
|
||||
DEBUG_INFO("WIFI", "擦除 gd32_ota 分区: %u bytes (fw=%u)",
|
||||
(unsigned)gd32_erase_size, (unsigned)gd32_valid_size);
|
||||
esp_err_t erase_err = OtaCoordinator::instance().eraseGd32Partition(gd32_erase_size);
|
||||
if (erase_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
||||
char err_msg[96];
|
||||
snprintf(err_msg, sizeof(err_msg), "erase gd32_ota failed: %s", esp_err_to_name(erase_err));
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, err_msg);
|
||||
return ESP_FAIL;
|
||||
// ★ 若前端已通过 /ota/prepare 完成擦除,直接复用 prepared 状态
|
||||
bool used_gd32_prepare = false;
|
||||
if (s_gd32_prepared) {
|
||||
if (gd32_valid_size == s_gd32_prepare_size) {
|
||||
used_gd32_prepare = true;
|
||||
s_gd32_prepared = false;
|
||||
s_gd32_prepare_size = 0;
|
||||
DEBUG_INFO("WIFI", "GD32 OTA 使用已 prepared 状态,跳过擦除");
|
||||
} else {
|
||||
DEBUG_WARN("WIFI", "GD32 prepared size mismatch (prepared=%u, upload=%u),重新初始化",
|
||||
s_gd32_prepare_size, gd32_valid_size);
|
||||
clear_gd32_prepare();
|
||||
}
|
||||
}
|
||||
OtaCoordinator::instance().clearGd32Meta();
|
||||
|
||||
uint8_t *buf = s_ota_recv_buf;
|
||||
if (!used_gd32_prepare) {
|
||||
if (OtaRelay::getState() != RELAY_IDLE) {
|
||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState());
|
||||
OtaRelay::instance().reset();
|
||||
}
|
||||
|
||||
// ★ 统一调用 prepareGd32Storage 完成:begin + erase + clearMeta + saveMeta
|
||||
// 与 BLE 路径(process_gd32_packet seq=0)共用同一序列,消除隐式差异。
|
||||
esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(gd32_valid_size, OTA_CHANNEL_WIFI);
|
||||
if (prep_err == ESP_ERR_INVALID_STATE) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)");
|
||||
clear_gd32_prepare();
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "system busy with another OTA operation");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
} else if (prep_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err));
|
||||
clear_gd32_prepare();
|
||||
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);
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
WifiOta::instance().stateLock();
|
||||
WifiOta::instance().setGd32StorageDone(false);
|
||||
WifiOta::instance().stateUnlock();
|
||||
}
|
||||
|
||||
// ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler
|
||||
uint32_t my_epoch = OtaCoordinator::instance().getOtaEpoch();
|
||||
|
||||
uint8_t *buf = recv_buf;
|
||||
|
||||
int received = 0;
|
||||
|
||||
// expected_fw_size_raw 仅用于后续日志输出;meta 已由 prepareGd32Storage
|
||||
// (非 prepared 路径)或 /ota/prepare(prepared 路径)写入,此处不再重复保存。
|
||||
uint32_t expected_fw_size_raw = gd32_valid_size;
|
||||
ota_meta_t meta_raw = {
|
||||
.total_size = expected_fw_size_raw,
|
||||
.stored_size = 0,
|
||||
.last_seq = 0,
|
||||
.checksum = {0}
|
||||
};
|
||||
OtaCoordinator::instance().saveGd32Meta(&meta_raw);
|
||||
uint32_t gd32_offset = 0;
|
||||
|
||||
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
||||
@@ -359,7 +546,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
|
||||
first_chunk_len, esp_err_to_name(err));
|
||||
// ★ 首块写入失败:用 abortCurrentIfEpoch 避免误中止新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
received = first_chunk_len;
|
||||
@@ -378,7 +568,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "GD32 upload interrupted: received=%d expected=%d",
|
||||
received, req->content_len);
|
||||
// ★ 客户端断开(刷新网页/关浏览器/网络掉):用 abortCurrentIfEpoch 避免误中止新会话
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "upload interrupted");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -386,7 +579,10 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s",
|
||||
(unsigned)gd32_offset, ret, esp_err_to_name(err));
|
||||
// ★ 写入失败:同上,用 abortCurrentIfEpoch
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||
free(recv_buf);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -416,6 +612,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
"\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}";
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
free(recv_buf);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
+29
-135
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* wifi_ota.cpp - WiFi OTA 服务器实现
|
||||
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
|
||||
* T4.1 - T4.5
|
||||
@@ -442,144 +442,38 @@ esp_err_t WifiOta::httpServerStart()
|
||||
}
|
||||
}
|
||||
|
||||
httpd_uri_t upload_uri = {
|
||||
.uri = "/ota/upload",
|
||||
.method = HTTP_POST,
|
||||
.handler = ota_upload_handler,
|
||||
.user_ctx = NULL
|
||||
struct UriEntry {
|
||||
const char *uri;
|
||||
httpd_method_t method;
|
||||
esp_err_t (*handler)(httpd_req_t *);
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &upload_uri);
|
||||
httpd_uri_t upload_options_uri = {
|
||||
.uri = "/ota/upload",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
static const UriEntry kHttpRoutes[] = {
|
||||
{"/ota/upload", HTTP_POST, ota_upload_handler},
|
||||
{"/ota/relay", HTTP_POST, ota_relay_trigger_handler},
|
||||
{"/ota/update", HTTP_POST, esp32_ota_update_handler},
|
||||
{"/restart", HTTP_POST, restart_handler},
|
||||
{"/logs", HTTP_GET, logs_handler},
|
||||
{"/system/status", HTTP_GET, system_status_handler},
|
||||
{"/system/probe", HTTP_GET, system_probe_handler},
|
||||
{"/can/send", HTTP_POST, can_send_handler},
|
||||
{"/spi/send", HTTP_POST, spi_send_handler},
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &upload_options_uri);
|
||||
|
||||
httpd_uri_t relay_uri = {
|
||||
.uri = "/ota/relay",
|
||||
.method = HTTP_POST,
|
||||
.handler = ota_relay_trigger_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &relay_uri);
|
||||
httpd_uri_t relay_options_uri = {
|
||||
.uri = "/ota/relay",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &relay_options_uri);
|
||||
for (const auto &r : kHttpRoutes) {
|
||||
httpd_uri_t main_uri = {};
|
||||
main_uri.uri = r.uri;
|
||||
main_uri.method = r.method;
|
||||
main_uri.handler = r.handler;
|
||||
main_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &main_uri);
|
||||
|
||||
httpd_uri_t esp32_update_uri = {
|
||||
.uri = "/ota/update",
|
||||
.method = HTTP_POST,
|
||||
.handler = esp32_ota_update_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_update_uri);
|
||||
httpd_uri_t esp32_update_options_uri = {
|
||||
.uri = "/ota/update",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &esp32_update_options_uri);
|
||||
|
||||
httpd_uri_t restart_uri = {
|
||||
.uri = "/restart",
|
||||
.method = HTTP_POST,
|
||||
.handler = restart_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &restart_uri);
|
||||
httpd_uri_t restart_options_uri = {
|
||||
.uri = "/restart",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &restart_options_uri);
|
||||
|
||||
httpd_uri_t logs_uri = {
|
||||
.uri = "/logs",
|
||||
.method = HTTP_GET,
|
||||
.handler = logs_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_uri);
|
||||
httpd_uri_t logs_options_uri = {
|
||||
.uri = "/logs",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &logs_options_uri);
|
||||
|
||||
// 系统状态查询端点
|
||||
httpd_uri_t status_uri = {
|
||||
.uri = "/system/status",
|
||||
.method = HTTP_GET,
|
||||
.handler = system_status_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_uri);
|
||||
httpd_uri_t status_options_uri = {
|
||||
.uri = "/system/status",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &status_options_uri);
|
||||
|
||||
// PR2: GD32 APP 探测端点
|
||||
httpd_uri_t system_probe_uri = {
|
||||
.uri = "/system/probe",
|
||||
.method = HTTP_GET,
|
||||
.handler = system_probe_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_probe_uri);
|
||||
httpd_uri_t system_probe_options_uri = {
|
||||
.uri = "/system/probe",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &system_probe_options_uri);
|
||||
|
||||
// CAN 帧转发
|
||||
httpd_uri_t can_send_uri = {
|
||||
.uri = "/can/send",
|
||||
.method = HTTP_POST,
|
||||
.handler = can_send_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_send_uri);
|
||||
httpd_uri_t can_send_options_uri = {
|
||||
.uri = "/can/send",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &can_send_options_uri);
|
||||
|
||||
// SPI 直接发送
|
||||
httpd_uri_t spi_send_uri = {
|
||||
.uri = "/spi/send",
|
||||
.method = HTTP_POST,
|
||||
.handler = spi_send_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_uri);
|
||||
httpd_uri_t spi_send_options_uri = {
|
||||
.uri = "/spi/send",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = http_options_handler,
|
||||
.user_ctx = NULL
|
||||
};
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri);
|
||||
httpd_uri_t opts_uri = {};
|
||||
opts_uri.uri = r.uri;
|
||||
opts_uri.method = HTTP_OPTIONS;
|
||||
opts_uri.handler = http_options_handler;
|
||||
opts_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /spi/send");
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
#include "gd32_flash_store.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
// ============================================================
|
||||
// 单例
|
||||
@@ -24,6 +26,34 @@ Gd32FlashStore& Gd32FlashStore::instance()
|
||||
return inst;
|
||||
}
|
||||
|
||||
// 构造时创建 NVS 互斥锁。
|
||||
// 单例静态局部变量在首次 instance() 调用时构造,此时 FreeRTOS 已就绪。
|
||||
Gd32FlashStore::Gd32FlashStore()
|
||||
{
|
||||
m_nvsMutex = xSemaphoreCreateMutex();
|
||||
if (!m_nvsMutex) {
|
||||
DEBUG_ERR("OTA", "Gd32FlashStore NVS mutex 创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NVS 互斥锁辅助
|
||||
// ============================================================
|
||||
bool Gd32FlashStore::lockNvs(uint32_t timeout_ms)
|
||||
{
|
||||
// 参考 OtaCoordinator::lock:返回 take 结果,由调用方传回 unlockNvs 决定是否 give。
|
||||
if (!m_nvsMutex) return false;
|
||||
return xSemaphoreTake(m_nvsMutex, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
|
||||
}
|
||||
|
||||
void Gd32FlashStore::unlockNvs(bool handle)
|
||||
{
|
||||
// 仅在对应 lockNvs 成功 take 时才 give,避免误释放。
|
||||
if (handle && m_nvsMutex) {
|
||||
xSemaphoreGive(m_nvsMutex);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分区懒初始化
|
||||
// ============================================================
|
||||
@@ -132,8 +162,8 @@ esp_err_t Gd32FlashStore::erasePartition(uint32_t size)
|
||||
// NVS 元数据持久化
|
||||
// ============================================================
|
||||
|
||||
/** @brief 从 NVS 加载 OTA 元数据,命名空间不存在时返回全零结构。 */
|
||||
esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
/** @brief 已持锁版本:从 NVS 加载 OTA 元数据,命名空间不存在时返回全零结构。 */
|
||||
esp_err_t Gd32FlashStore::metaLoadLocked(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
@@ -141,7 +171,15 @@ esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(OTA_META_NVS_NAMESPACE, NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return ESP_OK;
|
||||
// 区分 nvs_open 失败原因,不再一律当 OK。
|
||||
// ESP_ERR_NVS_NOT_FOUND = 命名空间不存在(首次使用/被擦除),视为空元数据,保持兼容。
|
||||
// 其他错误(NVS 未初始化/分区丢失/损坏等)返回真实错误码,
|
||||
// 避免掩盖底层故障导致调用方误判为"空元数据"继续执行。
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
return ESP_OK;
|
||||
}
|
||||
DEBUG_WARN("OTA", "metaLoad nvs_open 失败(非 NOT_FOUND): %s", esp_err_to_name(err));
|
||||
return err;
|
||||
}
|
||||
|
||||
uint32_t val = 0;
|
||||
@@ -159,8 +197,24 @@ esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief 将 OTA 元数据写入 NVS。 */
|
||||
esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
/** @brief 从 NVS 加载 OTA 元数据(加锁包装)。 */
|
||||
esp_err_t Gd32FlashStore::metaLoad(ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
// 加锁失败不能跳过锁直接访问 NVS,清零输出并返回超时
|
||||
memset(meta, 0, sizeof(ota_meta_t));
|
||||
DEBUG_WARN("OTA", "metaLoad 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t err = metaLoadLocked(meta);
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 已持锁版本:将 OTA 元数据写入 NVS。 */
|
||||
esp_err_t Gd32FlashStore::metaSaveLocked(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
@@ -184,25 +238,57 @@ esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief 传输过程中更新元数据的 stored_size 和 last_seq 字段。 */
|
||||
/** @brief 将 OTA 元数据写入 NVS(加锁包装)。 */
|
||||
esp_err_t Gd32FlashStore::metaSave(const ota_meta_t *meta)
|
||||
{
|
||||
if (!meta) return ESP_ERR_INVALID_ARG;
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaSave 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t err = metaSaveLocked(meta);
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 传输过程中更新元数据的 stored_size 和 last_seq 字段。
|
||||
* 整个读-改-写过程持锁,避免并发 metaUpdate 丢失更新。
|
||||
* 内部调用已持锁的 metaLoadLocked/metaSaveLocked,不再经公开方法重复加锁。
|
||||
*/
|
||||
esp_err_t Gd32FlashStore::metaUpdate(uint32_t stored_size, uint32_t last_seq)
|
||||
{
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaUpdate 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = metaLoad(&meta);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
|
||||
return metaSave(&meta);
|
||||
esp_err_t err = metaLoadLocked(&meta);
|
||||
if (err == ESP_OK) {
|
||||
meta.stored_size = stored_size;
|
||||
meta.last_seq = last_seq;
|
||||
err = metaSaveLocked(&meta);
|
||||
}
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
/** @brief OTA 完成或中止时清除元数据(全零写入)。 */
|
||||
esp_err_t Gd32FlashStore::metaClear()
|
||||
{
|
||||
DEBUG_INFO("OTA", "清除 OTA 元数据");
|
||||
bool locked = lockNvs(kNvsLockTimeoutMs);
|
||||
if (!locked) {
|
||||
DEBUG_WARN("OTA", "metaClear 加锁超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
ota_meta_t meta = {};
|
||||
return metaSave(&meta);
|
||||
esp_err_t err = metaSaveLocked(&meta);
|
||||
unlockNvs(true);
|
||||
return err;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -296,13 +382,12 @@ esp_err_t Gd32FlashStore::metaVerifyChecksum()
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
// 检查 checksum 是否全零(未计算过)
|
||||
bool all_zero = true;
|
||||
for (size_t i = 0; i < sizeof(meta.checksum); ++i) {
|
||||
if (meta.checksum[i] != 0x00) { all_zero = false; break; }
|
||||
}
|
||||
if (all_zero) {
|
||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,跳过校验(旧数据兼容)");
|
||||
return ESP_OK;
|
||||
// 注意:返回 INVALID_STATE 而非 ESP_OK,由调用方决定是否容忍。
|
||||
// 历史上这里直接返回 OK 跳过校验,但 meta 被意外清零时会掩盖固件损坏。
|
||||
static const uint8_t kZeroSha[32] = {0};
|
||||
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
|
||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,返回 INVALID_STATE(调用方决定容忍或失败)");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
uint8_t computed[32] = {0};
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* 不再由 OtaRelay 承担存储职责(解决 H1 职责超载)。
|
||||
*
|
||||
* 线程模型:
|
||||
* - 所有方法均为 static,由调用方(协调器/OtaRelay/传输层)保证串行化
|
||||
* - 通过 instance() 单例访问,NVS 元数据读写由内部 mutex 保护
|
||||
* - metaLoad/metaSave/metaUpdate/metaClear 串行化,读-改-写原子
|
||||
* - 分区指针懒初始化,首次使用时查找并缓存
|
||||
*/
|
||||
#ifndef GD32_FLASH_STORE_H
|
||||
@@ -15,6 +16,8 @@
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_partition.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#define OTA_META_NVS_NAMESPACE "ota_meta" ///< OTA 元数据 NVS 命名空间
|
||||
|
||||
@@ -71,9 +74,11 @@ public:
|
||||
// ── NVS 元数据持久化 ──
|
||||
|
||||
/**
|
||||
* @brief 从 NVS 加载 OTA 元数据(命名空间不存在时返回全零结构)。
|
||||
* @param meta 输出元数据结构体指针。
|
||||
* @return ESP_OK 成功。
|
||||
* @brief 从 NVS 加载 OTA 元数据。
|
||||
* @param meta 输出元数据结构体指针(任何错误路径下都会被清零)。
|
||||
* @return ESP_OK 成功(含命名空间不存在的兼容路径,返回全零结构);
|
||||
* ESP_ERR_TIMEOUT 加锁超时;
|
||||
* 其他错误码 = nvs_open 真实失败(如 NVS 未初始化/损坏),不再一律当 OK。
|
||||
*/
|
||||
esp_err_t metaLoad(ota_meta_t *meta);
|
||||
|
||||
@@ -135,12 +140,24 @@ public:
|
||||
const esp_partition_t* getPartition();
|
||||
|
||||
private:
|
||||
Gd32FlashStore() = default;
|
||||
Gd32FlashStore();
|
||||
~Gd32FlashStore() = default;
|
||||
Gd32FlashStore(const Gd32FlashStore&) = delete;
|
||||
Gd32FlashStore& operator=(const Gd32FlashStore&) = delete;
|
||||
|
||||
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
||||
|
||||
// ── NVS 互斥锁(保护元数据读-改-写原子性)──
|
||||
// lockNvs 返回是否成功 take;unlockNvs(handle) 据此决定是否 give,
|
||||
// 参考 OtaCoordinator::lock/unlock 模式(避免共享 bool 被并发覆盖导致死锁)。
|
||||
SemaphoreHandle_t m_nvsMutex = nullptr;
|
||||
bool lockNvs(uint32_t timeout_ms);
|
||||
void unlockNvs(bool handle);
|
||||
static constexpr uint32_t kNvsLockTimeoutMs = 100; ///< NVS 加锁超时(ms)
|
||||
|
||||
// 已持锁版本:由持锁的公开方法或 metaUpdate 调用,不再自行加锁,避免递归 take 死锁
|
||||
esp_err_t metaLoadLocked(ota_meta_t *meta);
|
||||
esp_err_t metaSaveLocked(const ota_meta_t *meta);
|
||||
};
|
||||
|
||||
#endif // GD32_FLASH_STORE_H
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* protocol.cpp - 协议层实现
|
||||
* T2.1, T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
@@ -99,21 +99,17 @@ int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
|
||||
|
||||
memset(sess, 0, sizeof(session_ctx_t));
|
||||
memcpy(sess->key, k_session, 16);
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥和状态。
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
*/
|
||||
void Protocol::sessionKeyClear(session_ctx_t *sess)
|
||||
{
|
||||
if (sess) {
|
||||
memset(sess->key, 0, 16);
|
||||
memset(sess->iv, 0, 16);
|
||||
sess->used = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* protocol.h - 帧格式、CRC16、AES 加密解密
|
||||
* T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
@@ -12,11 +12,6 @@
|
||||
#include <stdbool.h>
|
||||
#include "app_config.h"
|
||||
|
||||
// 帧格式: SOF(1B) + CMD(1B) + LEN(2B LE) + PAYLOAD(LEN) + CRC16(2B LE)
|
||||
#define FRAME_SOF (0xEB) // 帧起始标记
|
||||
#define FRAME_MAX_PAYLOAD (64) // 安全最大 payload
|
||||
#define FRAME_OVERHEAD (6) // SOF + CMD + LEN(2) + CRC16(2)
|
||||
|
||||
// 安全常量(定义在 app_config.h 中)
|
||||
inline constexpr size_t MASTER_KEY_HERE_LEN = 16; ///< AES-128 主密钥长度(字节)
|
||||
inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(字节)
|
||||
@@ -24,8 +19,6 @@ inline constexpr size_t CHIP_UID_LEN = 12; ///< GD32 芯片 UID 长度(
|
||||
// AES 会话上下文
|
||||
typedef struct {
|
||||
uint8_t key[16]; // 会话密钥 K_session
|
||||
uint8_t iv[16]; // CBC 模式 IV
|
||||
int used; // 会话是否已初始化标志
|
||||
} session_ctx_t;
|
||||
|
||||
/**
|
||||
@@ -64,7 +57,7 @@ public:
|
||||
*/
|
||||
static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session);
|
||||
/**
|
||||
* @brief 清除会话上下文的密钥和 IV 数据。
|
||||
* @brief 清除会话上下文中的密钥数据。
|
||||
* @param sess 会话上下文指针。
|
||||
*/
|
||||
static void sessionKeyClear(session_ctx_t *sess);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* spi_master.cpp - SPI Master 驱动实现
|
||||
* T1.1 - T1.4
|
||||
* ESP-IDF 6.0 适配版本
|
||||
@@ -707,7 +707,7 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||
DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries);
|
||||
|
||||
while (retry_count < max_retries) {
|
||||
ret = syncTransferUnlocked(cmd, payload, payload_len,
|
||||
ret = transferRequestResponse(cmd, payload, payload_len,
|
||||
recv_buf, recv_len, 3000);
|
||||
if (ret == ESP_OK) {
|
||||
if (retry_count > 0) {
|
||||
@@ -728,34 +728,3 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||
|
||||
// CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦)
|
||||
|
||||
// ============================================================
|
||||
// 无锁内部版本实现
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 无锁版本的同步 SPI 传输实现。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param payload_len 负载数据长度。
|
||||
* @param recv_buf 接收数据缓冲区。
|
||||
* @param recv_len 接收数据长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t SpiMaster::syncTransferUnlocked(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms)
|
||||
{
|
||||
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 无锁版本的 SPI 发送实现。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param len 负载数据长度。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t SpiMaster::sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len)
|
||||
{
|
||||
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* spi_master.h - SPI Master 驱动头文件
|
||||
* CANFD 3通道发送接口 + 底层 SPI 总线
|
||||
*
|
||||
@@ -182,27 +182,6 @@ private:
|
||||
|
||||
|
||||
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
|
||||
/**
|
||||
* @brief 无锁版本的同步 SPI 传输(调用者须已持有锁)。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param payload_len 负载数据长度。
|
||||
* @param recv_buf 接收数据缓冲区。
|
||||
* @param recv_len 接收数据长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t syncTransferUnlocked(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms);
|
||||
/**
|
||||
* @brief 无锁版本的 SPI 发送(调用者须已持有锁)。
|
||||
* @param cmd 命令字节。
|
||||
* @param payload 负载数据缓冲区。
|
||||
* @param len 负载数据长度。
|
||||
* @return ESP_OK 表示成功,否则返回错误码。
|
||||
*/
|
||||
esp_err_t sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
/**
|
||||
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
|
||||
* @param resp_type 输出响应类型。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
CONFIG_IDF_TARGET="esp32c3"
|
||||
CONFIG_ESP32C3_REV_MIN_101=y
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
@@ -1,4 +0,0 @@
|
||||
CONFIG_IDF_TARGET="esp32c2"
|
||||
CONFIG_XTAL_FREQ_26=y
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
@@ -1,5 +0,0 @@
|
||||
CONFIG_IDF_TARGET="esp32c2"
|
||||
CONFIG_XTAL_FREQ_26=y
|
||||
CONFIG_ESP32C2_REV_MIN_200=y
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
@@ -1,3 +0,0 @@
|
||||
CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=n
|
||||
CONFIG_ESP_WIFI_SSID="ssid_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
CONFIG_ESP_WIFI_PASSWORD="password_${IDF_TARGET}_${CI_PIPELINE_ID}"
|
||||
+24
-4
@@ -274,6 +274,19 @@ function formatHttpErrorDetail(text) {
|
||||
if (detail.length > 180) detail = detail.slice(0, 177) + '...';
|
||||
return detail;
|
||||
}
|
||||
function isOtaBusyError(status, detail) {
|
||||
var text = (detail || '').toLowerCase();
|
||||
return status === 403 ||
|
||||
status === 409 ||
|
||||
status === 423 ||
|
||||
text.indexOf('busy') >= 0 ||
|
||||
text.indexOf('occupied') >= 0 ||
|
||||
text.indexOf('another ota') >= 0 ||
|
||||
text.indexOf('invalid state') >= 0 ||
|
||||
text.indexOf('占用') >= 0 ||
|
||||
text.indexOf('互斥') >= 0 ||
|
||||
text.indexOf('系统忙') >= 0;
|
||||
}
|
||||
/** @description 显示 Toast 提示消息,支持 info/success/warning/error 类型。 */
|
||||
function showToast(message, type) {
|
||||
type = type || 'success';
|
||||
@@ -1289,7 +1302,13 @@ function clearFile(chip) {
|
||||
/** @description 启动 ESP32 本机 OTA 上传流程(互斥获取→分块上传→SHA验证→重启)。 */
|
||||
async function startUpload(chip) {
|
||||
var file = chip === 'esp32' ? esp32File : gd32File;
|
||||
if (!file || isUploading) return;
|
||||
if (!file) return;
|
||||
if (isUploading) {
|
||||
var busyChip = (activeUploadChip || window.flashTarget || chip || 'ota').toUpperCase();
|
||||
addLog('W', '上传', '已有 ' + busyChip + ' OTA 正在进行,拒绝新的 ' + chip.toUpperCase() + ' 上传');
|
||||
showToast('OTA 通道正在占用,请等待当前刷写结束', 'error');
|
||||
return;
|
||||
}
|
||||
pausePollingForUpload();
|
||||
// ★ 必须先抢锁再 await:守卫同步设置,await 期间重复点击才能被拦下。
|
||||
// 旧实现把 isUploading 设在 await handle.getFile() 之后 → 用户在 BLE 期间
|
||||
@@ -1480,9 +1499,10 @@ async function startUpload(chip) {
|
||||
setProgress(45, '上传完成,校验并触发 GD32 OTA...');
|
||||
triggerGd32Relay();
|
||||
}
|
||||
} else if (xhr.status === 403) {
|
||||
addLog('E', '上传', 'ESP32 OTA 被其他通道占用(BLE/WiFi 互斥),请稍后重试');
|
||||
showToast('ESP32 OTA 被其他通道占用,请稍后重试', 'error');
|
||||
} else if (isOtaBusyError(xhr.status, formatHttpErrorDetail(xhr.responseText))) {
|
||||
var busyDetail = formatHttpErrorDetail(xhr.responseText);
|
||||
addLog('E', '上传', chip.toUpperCase() + ' OTA 被其他通道占用(BLE/WiFi 互斥)' + (busyDetail ? ': ' + busyDetail : ''));
|
||||
showToast('OTA 通道被占用,请稍后重试', 'error');
|
||||
cleanupAfterOtaFailure('通道被占用');
|
||||
} else {
|
||||
var detail = formatHttpErrorDetail(xhr.responseText);
|
||||
|
||||
+93
-29
@@ -19,6 +19,8 @@
|
||||
const BLE_OTA_FINISH_REBOOT_NOW = 0x07;
|
||||
const BLE_OTA_FINISH_GD32_RELAY_START = 0x10;
|
||||
const BLE_OTA_FINISH_GD32_RELAY_DONE = 0x11;
|
||||
const BLE_OTA_FINISH_ABORTED = 0x20; // 用户主动中止(BLE 不断开)
|
||||
const BLE_OTA_CTRL_ABORT_OTA = 0xFB; // 中止当前 OTA(不断开 BLE)
|
||||
const BLE_OTA_FINISH_ERR_FINALIZE = 0x81;
|
||||
const BLE_OTA_FINISH_ERR_MUTEX = 0x83;
|
||||
const BLE_OTA_FINISH_ERR_GD32_SIZE = 0x84;
|
||||
@@ -242,6 +244,10 @@
|
||||
return value & 0xFFFF;
|
||||
}
|
||||
|
||||
function isSessionlessFinish(value) {
|
||||
return finishSessionId(value) === 0;
|
||||
}
|
||||
|
||||
function hasCurrentWaiter(targetKey) {
|
||||
return !!(state.otaResultWaiter && state.otaResultWaiter.targetKey === targetKey);
|
||||
}
|
||||
@@ -612,6 +618,7 @@
|
||||
[BLE_OTA_FINISH_REBOOT_NOW]: '[FINISH] ESP32 即将重启',
|
||||
[BLE_OTA_FINISH_GD32_RELAY_START]: '[FINISH] GD32 OTA 转发已开始',
|
||||
[BLE_OTA_FINISH_GD32_RELAY_DONE]: '[FINISH] GD32 OTA 转发完成',
|
||||
[BLE_OTA_FINISH_ABORTED]: '[FINISH] OTA 已中止(BLE 保持连接)',
|
||||
[BLE_OTA_FINISH_ERR_FINALIZE]: `[FINISH] ESP32 OTA 收尾失败: err=${value}`,
|
||||
[BLE_OTA_FINISH_ERR_MUTEX]: '[FINISH] OTA 被其他通道占用,互斥拒绝',
|
||||
[BLE_OTA_FINISH_ERR_GD32_SIZE]: `[FINISH] GD32 固件超出分区大小: ${formatBytes(value)} (上限 216KB)`,
|
||||
@@ -650,6 +657,29 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 0x20 ABORTED:ESP32 已中止 OTA(BLE 保持连接)──
|
||||
// 中止可由用户主动点"中止"按钮(abortTask 发 0xFB)触发,
|
||||
// 也可能是设备端因错误自动中止。两种情况都走同一清理路径。
|
||||
if (stage === BLE_OTA_FINISH_ABORTED) {
|
||||
state.abortRequested = true;
|
||||
state.expectedReboot = false;
|
||||
state.reconnecting = false;
|
||||
state.reconnectAttempts = 0;
|
||||
state.lastUploadStatus = 'aborted';
|
||||
setProgress(0, 'OTA 已中止');
|
||||
setBleStageError('OTA 已中止');
|
||||
showToast('OTA 已中止(BLE 保持连接)', 'warning');
|
||||
// 解除任何等待 ACK / OTA 结果的 Promise
|
||||
if (state.ackWaiter) {
|
||||
const waiter = state.ackWaiter;
|
||||
state.ackWaiter = null;
|
||||
clearTimeout(waiter.timer);
|
||||
waiter.reject(new Error('OTA 已中止'));
|
||||
}
|
||||
rejectOtaResult('OTA 已中止', { stage: stage, value: value });
|
||||
return;
|
||||
}
|
||||
|
||||
if (stage === BLE_OTA_FINISH_ERR_GD32_RELAY) {
|
||||
if (!finishBelongsToWaiter('gd32', value)) return;
|
||||
if (typeof switchStageFlow === 'function') switchStageFlow('gd32');
|
||||
@@ -687,9 +717,12 @@
|
||||
|
||||
// OTA 结果 toast 提示(用户可见,不只是在日志里)
|
||||
if (stage === BLE_OTA_FINISH_ERR_MUTEX) {
|
||||
if (hasCurrentWaiter('esp32') && !finishBelongsToWaiter('esp32', value)) return;
|
||||
if (hasCurrentWaiter('gd32') && !finishBelongsToWaiter('gd32', value)) return;
|
||||
if (!isSessionlessFinish(value)) {
|
||||
if (hasCurrentWaiter('esp32') && !finishBelongsToWaiter('esp32', value)) return;
|
||||
if (hasCurrentWaiter('gd32') && !finishBelongsToWaiter('gd32', value)) return;
|
||||
}
|
||||
showToast('OTA 被其他通道占用,请稍后重试', 'error');
|
||||
state.abortRequested = true;
|
||||
setProgress(0, 'OTA 被其他通道占用');
|
||||
state.lastUploadStatus = 'failed';
|
||||
setBleStageError('OTA 被其他通道占用');
|
||||
@@ -884,6 +917,11 @@
|
||||
}
|
||||
|
||||
function waitForAck(cmd, expectedSeq, timeoutMs = 8000, opts = {}) {
|
||||
// ★ 中止快速退出:若 0x20 ABORTED 已先于本调用到达(ackWaiter 当时为 null),
|
||||
// 这里立即 reject,避免在已中止的会话上盲等超时。
|
||||
if (state.abortRequested) {
|
||||
return Promise.reject(new Error('OTA 已中止'));
|
||||
}
|
||||
const gen = state.ackGeneration;
|
||||
// ★ 提前返回:只要 cmd 匹配 + generation 匹配 + 字节数足够,即可 resolve
|
||||
// 不再要求 lastAckSeq >= expectedSeq(与 handleNotify 保持一致)
|
||||
@@ -1227,6 +1265,9 @@
|
||||
// 流水线发送:不逐包 await,减少 BLE 栈 per-packet 开销
|
||||
// ★ 不再在循环内 setProgress:进度完全由设备 ACK 后的 state.lastAckBytes 驱动
|
||||
for (let seq = windowStart; seq <= endSeq; seq++) {
|
||||
// ★ 中止检查放在循环内:abortTask 可能在本窗口发送过程中触发
|
||||
// (windowStart 处的检查无法覆盖窗口中途的 abort)
|
||||
if (state.abortRequested) throw new Error('用户中止');
|
||||
const start = seq * payloadSize;
|
||||
const end = Math.min(start + payloadSize, totalSize);
|
||||
const payload = fileBytes.slice(start, end);
|
||||
@@ -1271,6 +1312,9 @@
|
||||
let prevAckBytes = state.lastAckBytes; // ★ 进度跟踪:检测重试是否有效
|
||||
// ★ 仅以字节数判断是否完成,不依赖 ackOk(generation 不匹配时 ackOk 不可靠)
|
||||
while (state.lastAckBytes < expectedBytes && retryCount < MAX_WINDOW_RETRIES) {
|
||||
// ★ 中止检查:0x20 ABORTED 可能在上一轮 waitForAck 超时期间到达,
|
||||
// 或在 sleep/续发过程中到达。立即抛出,避免无谓重试已中止的会话。
|
||||
if (state.abortRequested) throw new Error('用户中止');
|
||||
// 如果 ESP32 已确认所有数据(ACK 延迟到达),直接跳出
|
||||
if (state.lastAckBytes >= expectedBytes) break;
|
||||
|
||||
@@ -1296,6 +1340,8 @@
|
||||
await sleep(10);
|
||||
|
||||
for (let seq = resumeSeq; seq <= endSeq; seq++) {
|
||||
// ★ 中止检查:续发过程中可能收到 0x20 ABORTED
|
||||
if (state.abortRequested) throw new Error('用户中止');
|
||||
const start = seq * payloadSize;
|
||||
const end = Math.min(start + payloadSize, totalSize);
|
||||
const payload = fileBytes.slice(start, end);
|
||||
@@ -1436,6 +1482,12 @@
|
||||
bleLog('OTA', `📊 文件 SHA-256: ${fileSha256.toUpperCase()}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
// ★ 中止不算失败:0x20 ABORTED handler 已更新 UI(progress/stage/toast),
|
||||
// 这里只记录日志并返回 false,避免把"用户中止"误报为写入失败。
|
||||
if (state.abortRequested || state.lastUploadStatus === 'aborted') {
|
||||
bleLog('OTA', `写入已中止: ${err.message}`, 'W');
|
||||
return false;
|
||||
}
|
||||
state.lastUploadStatus = 'failed';
|
||||
state.expectedReboot = false; // ★ 刷写失败,取消重连退避
|
||||
state.reconnecting = false;
|
||||
@@ -1443,23 +1495,9 @@
|
||||
bleLog('OTA', `写入失败: ${err.message}`, 'E');
|
||||
setProgress(0, `写入失败: ${err.message}`);
|
||||
setBleStageError(err.message);
|
||||
// ★ BLE OTA 失败/中止后强制断开 BLE 连接,并等待实际断连完成:
|
||||
// 触发 ESP32 后端的 GAP disconnect 处理器 → ble_esp32_ota_abort(),
|
||||
// 清理 OTA 会话(Esp32OtaCore::abort + reorder buffer 释放 + 协调器状态复位)。
|
||||
// 等待 gattserverdisconnected 事件确保断连真正生效,避免用户快速重试时
|
||||
// 旧会话尚未清理导致 seq=0 被误判为重传。
|
||||
if (state.server && state.device && state.device.gatt && state.device.gatt.connected) {
|
||||
bleLog('OTA', '刷写失败,断开 BLE 以清理 ESP32 OTA 会话...', 'W');
|
||||
try {
|
||||
await new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = () => { if (!done) { done = true; resolve(); } };
|
||||
state.device.addEventListener('gattserverdisconnected', finish, { once: true });
|
||||
setTimeout(finish, 1000); // 1 秒兜底超时
|
||||
state.device.gatt.disconnect();
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
// ★ BLE OTA 失败后不再强制断开 BLE:错误 FINISH 已通过 0xFB 路径或设备端
|
||||
// 自动 abort 清理了 ESP32 OTA 会话。保留连接让用户可以快速重试或查看日志。
|
||||
// 仅当连接已半死(gatt.connected=false)时由 onDisconnected 走重连路径。
|
||||
return false;
|
||||
} finally {
|
||||
if (state.ackWaiter) {
|
||||
@@ -1511,12 +1549,8 @@
|
||||
const ok = await uploadFirmware();
|
||||
if (!ok || state.abortRequested) {
|
||||
bleLog('AUTO', '一键刷写中止', 'W');
|
||||
// ★ 确保 BLE 已断开(触发 ESP32 OTA 会话清理)
|
||||
// uploadFirmware catch 块已尝试断开,这里做安全网兜底
|
||||
if (state.device && state.device.gatt && state.device.gatt.connected) {
|
||||
bleLog('AUTO', '断开 BLE 以清理 ESP32 OTA 会话...', 'W');
|
||||
try { state.device.gatt.disconnect(); } catch (_) {}
|
||||
}
|
||||
// ★ 中止/失败后不再断开 BLE:0xFB 已让设备端清理 OTA 会话,保留连接方便重试。
|
||||
// 若连接已半死,onDisconnected 会自动走重连路径。
|
||||
return;
|
||||
}
|
||||
if (cfg.label === 'ESP32') {
|
||||
@@ -1535,10 +1569,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** @description 中止当前 BLE OTA 任务并清理状态。 */
|
||||
function abortTask() {
|
||||
state.abortRequested = true;
|
||||
bleLog('OTA', '已请求中止,等待当前分包返回');
|
||||
/**
|
||||
* @description 中止当前 BLE OTA 任务并清理状态。
|
||||
* 改进:发 0xFB 控制命令让 ESP32 主动清理 OTA 会话,BLE 保持连接。
|
||||
* 旧做法是直接断开 BLE 触发 GAP disconnect 清理,导致用户需重连。
|
||||
* 若 0xFB 发送失败(如 CTRL 特征不可用),回退到设置标志等待发包循环自然退出。
|
||||
*/
|
||||
async function abortTask() {
|
||||
state.abortRequested = true; // 立即停止发送循环
|
||||
bleLog('OTA', '已请求中止,发送 0xFB 让设备端清理(BLE 保持连接)');
|
||||
|
||||
if (state.ctrlChar && state.server && state.device.gatt.connected) {
|
||||
try {
|
||||
const session = state.sessionId || 0;
|
||||
const abortCmd = new Uint8Array([BLE_OTA_CTRL_ABORT_OTA, session & 0xFF, (session >> 8) & 0xFF]);
|
||||
await state.ctrlChar.writeValue(abortCmd);
|
||||
bleLog('OTA', '0xFB 已发送,等待设备端回复 ABORTED (stage=0x20)');
|
||||
} catch (e) {
|
||||
bleLog('OTA', '0xFB 发送失败: ' + e.message + ',回退到本地中止', 'E');
|
||||
// CTRL 写入失败时回退:本地立即标记失败,不再等设备端 ABORTED 回复
|
||||
state.lastUploadStatus = 'failed';
|
||||
if (state.ackWaiter) {
|
||||
const waiter = state.ackWaiter;
|
||||
state.ackWaiter = null;
|
||||
clearTimeout(waiter.timer);
|
||||
waiter.reject(new Error('中止失败: ' + e.message));
|
||||
}
|
||||
rejectOtaResult('中止失败: ' + e.message, { stage: 0xFB, value: 0 });
|
||||
}
|
||||
} else {
|
||||
// CTRL 特征未就绪(连接已断或未完成握手),仅本地清理
|
||||
bleLog('OTA', 'CTRL 特征不可用,仅本地中止', 'W');
|
||||
state.lastUploadStatus = 'failed';
|
||||
rejectOtaResult('CTRL 特征不可用', { stage: 0xFB, value: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// showToast 使用 app.js 中的全局版本
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
index.html - ESP32 OTA 中继控制台主页面
|
||||
支持 WiFi OTA(ESP32 本机 + GD32 中继)和 BLE OTA 双通道固件升级。
|
||||
配套脚本:app.js (WiFi OTA), ble_ota.js (BLE OTA), style.css (UI 样式)
|
||||
@@ -9,7 +9,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ESP32 OTA 中继控制台</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260619c">
|
||||
<link rel="stylesheet" href="style.css?v=20260625b">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -426,7 +426,7 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=20260623a"></script>
|
||||
<script src="app.js?v=20260626a"></script>
|
||||
<script>
|
||||
window.connectionMode = window.connectionMode || 'wifi';
|
||||
window.flashTarget = window.flashTarget || 'esp32';
|
||||
@@ -678,6 +678,6 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
applySharedModeUi();
|
||||
})();
|
||||
</script>
|
||||
<script src="ble_ota.js?v=20260625a"></script>
|
||||
<script src="ble_ota.js?v=20260626a"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+204
@@ -1504,3 +1504,207 @@ code { color: #d3f9ff; font-family: var(--font-mono); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===== ENHANCED ANIMATIONS (v20260625b) ===== */
|
||||
|
||||
/* Global keyframes */
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes popIn {
|
||||
0% { opacity: 0; transform: scale(.85); }
|
||||
60% { opacity: 1; transform: scale(1.04); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
@keyframes ringPulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(34,211,238,.55); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(34,211,238,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(34,211,238,0); }
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-4px); }
|
||||
40% { transform: translateX(4px); }
|
||||
60% { transform: translateX(-3px); }
|
||||
80% { transform: translateX(3px); }
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes floatY {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
@keyframes glowBreath {
|
||||
0%, 100% { box-shadow: 0 0 8px var(--accent-dim); }
|
||||
50% { box-shadow: 0 0 18px rgba(34,211,238,.45); }
|
||||
}
|
||||
@keyframes flowDash {
|
||||
to { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Page entrance — header + cards staggered */
|
||||
.header-bar { animation: fadeInUp .45s ease both; }
|
||||
.main-layout > div > .card { animation: fadeInUp .5s ease both; }
|
||||
.main-layout > div:nth-child(1) > .card:nth-child(1) { animation-delay: .05s; }
|
||||
.main-layout > div:nth-child(1) > .card:nth-child(2) { animation-delay: .12s; }
|
||||
.main-layout > div:nth-child(1) > .card:nth-child(3) { animation-delay: .19s; }
|
||||
.main-layout > div:nth-child(1) > .card:nth-child(4) { animation-delay: .26s; }
|
||||
.main-layout > div:nth-child(2) > .card:nth-child(1) { animation-delay: .08s; }
|
||||
.main-layout > div:nth-child(2) > .card:nth-child(2) { animation-delay: .15s; }
|
||||
.main-layout > div:nth-child(2) > .card:nth-child(3) { animation-delay: .22s; }
|
||||
.main-layout > div:nth-child(2) > .card:nth-child(4) { animation-delay: .29s; }
|
||||
|
||||
/* Card hover — lift + glow */
|
||||
.card {
|
||||
transition: transform .25s ease, border-color .25s ease, box-shadow .25s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(34,211,238,.28);
|
||||
box-shadow: 0 10px 28px rgba(0,0,0,.35), 0 0 0 1px rgba(34,211,238,.08);
|
||||
}
|
||||
|
||||
/* Brand dot — gentle ring pulse */
|
||||
.header-brand .dot-mark {
|
||||
animation: ringPulse 2.2s ease-out infinite, glowBreath 3.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Pills — subtle hover */
|
||||
.pill { transition: transform .2s ease, border-color .2s ease, background .2s ease; }
|
||||
.pill:hover { transform: translateY(-1px); border-color: rgba(34,211,238,.3); }
|
||||
|
||||
/* Button — press + sheen on hover for primary */
|
||||
.btn { position: relative; overflow: hidden; }
|
||||
.btn:active:not(:disabled) { transform: translateY(0) scale(.97); transition: transform .08s ease; }
|
||||
.btn-primary::before,
|
||||
.btn-success::before,
|
||||
.btn-danger::before {
|
||||
content: "";
|
||||
position: absolute; top: 0; left: -120%;
|
||||
width: 60%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
|
||||
transform: skewX(-20deg);
|
||||
transition: left .55s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled)::before,
|
||||
.btn-success:hover:not(:disabled)::before,
|
||||
.btn-danger:hover:not(:disabled)::before {
|
||||
left: 130%;
|
||||
}
|
||||
|
||||
/* Upload zone — drag pulse + icon float */
|
||||
.upload-zone { transition: border-color .2s, background .2s, transform .2s; }
|
||||
.upload-zone:hover { transform: translateY(-1px); }
|
||||
.upload-zone .upload-ico {
|
||||
display: inline-block;
|
||||
transition: transform .25s ease;
|
||||
}
|
||||
.upload-zone:hover .upload-ico { animation: floatY 1.6s ease-in-out infinite; }
|
||||
.upload-zone.dragover { animation: ringPulse 1.2s ease-out infinite; }
|
||||
|
||||
/* File info appearance */
|
||||
.file-info.show { animation: fadeInUp .3s ease both; }
|
||||
|
||||
/* Progress bar — striped flow on active */
|
||||
.progress-fill {
|
||||
background-image:
|
||||
linear-gradient(90deg, #22d3ee, #06b6d4),
|
||||
repeating-linear-gradient(45deg,
|
||||
rgba(255,255,255,.18) 0 10px,
|
||||
rgba(255,255,255,0) 10px 20px);
|
||||
background-size: 100% 100%, 200% 100%;
|
||||
background-blend-mode: normal, overlay;
|
||||
animation: flowDash 1.4s linear infinite;
|
||||
}
|
||||
.progress-fill.done { animation: none; }
|
||||
|
||||
/* Phase step capsule — pop when becoming active/done */
|
||||
.pp-step.active,
|
||||
.pp-step.done { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
|
||||
|
||||
/* Stage flow — connector grow + dot pop */
|
||||
.stage-node:not(:last-child)::after {
|
||||
background: linear-gradient(90deg, var(--accent) 0%, var(--accent) 50%, var(--border) 50%, var(--border) 100%);
|
||||
background-size: 200% 100%;
|
||||
background-position: 100% 0;
|
||||
transition: background-position .5s ease;
|
||||
}
|
||||
.stage-node.done:not(:last-child)::after,
|
||||
.stage-node.live:not(:last-child)::after {
|
||||
background-position: 0% 0;
|
||||
}
|
||||
.stage-dot { transition: transform .3s ease, background .3s, border-color .3s, color .3s, box-shadow .3s; }
|
||||
.stage-node.live .stage-dot {
|
||||
animation: ringPulse 1.6s ease-out infinite;
|
||||
}
|
||||
.stage-node.done .stage-dot { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
|
||||
.stage-node.err .stage-dot { animation: shake .4s ease both; }
|
||||
|
||||
/* Stage status bar — slide in when state changes */
|
||||
.stage-status-bar.active,
|
||||
.stage-status-bar.ok,
|
||||
.stage-status-bar.err { animation: fadeInUp .3s ease both; }
|
||||
|
||||
/* Stage result panel */
|
||||
.stage-result { animation: popIn .4s cubic-bezier(.4,1.4,.5,1) both; }
|
||||
|
||||
/* Metric / badge values — number tick highlight */
|
||||
.metric .v,
|
||||
.badge-stat .v {
|
||||
transition: color .3s ease, transform .3s ease;
|
||||
}
|
||||
.metric:hover,
|
||||
.badge-stat:hover {
|
||||
border-color: rgba(34,211,238,.25);
|
||||
transition: border-color .25s ease;
|
||||
}
|
||||
|
||||
/* Conn indicator — light ring pulse when warn */
|
||||
.conn-indicator { transition: background .25s ease, border-color .25s ease; }
|
||||
.conn-indicator.warn .light { animation: ringPulse 1.4s ease-out infinite; }
|
||||
.conn-indicator.ok .light { animation: glowBreath 3s ease-in-out infinite; }
|
||||
|
||||
/* Heap / task bars — sheen */
|
||||
.heap-bar-track,
|
||||
.status-task-bar { position: relative; overflow: hidden; }
|
||||
.heap-bar-track::after,
|
||||
.status-task-bar::after {
|
||||
content: "";
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.12) 50%, transparent 100%);
|
||||
transform: translateX(-100%);
|
||||
animation: shimmer 2.6s linear infinite;
|
||||
background-size: 200% 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.heap-bar-fill,
|
||||
.status-task-bar-fill { transition: width .6s cubic-bezier(.4,0,.2,1); }
|
||||
|
||||
/* Log lines — fresh entry fade-in */
|
||||
.log-line { animation: fadeIn .25s ease both; }
|
||||
|
||||
/* Toast — pop and float */
|
||||
.toast.show { animation: popIn .35s cubic-bezier(.4,1.4,.5,1) both; }
|
||||
|
||||
/* Reduced motion — respect user preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: .001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .001ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user