feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制
移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2 CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发 实现流量控制,提高OTA传输稳定性。 refactor(stack): 统一任务栈大小配置并添加CPU统计功能 - 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配 - 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样 - 更新main.cpp中的任务创建使用新的栈大小常量 - 调整health_monitor和ota_relay等任务的栈分配 perf(ota): 改进ESP32和GD32 OTA分区预擦策略 - ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞 - GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量 - 优化OTA协调器状态管理,修复FINALIZING状态残留问题 fix(log): 改进UDP日志发送的堆内存管理 - 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer - 降低UDP发送跳过的内存阈值从10KB到6KB - 降低日志排水最小内存阈值从8KB到4KB docs(readme): 更新OTA配置参数文档说明 移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为 基于CREDIT背压机制的描述。
This commit is contained in:
@@ -433,9 +433,8 @@ UDP socket 延迟创建(首次 `send()` 时才分配),WiFi 未连接时 `s
|
|||||||
| OTA | `OTA_BLOCK_SIZE` | 8192 | 读写块大小 |
|
| OTA | `OTA_BLOCK_SIZE` | 8192 | 读写块大小 |
|
||||||
| OTA | `OTA_MAX_FIRMWARE_SIZE` | 216KB | GD32 固件上限 |
|
| OTA | `OTA_MAX_FIRMWARE_SIZE` | 216KB | GD32 固件上限 |
|
||||||
| OTA | `GD32_OTA_PACKET_SIZE` | 48 | 每包载荷字节 |
|
| OTA | `GD32_OTA_PACKET_SIZE` | 48 | 每包载荷字节 |
|
||||||
| OTA | `OTA_RESEND_RETRIES` | 10 | 单包最大重发次数 |
|
|
||||||
| OTA | `OTA_PACKET_HEALTH_CHECK_INTERVAL` | 32 | 每 N 包健康检查 |
|
| OTA | `OTA_PACKET_HEALTH_CHECK_INTERVAL` | 32 | 每 N 包健康检查 |
|
||||||
| OTA | `OTA_PACKET_DELAY_MS` | 5 | 每包发送后延时(ms) |
|
| OTA | 包间节奏 | CREDIT 背压 | `SpiMaster`:`remoteCredit==0` 时 wait/拒发,无固定包间 delay |
|
||||||
| OTA | `OTA_WIFI_RSSI_CRITICAL` | -88 dBm | OTA 期间 RSSI 危险阈值 |
|
| OTA | `OTA_WIFI_RSSI_CRITICAL` | -88 dBm | OTA 期间 RSSI 危险阈值 |
|
||||||
| OTA | `OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS` | 15000 | OTA 前 RSSI 采样最大时效 |
|
| OTA | `OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS` | 15000 | OTA 前 RSSI 采样最大时效 |
|
||||||
| 阈值 | `TEMP_WARNING` | 80°C | 温度警告阈值 |
|
| 阈值 | `TEMP_WARNING` | 80°C | 温度警告阈值 |
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ idf_component_register(
|
|||||||
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
|
"server/http_handlers.cpp" # HTTP 路由处理(H2 从 wifi_ota 提取)
|
||||||
"server/ble_ota.cpp" # BLE GATT OTA 服务器
|
"server/ble_ota.cpp" # BLE GATT OTA 服务器
|
||||||
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存/栈水位主动巡检)
|
"monitor/health_monitor.cpp" # 系统健康监控(温度/RSSI/堆内存/栈水位主动巡检)
|
||||||
|
"monitor/cpu_stats.cpp" # 1Hz FreeRTOS CPU 占用采样任务
|
||||||
"monitor/can_feedback_task.cpp" # CAN 1Hz 反馈轮询任务(0xC1)
|
"monitor/can_feedback_task.cpp" # CAN 1Hz 反馈轮询任务(0xC1)
|
||||||
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
|
"monitor/debug_log.cpp" # 三路调试日志(UART/UDP/环形缓冲)
|
||||||
"monitor/task_stack_check.cpp" # 任务栈水位指标收集(health_mon 巡检 + /system/status 共享)
|
"monitor/task_stack_check.cpp" # 任务栈水位指标收集(health_mon 巡检 + /system/status 共享)
|
||||||
|
|||||||
+5
-13
@@ -10,25 +10,17 @@ menu "DCU OTA Configuration"
|
|||||||
validated against actual wiring length/quality and GD32
|
validated against actual wiring length/quality and GD32
|
||||||
SPI slave timing.
|
SPI slave timing.
|
||||||
|
|
||||||
config DCU_OTA_PACKET_INTERVAL_MS
|
|
||||||
int "Delay between GD32 OTA packets (ms)"
|
|
||||||
default 1
|
|
||||||
range 0 50
|
|
||||||
help
|
|
||||||
Idle delay inserted after each successfully acknowledged
|
|
||||||
firmware packet before sending the next one.
|
|
||||||
Lower value speeds up OTA, but if GD32 cannot finish
|
|
||||||
flash write / prepare next ACK in time, set it back.
|
|
||||||
|
|
||||||
config DCU_OTA_POLL_BACKOFF_MS
|
config DCU_OTA_POLL_BACKOFF_MS
|
||||||
int "Polling backoff when GD32 is not ready (ms)"
|
int "Polling backoff when GD32 is not ready (ms)"
|
||||||
default 2
|
default 2
|
||||||
range 0 50
|
range 0 50
|
||||||
help
|
help
|
||||||
Delay between SPI response polls when GD32 returns WAIT
|
Delay between SPI response polls when GD32 returns WAIT
|
||||||
or does not drive MISO yet. Lower value reduces per-packet
|
or does not drive MISO yet. Packet pacing uses SPI Link v2
|
||||||
idle time and can significantly speed up OTA, but if GD32
|
CREDIT backpressure in SpiMaster (remoteCredit==0 → wait/
|
||||||
cannot finish flash write before the next poll, increase it.
|
refuse TX), not a fixed inter-packet delay. Lower value
|
||||||
|
reduces poll idle time; increase if GD32 needs more time
|
||||||
|
between polls.
|
||||||
|
|
||||||
config DCU_OTA_FINISH_TIMEOUT_MS
|
config DCU_OTA_FINISH_TIMEOUT_MS
|
||||||
int "Timeout for FINISH_OTA command (ms)"
|
int "Timeout for FINISH_OTA command (ms)"
|
||||||
|
|||||||
+14
-11
@@ -14,6 +14,7 @@
|
|||||||
#include "esp_ota_ops.h"
|
#include "esp_ota_ops.h"
|
||||||
|
|
||||||
#include "monitor/health_monitor.h"
|
#include "monitor/health_monitor.h"
|
||||||
|
#include "monitor/cpu_stats.h"
|
||||||
#include "relay/ota_relay.h"
|
#include "relay/ota_relay.h"
|
||||||
#include "ota/ota_coordinator.h"
|
#include "ota/ota_coordinator.h"
|
||||||
#include "transport/spi_master.h"
|
#include "transport/spi_master.h"
|
||||||
@@ -33,7 +34,7 @@ extern "C" void app_main(void)
|
|||||||
esp_chip_info_t chip_info;
|
esp_chip_info_t chip_info;
|
||||||
esp_chip_info(&chip_info);
|
esp_chip_info(&chip_info);
|
||||||
|
|
||||||
// ★ M1: ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore(替代旧的全局变量)
|
// ESP32 OTA 分区互斥锁内聚到 Esp32OtaCore
|
||||||
Esp32OtaCore::initMutex();
|
Esp32OtaCore::initMutex();
|
||||||
|
|
||||||
// 初始化网络接口 + 事件循环(必须最早)
|
// 初始化网络接口 + 事件循环(必须最早)
|
||||||
@@ -43,7 +44,7 @@ extern "C" void app_main(void)
|
|||||||
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
|
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
|
||||||
DebugLog::instance().init();
|
DebugLog::instance().init();
|
||||||
|
|
||||||
// ★ WiFi 提前初始化(调度器启动前)
|
// WiFi 提前初始化(调度器启动前)
|
||||||
bool wifi_sta_ok = WifiOta::instance().earlyInit();
|
bool wifi_sta_ok = WifiOta::instance().earlyInit();
|
||||||
|
|
||||||
HealthMonitor::instance().init();
|
HealthMonitor::instance().init();
|
||||||
@@ -61,7 +62,7 @@ extern "C" void app_main(void)
|
|||||||
xRet = xTaskCreatePinnedToCore(
|
xRet = xTaskCreatePinnedToCore(
|
||||||
HealthMonitor::taskEntry,
|
HealthMonitor::taskEntry,
|
||||||
"health_mon",
|
"health_mon",
|
||||||
4096,
|
TASK_STACK_HEALTH_MON,
|
||||||
NULL,
|
NULL,
|
||||||
configMAX_PRIORITIES - 4,
|
configMAX_PRIORITIES - 4,
|
||||||
NULL,
|
NULL,
|
||||||
@@ -76,12 +77,15 @@ extern "C" void app_main(void)
|
|||||||
DEBUG_INFO("MAIN", "健康监控任务已创建");
|
DEBUG_INFO("MAIN", "健康监控任务已创建");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CPU 占用 1Hz 采样(非致命;失败时 /system/status 的 cpu.valid 保持 false) */
|
||||||
|
CpuStats::instance().start();
|
||||||
|
|
||||||
Protocol::instance().init();
|
Protocol::instance().init();
|
||||||
DEBUG_INFO("MAIN", "协议层初始化完成");
|
DEBUG_INFO("MAIN", "协议层初始化完成");
|
||||||
|
|
||||||
// ★ 全局 OTA 协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
|
// OTA 全局协调器初始化(在 OTA task 创建前,确保 mutex 就绪)
|
||||||
OtaCoordinator::instance().init();
|
OtaCoordinator::instance().init();
|
||||||
// ★ L1: 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener)
|
// 注册 BLE 射频共存监听器(WifiOta 实现 IBleCoexListener)
|
||||||
OtaCoordinator::instance().setBleCoexListener(&WifiOta::instance());
|
OtaCoordinator::instance().setBleCoexListener(&WifiOta::instance());
|
||||||
DEBUG_INFO("MAIN", "OTA 全局协调器已初始化");
|
DEBUG_INFO("MAIN", "OTA 全局协调器已初始化");
|
||||||
|
|
||||||
@@ -96,7 +100,7 @@ extern "C" void app_main(void)
|
|||||||
xRet = xTaskCreatePinnedToCore(
|
xRet = xTaskCreatePinnedToCore(
|
||||||
OtaRelay::taskEntry,
|
OtaRelay::taskEntry,
|
||||||
"ota_relay",
|
"ota_relay",
|
||||||
6144,
|
TASK_STACK_OTA_RELAY,
|
||||||
NULL,
|
NULL,
|
||||||
configMAX_PRIORITIES - 2,
|
configMAX_PRIORITIES - 2,
|
||||||
NULL,
|
NULL,
|
||||||
@@ -115,7 +119,7 @@ extern "C" void app_main(void)
|
|||||||
xRet = xTaskCreatePinnedToCore(
|
xRet = xTaskCreatePinnedToCore(
|
||||||
WifiOta::taskEntry,
|
WifiOta::taskEntry,
|
||||||
"wifi_ota",
|
"wifi_ota",
|
||||||
8192,
|
TASK_STACK_WIFI_OTA,
|
||||||
NULL,
|
NULL,
|
||||||
configMAX_PRIORITIES - 3,
|
configMAX_PRIORITIES - 3,
|
||||||
NULL,
|
NULL,
|
||||||
@@ -134,7 +138,7 @@ extern "C" void app_main(void)
|
|||||||
xRet = xTaskCreatePinnedToCore(
|
xRet = xTaskCreatePinnedToCore(
|
||||||
BleOta::taskEntry,
|
BleOta::taskEntry,
|
||||||
"ble_ota",
|
"ble_ota",
|
||||||
4096,
|
TASK_STACK_BLE_OTA,
|
||||||
NULL,
|
NULL,
|
||||||
configMAX_PRIORITIES - 3,
|
configMAX_PRIORITIES - 3,
|
||||||
NULL,
|
NULL,
|
||||||
@@ -158,7 +162,7 @@ extern "C" void app_main(void)
|
|||||||
DEBUG_WARN("MAIN", "CAN 反馈轮询任务创建失败(非致命,OTA 不受影响)");
|
DEBUG_WARN("MAIN", "CAN 反馈轮询任务创建失败(非致命,OTA 不受影响)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ OTA 回滚保护:标记当前固件有效
|
// OTA 回滚保护:标记当前固件有效
|
||||||
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
|
// 启用 CONFIG_APP_ROLLBACK_ENABLE 后,新固件首次启动必须调用此函数
|
||||||
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
|
// 若未调用就崩溃/重启,bootloader 会自动回退到上一个有效分区
|
||||||
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
|
esp_err_t ota_valid_err = esp_ota_mark_app_valid_cancel_rollback();
|
||||||
@@ -171,8 +175,7 @@ extern "C" void app_main(void)
|
|||||||
DEBUG_INFO("MAIN", "========================================");
|
DEBUG_INFO("MAIN", "========================================");
|
||||||
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
||||||
|
|
||||||
// ★ 启动最后步骤:读取 GD32 APP 版本号
|
// 启动最后步骤:读取 GD32 APP 版本号
|
||||||
// 参考 OtaRelay::doWaitForAppAlive 刷写后读取逻辑:
|
|
||||||
// 先 hello() 建立 SPI v2 会话,再 probeGd32AppOnce 读取版本
|
// 先 hello() 建立 SPI v2 会话,再 probeGd32AppOnce 读取版本
|
||||||
// GD32 APP 完整初始化需 5-6 秒,使用 20 次重试 × 300ms 窗口
|
// GD32 APP 完整初始化需 5-6 秒,使用 20 次重试 × 300ms 窗口
|
||||||
{
|
{
|
||||||
|
|||||||
+23
-10
@@ -44,22 +44,35 @@ inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4; ///< AP 模
|
|||||||
// HTTP 服务器配置
|
// HTTP 服务器配置
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服务器监听端口
|
inline constexpr uint16_t HTTP_SERVER_PORT = 80; ///< HTTP 服务器监听端口
|
||||||
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 10240; ///< HTTP 服务器任务栈大小(字节)
|
|
||||||
// ★ 历史:6144 栈溢出;8192 在上传收尾仍紧。曾提到 12288 但 ESP32-C3 堆极紧
|
// ============================================================
|
||||||
// (OTA 后 free~5KB、largest~1KB)。SHA context 已改 static,httpd 栈 10KB 折中。
|
// 任务栈大小(字节)— 2026-07 按板端 task_stack 水位收敛
|
||||||
// 真·栈溢出钩子会 abort;卡 100% 更多是堆碎导致回包/后续分配失败。
|
// 规则:peak_used 实测 + 至少 ~1.5–2KB 余量;httpd 路径最重单独放宽。
|
||||||
|
// 实测(空闲/常规):http peak~5KB, wifi_ota~1.4KB, ota_relay~1KB,
|
||||||
|
// health~1.2KB, cpu_stats~2KB, ble*~1KB, reconnect~1KB。
|
||||||
|
// ============================================================
|
||||||
|
inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; ///< httpd:原 10KB,peak~5KB → 8KB(余量~3KB)
|
||||||
|
inline constexpr uint32_t TASK_STACK_WIFI_OTA = 4096; ///< wifi_ota 主循环:原 8KB,peak~1.4KB
|
||||||
|
inline constexpr uint32_t TASK_STACK_OTA_RELAY = 2560; ///< ota_relay:2.5KB(实测 peak~1KB;完整 GD32 中继后再确认水位)
|
||||||
|
inline constexpr uint32_t TASK_STACK_SPI_WORKER = 5120; ///< spi_worker:无单独水位,6144→5120 小幅收
|
||||||
|
inline constexpr uint32_t TASK_STACK_BLE_OTA = 3072; ///< ble_ota 主任务:peak~1KB
|
||||||
|
inline constexpr uint32_t TASK_STACK_BLE_WORKER = 3072; ///< ble_ota_worker:peak~1.1KB(负载峰值曾~1.9KB)
|
||||||
|
inline constexpr uint32_t TASK_STACK_HEALTH_MON = 3072; ///< health_mon:peak~1.2KB
|
||||||
|
inline constexpr uint32_t TASK_STACK_CPU_STATS = 3072; ///< cpu_stats:peak~2KB
|
||||||
|
inline constexpr uint32_t TASK_STACK_WIFI_RECONNECT = 2560; ///< wifi_reconnect:peak~1KB
|
||||||
|
inline constexpr uint32_t TASK_STACK_CAN_FB = 3072; ///< can_fb:1Hz 轮询
|
||||||
|
inline constexpr uint32_t TASK_STACK_BLE_FINISH = 4096; ///< 按需:SHA finalize(保持)
|
||||||
|
inline constexpr uint32_t TASK_STACK_BLE_GD32_WAIT = 3072; ///< 按需:GD32 中继等待
|
||||||
|
inline constexpr uint32_t TASK_STACK_BLE_RESTART = 2048; ///< 按需:延时重启
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// OTA 配置
|
// OTA 配置
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr size_t OTA_BLOCK_SIZE = 2048; ///< 上传接收块:2KB 降峰值堆(原 4/8KB)
|
inline constexpr size_t OTA_BLOCK_SIZE = 2048; ///< 上传接收块:2KB 降峰值 BSS/堆(s_ota_upload_recv 静态占用)
|
||||||
|
// OTA_BLOCK_SIZE 演进:8KB(原)→2KB(压堆)→4KB(试吞吐)→2KB(2026-07 回退省 DRAM)。
|
||||||
|
// 4KB 时 s_ota_upload_recv BSS +2KB,开机 free/largest 更紧;回退 2KB 优先保内存。
|
||||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB = 221184B = 0x35F90,固件大小上限
|
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 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; // 每包重发次数上限
|
|
||||||
inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间
|
|
||||||
inline constexpr uint32_t OTA_POST_SEND_DELAY_MS = 3; // 发送请求后首次轮询前的延迟(ms),无 READY 拉低时防止读到 stale 数据
|
|
||||||
inline constexpr uint32_t OTA_POLL_NO_FRAME_DELAY_MS = 8; // 轮询未找到帧时的重试延迟(ms),无 READY 拉低时需更长
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 健康监控阈值
|
// 健康监控阈值
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
// 固件版本
|
// 固件版本
|
||||||
#define FW_VERSION_MAJOR 0
|
#define FW_VERSION_MAJOR 0
|
||||||
#define FW_VERSION_MINOR 3
|
#define FW_VERSION_MINOR 3
|
||||||
#define FW_VERSION_PATCH 46
|
#define FW_VERSION_PATCH 47
|
||||||
#define FW_VERSION_STRING "0.3.46"
|
#define FW_VERSION_STRING "0.3.47"
|
||||||
|
|
||||||
// 编译时间
|
// 编译时间
|
||||||
#define BUILD_TIME "2026-07-15 12:08:48"
|
#define BUILD_TIME "2026-07-16 01:10:05"
|
||||||
|
|
||||||
// Git 信息
|
// Git 信息
|
||||||
#define GIT_HASH "5a89cec"
|
#define GIT_HASH "a47a027"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
|
|
||||||
|
#include "app_config.h"
|
||||||
#include "transport/canfd_bridge.h"
|
#include "transport/canfd_bridge.h"
|
||||||
#include "transport/spi_master.h"
|
#include "transport/spi_master.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
@@ -130,7 +131,7 @@ bool startCanFeedbackTask()
|
|||||||
BaseType_t xRet = xTaskCreatePinnedToCore(
|
BaseType_t xRet = xTaskCreatePinnedToCore(
|
||||||
taskLoop,
|
taskLoop,
|
||||||
"can_fb",
|
"can_fb",
|
||||||
4096,
|
TASK_STACK_CAN_FB,
|
||||||
nullptr,
|
nullptr,
|
||||||
4,
|
4,
|
||||||
nullptr,
|
nullptr,
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* cpu_stats.cpp - 1Hz FreeRTOS CPU 占用采样任务
|
||||||
|
*/
|
||||||
|
#include "cpu_stats.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include "app_config.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "monitor/debug_log.h"
|
||||||
|
|
||||||
|
CpuStats& CpuStats::instance()
|
||||||
|
{
|
||||||
|
static CpuStats inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CpuStats::start()
|
||||||
|
{
|
||||||
|
if (m_started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_started = true;
|
||||||
|
|
||||||
|
#if !defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
|
||||||
|
portENTER_CRITICAL(&m_mux);
|
||||||
|
m_published = {};
|
||||||
|
m_published.available = false;
|
||||||
|
portEXIT_CRITICAL(&m_mux);
|
||||||
|
DEBUG_WARN("CPU", "未启用 CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS,跳过采样任务");
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
portENTER_CRITICAL(&m_mux);
|
||||||
|
m_published = {};
|
||||||
|
m_published.available = true;
|
||||||
|
m_published.valid = false;
|
||||||
|
portEXIT_CRITICAL(&m_mux);
|
||||||
|
|
||||||
|
BaseType_t ok = xTaskCreatePinnedToCore(
|
||||||
|
&CpuStats::taskEntry,
|
||||||
|
"cpu_stats",
|
||||||
|
TASK_STACK_CPU_STATS,
|
||||||
|
this,
|
||||||
|
tskIDLE_PRIORITY + 1, /* 低优先级,少搅动占用统计 */
|
||||||
|
&m_taskHandle,
|
||||||
|
0);
|
||||||
|
if (ok != pdPASS) {
|
||||||
|
m_taskHandle = nullptr;
|
||||||
|
m_started = false;
|
||||||
|
DEBUG_ERR("CPU", "创建 cpu_stats 任务失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DEBUG_INFO("CPU", "cpu_stats 任务已启动,采样周期 %" PRIu32 " ms", CPU_STATS_SAMPLE_MS);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void CpuStats::getSnapshot(CpuUsageSnapshot& out) const
|
||||||
|
{
|
||||||
|
portENTER_CRITICAL(&m_mux);
|
||||||
|
out = m_published;
|
||||||
|
portEXIT_CRITICAL(&m_mux);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
|
||||||
|
|
||||||
|
static uint64_t runtimeDelta(configRUN_TIME_COUNTER_TYPE cur, configRUN_TIME_COUNTER_TYPE prev)
|
||||||
|
{
|
||||||
|
return cur >= prev ? (uint64_t)(cur - prev) : 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool taskNameIsIdle(const char* name)
|
||||||
|
{
|
||||||
|
return name && strncmp(name, "IDLE", 4) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CpuStats::sampleOnce()
|
||||||
|
{
|
||||||
|
UBaseType_t task_count = uxTaskGetNumberOfTasks();
|
||||||
|
if (task_count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
task_count = std::min<UBaseType_t>(task_count + 2U, (UBaseType_t)CPU_TASK_SNAPSHOT_MAX);
|
||||||
|
|
||||||
|
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
|
||||||
|
if (!tasks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
|
||||||
|
UBaseType_t captured = uxTaskGetSystemState(tasks, task_count, &total_runtime);
|
||||||
|
const int64_t now_us = esp_timer_get_time();
|
||||||
|
|
||||||
|
if (captured == 0 || captured > task_count) {
|
||||||
|
DEBUG_WARN("CPU", "uxTaskGetSystemState 异常: captured=%lu buf=%lu",
|
||||||
|
(unsigned long)captured, (unsigned long)task_count);
|
||||||
|
free(tasks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CpuUsageSnapshot snap = {};
|
||||||
|
snap.available = true;
|
||||||
|
|
||||||
|
const bool had_previous = m_hasLast;
|
||||||
|
const configRUN_TIME_COUNTER_TYPE previous_total = m_lastTotal;
|
||||||
|
const int64_t previous_time_us = m_lastTimeUs;
|
||||||
|
const size_t previous_count = m_lastCount;
|
||||||
|
TaskSample previous_samples[CPU_TASK_SNAPSHOT_MAX];
|
||||||
|
for (size_t i = 0; i < previous_count && i < CPU_TASK_SNAPSHOT_MAX; ++i) {
|
||||||
|
previous_samples[i] = m_lastSamples[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 先更新基线,再算本窗口(与旧 HTTP 路径语义一致) */
|
||||||
|
size_t copy_count = std::min<size_t>((size_t)captured, CPU_TASK_SNAPSHOT_MAX);
|
||||||
|
for (size_t i = 0; i < copy_count; ++i) {
|
||||||
|
m_lastSamples[i].task_number = tasks[i].xTaskNumber;
|
||||||
|
m_lastSamples[i].runtime = tasks[i].ulRunTimeCounter;
|
||||||
|
}
|
||||||
|
m_lastCount = copy_count;
|
||||||
|
m_lastTotal = total_runtime;
|
||||||
|
m_lastTimeUs = now_us;
|
||||||
|
m_hasLast = true;
|
||||||
|
|
||||||
|
const bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
|
||||||
|
const uint64_t total_delta = can_compute ? runtimeDelta(total_runtime, previous_total) : 0U;
|
||||||
|
uint64_t idle_delta = 0U;
|
||||||
|
|
||||||
|
if (can_compute && total_delta > 0U) {
|
||||||
|
for (UBaseType_t i = 0; i < captured; ++i) {
|
||||||
|
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
|
||||||
|
bool found = false;
|
||||||
|
for (size_t j = 0; j < previous_count; ++j) {
|
||||||
|
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
|
||||||
|
last_runtime = previous_samples[j].runtime;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint64_t delta = runtimeDelta(tasks[i].ulRunTimeCounter, last_runtime);
|
||||||
|
double pct = (double)delta * 100.0 / (double)total_delta;
|
||||||
|
if (taskNameIsIdle(tasks[i].pcTaskName)) {
|
||||||
|
idle_delta += delta;
|
||||||
|
}
|
||||||
|
if (snap.task_count < CPU_TASK_SNAPSHOT_MAX) {
|
||||||
|
CpuTaskMetric& m = snap.tasks[snap.task_count++];
|
||||||
|
const char* n = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
|
||||||
|
strncpy(m.name, n, sizeof(m.name) - 1);
|
||||||
|
m.name[sizeof(m.name) - 1] = '\0';
|
||||||
|
m.task_number = tasks[i].xTaskNumber;
|
||||||
|
m.runtime_delta = delta;
|
||||||
|
m.usage_pct = pct;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snap.valid = true;
|
||||||
|
snap.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
|
||||||
|
snap.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
|
||||||
|
snap.usage_pct = std::max(0.0, 100.0 - snap.idle_pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(tasks);
|
||||||
|
|
||||||
|
portENTER_CRITICAL(&m_mux);
|
||||||
|
m_published = snap;
|
||||||
|
portEXIT_CRITICAL(&m_mux);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CpuStats::taskEntry(void* arg)
|
||||||
|
{
|
||||||
|
auto* self = static_cast<CpuStats*>(arg);
|
||||||
|
DEBUG_INFO("CPU", "=== cpu_stats 采样任务启动 ===");
|
||||||
|
|
||||||
|
TickType_t last_wake = xTaskGetTickCount();
|
||||||
|
const TickType_t period = pdMS_TO_TICKS(CPU_STATS_SAMPLE_MS);
|
||||||
|
|
||||||
|
/* 第一拍只建基线(valid=false);此后每拍窗口 ≈ 1000ms */
|
||||||
|
for (;;) {
|
||||||
|
self->sampleOnce();
|
||||||
|
vTaskDelayUntil(&last_wake, period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else /* !CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS */
|
||||||
|
|
||||||
|
void CpuStats::sampleOnce() {}
|
||||||
|
void CpuStats::taskEntry(void*) { vTaskDelete(nullptr); }
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* cpu_stats.h - FreeRTOS 运行时 CPU 占用采样
|
||||||
|
*
|
||||||
|
* 由独立低优先级任务按固定 1s 周期采样;/system/status 只读最新快照,
|
||||||
|
* 不再在 HTTP 线程里调用 uxTaskGetSystemState(避免窗口绑浏览器轮询、减 httpd 栈压)。
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
/** 单次快照最多跟踪的任务数 */
|
||||||
|
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
|
||||||
|
|
||||||
|
/** 采样周期(ms)— 窗口 sample_ms 应对齐此值 */
|
||||||
|
static constexpr uint32_t CPU_STATS_SAMPLE_MS = 1000;
|
||||||
|
|
||||||
|
struct CpuTaskMetric {
|
||||||
|
char name[configMAX_TASK_NAME_LEN];
|
||||||
|
UBaseType_t task_number;
|
||||||
|
uint64_t runtime_delta;
|
||||||
|
double usage_pct;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CpuUsageSnapshot {
|
||||||
|
bool available; ///< 固件是否启用 RUN_TIME_STATS
|
||||||
|
bool valid; ///< 是否已有完整窗口(至少两拍)
|
||||||
|
uint32_t sample_ms; ///< 本窗口墙钟长度
|
||||||
|
double usage_pct; ///< 100 - idle
|
||||||
|
double idle_pct;
|
||||||
|
CpuTaskMetric tasks[CPU_TASK_SNAPSHOT_MAX];
|
||||||
|
size_t task_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief CPU 运行时统计(单例 + 1Hz 采样任务)
|
||||||
|
*/
|
||||||
|
class CpuStats {
|
||||||
|
public:
|
||||||
|
static CpuStats& instance();
|
||||||
|
|
||||||
|
/** 创建采样任务(幂等;失败仅打日志,不阻塞启动) */
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/** 线程安全拷贝最新快照(HTTP 用) */
|
||||||
|
void getSnapshot(CpuUsageSnapshot& out) const;
|
||||||
|
|
||||||
|
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
CpuStats() = default;
|
||||||
|
|
||||||
|
static void taskEntry(void* arg);
|
||||||
|
void sampleOnce();
|
||||||
|
|
||||||
|
mutable portMUX_TYPE m_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
CpuUsageSnapshot m_published = {};
|
||||||
|
TaskHandle_t m_taskHandle = nullptr;
|
||||||
|
bool m_started = false;
|
||||||
|
|
||||||
|
/* 基线(仅采样任务写) */
|
||||||
|
struct TaskSample {
|
||||||
|
UBaseType_t task_number;
|
||||||
|
configRUN_TIME_COUNTER_TYPE runtime;
|
||||||
|
};
|
||||||
|
TaskSample m_lastSamples[CPU_TASK_SNAPSHOT_MAX] = {};
|
||||||
|
size_t m_lastCount = 0;
|
||||||
|
configRUN_TIME_COUNTER_TYPE m_lastTotal = 0;
|
||||||
|
int64_t m_lastTimeUs = 0;
|
||||||
|
bool m_hasLast = false;
|
||||||
|
};
|
||||||
@@ -367,7 +367,8 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
|
/* UDP 通道:堆紧时跳过 sendto,避免申请 LWIP TX pbuf 与 HTTP 回包抢堆。
|
||||||
* WiFi 未连接时 sendto 失败也静默跳过。 */
|
* WiFi 未连接时 sendto 失败也静默跳过。
|
||||||
|
* ★ 修复:堆不足跳过时写一条告警到 ring buffer,让前端 /logs 可见。 */
|
||||||
if (m_initialized && free_now >= kSkipUdpFree) {
|
if (m_initialized && free_now >= kSkipUdpFree) {
|
||||||
if (m_udpSock < 0) {
|
if (m_udpSock < 0) {
|
||||||
ensureUdpSocket();
|
ensureUdpSocket();
|
||||||
@@ -381,6 +382,16 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
|||||||
udp_ret = ESP_OK;
|
udp_ret = ESP_OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (m_initialized) {
|
||||||
|
/* 堆不足导致 UDP 被跳过——通过 ring buffer 记录,避免上位机无法感知 */
|
||||||
|
static uint32_t s_lastUdpSkipWarnMs = 0;
|
||||||
|
uint32_t now_ms = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||||
|
/* 限制告警频率:每 10 秒最多一条,避免刷屏 */
|
||||||
|
if ((now_ms - s_lastUdpSkipWarnMs) > 10000) {
|
||||||
|
s_lastUdpSkipWarnMs = now_ms;
|
||||||
|
static const char kUdpSkipMsg[] = "[W][LOG] UDP 发送已暂停: 堆内存不足\n";
|
||||||
|
ringPush(kUdpSkipMsg, sizeof(kUdpSkipMsg) - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同时写入环形缓冲区(供 HTTP /logs 拉取)——静态,不占堆
|
// 同时写入环形缓冲区(供 HTTP /logs 拉取)——静态,不占堆
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ private:
|
|||||||
* free < kDrainMinFree → /logs 尽量仍返回 ring(降低门槛,避免“沉默失败”) */
|
* free < kDrainMinFree → /logs 尽量仍返回 ring(降低门槛,避免“沉默失败”) */
|
||||||
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
|
static constexpr uint32_t kDropWarnFree = 6U * 1024U;
|
||||||
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
|
static constexpr uint32_t kDropInfoFree = 10U * 1024U;
|
||||||
static constexpr uint32_t kSkipUdpFree = 10U * 1024U;
|
static constexpr uint32_t kSkipUdpFree = 6U * 1024U; // ★ 修复:从 10KB 降至 6KB,sendto 本身不需大块堆
|
||||||
static constexpr uint32_t kDrainMinFree = 8U * 1024U;
|
static constexpr uint32_t kDrainMinFree = 4U * 1024U; // ★ 修复:从 8KB 降至 4KB,降低 /logs 返空的概率
|
||||||
|
|
||||||
esp_err_t ensureUdpSocket();
|
esp_err_t ensureUdpSocket();
|
||||||
void ringPush(const char *data, int len);
|
void ringPush(const char *data, int len);
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ public:
|
|||||||
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
|
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 由 WiFi 事件回调推送连接状态(解耦 HealthMonitor → WifiOta 依赖)。
|
* @brief 由 WiFi 事件回调推送连接状态(避免 HealthMonitor → WifiOta 依赖)。
|
||||||
* WiFi 连接/断开时由 wifi_ota 事件处理器主动调用,
|
* WiFi 连接/断开时由 wifi_ota 事件处理器主动调用,
|
||||||
* HealthMonitor 不再主动查询 WifiOta::isConnected()。
|
* HealthMonitor 无需主动查询 WifiOta::isConnected()。
|
||||||
* @param connected true=已连接,false=断开。
|
* @param connected true=已连接,false=断开。
|
||||||
*/
|
*/
|
||||||
void setWifiConnected(bool connected);
|
void setWifiConnected(bool connected);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "app_config.h"
|
#include "app_config.h"
|
||||||
#include "health_monitor.h"
|
#include "health_monitor.h"
|
||||||
|
#include "cpu_stats.h"
|
||||||
#include "ota_relay.h"
|
#include "ota_relay.h"
|
||||||
#include "wifi_ota.h"
|
#include "wifi_ota.h"
|
||||||
#include "ble_ota.h"
|
#include "ble_ota.h"
|
||||||
@@ -24,22 +25,25 @@ size_t collectTaskStackMetrics(TaskStackMetric* out, size_t max)
|
|||||||
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
|
out[n++] = { "http_server", xTaskGetHandle("httpd"), HTTP_SERVER_STACK_SIZE };
|
||||||
}
|
}
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 };
|
out[n++] = { "health_mon", HealthMonitor::instance().getTaskHandle(), TASK_STACK_HEALTH_MON };
|
||||||
}
|
}
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), 6144 };
|
out[n++] = { "cpu_stats", CpuStats::instance().getTaskHandle(), TASK_STACK_CPU_STATS };
|
||||||
}
|
}
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 };
|
out[n++] = { "ota_relay", OtaRelay::getTaskHandle(), TASK_STACK_OTA_RELAY };
|
||||||
}
|
}
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 };
|
out[n++] = { "wifi_ota", WifiOta::instance().getTaskHandle(), TASK_STACK_WIFI_OTA };
|
||||||
}
|
}
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), 4096 };
|
out[n++] = { "wifi_reconnect", WifiOta::instance().m_reconnectTask, TASK_STACK_WIFI_RECONNECT };
|
||||||
}
|
}
|
||||||
if (n < max) {
|
if (n < max) {
|
||||||
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 };
|
out[n++] = { "ble_ota", BleOta::instance().getTaskHandle(), TASK_STACK_BLE_OTA };
|
||||||
|
}
|
||||||
|
if (n < max) {
|
||||||
|
out[n++] = { "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), TASK_STACK_BLE_WORKER };
|
||||||
}
|
}
|
||||||
|
|
||||||
return n;
|
return n;
|
||||||
|
|||||||
+48
-15
@@ -25,7 +25,7 @@
|
|||||||
#define TAG "OTA_CORE"
|
#define TAG "OTA_CORE"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 分区互斥锁(M1: 内聚自 ota_mutex.h 全局变量)
|
// 分区互斥锁
|
||||||
// ============================================================
|
// ============================================================
|
||||||
SemaphoreHandle_t Esp32OtaCore::s_mutex = NULL;
|
SemaphoreHandle_t Esp32OtaCore::s_mutex = NULL;
|
||||||
volatile bool Esp32OtaCore::s_occupied = false;
|
volatile bool Esp32OtaCore::s_occupied = false;
|
||||||
@@ -172,22 +172,57 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
|||||||
return beginCleanupAndReturn(ESP_FAIL);
|
return beginCleanupAndReturn(ESP_FAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ★ 用 OTA_WITH_SEQUENTIAL_WRITES:边写边按扇区擦除,begin 几乎瞬时返回。
|
/* ★ 整分区预擦(非 SEQUENTIAL 边写边擦):
|
||||||
* 旧逻辑按 64KB 对齐预擦 ~1MB,会在 /ota/upload 开头阻塞几十秒~数分钟:
|
* 1) 扇区循环 erase + 喂狗/yield,避免 TASK_WDT 与 WiFi 饿死
|
||||||
* 浏览器 upload 进度已到 100%,设备还在擦 Flash → 表现为“卡在 100% 很久才成功”。 */
|
* 2) esp_ota_resume(erase_size≠SEQUENTIAL) → need_erase=false,写路径纯 write
|
||||||
DEBUG_INFO(TAG, "esp_ota_begin(SEQUENTIAL) 目标=%s size=%u free=%lu...",
|
* 3) 显式 invalidate inactive slot(resume 不像 begin 那样自动做)
|
||||||
m_partition->label, (unsigned)total_size,
|
* 预擦会在 begin 阶段占用数秒~数十秒,之后收流写 Flash 更快更稳。 */
|
||||||
|
const size_t part_size = m_partition->size;
|
||||||
|
const size_t sector = m_partition->erase_size ? m_partition->erase_size : 4096U;
|
||||||
|
DEBUG_INFO(TAG, "整分区预擦: %s size=%u sector=%u free=%lu...",
|
||||||
|
m_partition->label, (unsigned)part_size, (unsigned)sector,
|
||||||
(unsigned long)esp_get_free_heap_size());
|
(unsigned long)esp_get_free_heap_size());
|
||||||
const int64_t t0 = esp_timer_get_time();
|
const int64_t t0 = esp_timer_get_time();
|
||||||
esp_err_t err = esp_ota_begin(m_partition, OTA_WITH_SEQUENTIAL_WRITES, &m_handle);
|
for (size_t off = 0; off < part_size; ) {
|
||||||
|
size_t chunk = sector;
|
||||||
|
if (off + chunk > part_size) {
|
||||||
|
chunk = part_size - off;
|
||||||
|
}
|
||||||
|
esp_err_t erase_err = esp_partition_erase_range(m_partition, off, chunk);
|
||||||
|
if (erase_err != ESP_OK) {
|
||||||
|
DEBUG_ERR(TAG, "整分区预擦失败 @%u: %s",
|
||||||
|
(unsigned)off, esp_err_to_name(erase_err));
|
||||||
|
return beginCleanupAndReturn(erase_err);
|
||||||
|
}
|
||||||
|
off += chunk;
|
||||||
|
(void)esp_task_wdt_reset();
|
||||||
|
taskYIELD();
|
||||||
|
}
|
||||||
const int64_t t1 = esp_timer_get_time();
|
const int64_t t1 = esp_timer_get_time();
|
||||||
|
DEBUG_INFO(TAG, "整分区预擦完成 耗时 %lld ms",
|
||||||
|
(long long)((t1 - t0) / 1000));
|
||||||
|
|
||||||
|
#if defined(CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE) || defined(CONFIG_APP_ROLLBACK_ENABLE)
|
||||||
|
{
|
||||||
|
esp_err_t inv = esp_ota_invalidate_inactive_ota_data_slot();
|
||||||
|
if (inv != ESP_OK && inv != ESP_ERR_NOT_SUPPORTED) {
|
||||||
|
DEBUG_WARN(TAG, "invalidate inactive OTA slot: %s(继续)",
|
||||||
|
esp_err_to_name(inv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* erase_size 传 0(≠ OTA_WITH_SEQUENTIAL_WRITES)→ 写时不再擦扇区 */
|
||||||
|
esp_err_t err = esp_ota_resume(m_partition, /*erase_size=*/0,
|
||||||
|
/*image_offset=*/0, &m_handle);
|
||||||
|
const int64_t t2 = esp_timer_get_time();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s (耗时 %lld ms)",
|
DEBUG_ERR(TAG, "esp_ota_resume 失败: %s (耗时 %lld ms)",
|
||||||
esp_err_to_name(err), (long long)((t1 - t0) / 1000));
|
esp_err_to_name(err), (long long)((t2 - t1) / 1000));
|
||||||
return beginCleanupAndReturn(err);
|
return beginCleanupAndReturn(err);
|
||||||
}
|
}
|
||||||
DEBUG_INFO(TAG, "esp_ota_begin 完成 耗时 %lld ms free=%lu",
|
DEBUG_INFO(TAG, "esp_ota_resume 完成 耗时 %lld ms free=%lu",
|
||||||
(long long)((t1 - t0) / 1000),
|
(long long)((t2 - t1) / 1000),
|
||||||
(unsigned long)esp_get_free_heap_size());
|
(unsigned long)esp_get_free_heap_size());
|
||||||
|
|
||||||
m_received = 0;
|
m_received = 0;
|
||||||
@@ -196,7 +231,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
|||||||
m_ended = false;
|
m_ended = false;
|
||||||
memset(m_sha256, 0, sizeof(m_sha256));
|
memset(m_sha256, 0, sizeof(m_sha256));
|
||||||
|
|
||||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (sequential erase)",
|
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节 (full-partition erase)",
|
||||||
m_partition->label, (unsigned)total_size);
|
m_partition->label, (unsigned)total_size);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -213,11 +248,9 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
|
|||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sequential 模式写时可能触发扇区擦除(禁 cache),期间 WiFi 易饿死。
|
/* 整分区已预擦:此处为纯 write,不再扇区擦除。 */
|
||||||
* 写后 yield 一次即可;写前再 yield 会无谓拖慢吞吐(httpd 侧另有堆感知 pacing)。 */
|
|
||||||
(void)esp_task_wdt_reset();
|
(void)esp_task_wdt_reset();
|
||||||
esp_err_t err = esp_ota_write(m_handle, data, len);
|
esp_err_t err = esp_ota_write(m_handle, data, len);
|
||||||
taskYIELD();
|
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
m_received += len;
|
m_received += len;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* begin → write*(N) → finalize / abort
|
* begin → write*(N) → finalize / abort
|
||||||
*
|
*
|
||||||
* 统一以 WiFi 流程为基准:
|
* 统一以 WiFi 流程为基准:
|
||||||
* - esp_ota_begin 第二参数传 total_size
|
* - begin:整分区预擦后 esp_ota_resume(写路径不再边写边擦)
|
||||||
* - SHA-256 在 finalize 时从 OTA 分区读回计算(不在上传期间增量计算)
|
* - SHA-256 在 finalize 时从 OTA 分区读回计算(不在上传期间增量计算)
|
||||||
* - finalize 顺序: 读回 SHA-256 → 比对前端 SHA → esp_ota_end → esp_ota_set_boot_partition → 可选重启
|
* - finalize 顺序: 读回 SHA-256 → 比对前端 SHA → esp_ota_end → esp_ota_set_boot_partition → 可选重启
|
||||||
* - m_ended 标志位防止 esp_ota_end 成功后再次 esp_ota_abort 同一 handle
|
* - m_ended 标志位防止 esp_ota_end 成功后再次 esp_ota_abort 同一 handle
|
||||||
@@ -102,16 +102,15 @@ public:
|
|||||||
/** @brief 获取已接收的字节数。 */
|
/** @brief 获取已接收的字节数。 */
|
||||||
size_t getReceived() const { return m_received; }
|
size_t getReceived() const { return m_received; }
|
||||||
|
|
||||||
// ── 分区互斥锁管理(M1: 从 ota_mutex.h 全局变量内聚到此类)──
|
// ── 分区互斥锁管理 ──
|
||||||
/**
|
/**
|
||||||
* @brief 初始化 ESP32 OTA 分区互斥锁(在 app_main 中调用)。
|
* @brief 初始化 ESP32 OTA 分区互斥锁(在 app_main 中调用)。
|
||||||
* 替代旧的全局 g_esp32OtaMutex / g_esp32OtaOccupied。
|
|
||||||
*/
|
*/
|
||||||
static void initMutex();
|
static void initMutex();
|
||||||
private:
|
private:
|
||||||
Esp32OtaCore() = default;
|
Esp32OtaCore() = default;
|
||||||
|
|
||||||
// ── 分区互斥锁(M1: 内聚自 ota_mutex.h)──
|
// ── 分区互斥锁 ──
|
||||||
static SemaphoreHandle_t s_mutex; ///< ESP32 OTA 分区互斥锁
|
static SemaphoreHandle_t s_mutex; ///< ESP32 OTA 分区互斥锁
|
||||||
static volatile bool s_occupied; ///< 分区占用标志
|
static volatile bool s_occupied; ///< 分区占用标志
|
||||||
static bool acquireMutex(uint32_t timeout_ms);
|
static bool acquireMutex(uint32_t timeout_ms);
|
||||||
|
|||||||
@@ -98,13 +98,13 @@ void OtaCoordinator::watchdogCheckLocked()
|
|||||||
return; // 未超时
|
return; // 未超时
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ 超时自愈:强制转 ERROR。
|
// ★ 超时自愈:完整中止到 IDLE(释放 Esp32OtaCore / 中继 / GD32 meta 等)。
|
||||||
// ERROR 不在任何 beginXxx() 的拒绝列表中,下次请求会自动从 ERROR 恢复。
|
// 旧逻辑只标 ERROR 不清理子系统:若卡在 FINALIZING 且 core 已半释放,
|
||||||
// 清理由各子系统自行负责(abortCurrent 中会按 ERROR 不做实际清理,仅复位状态)。
|
// 前端仍长时间判“占用”;若 core 仍持互斥,ERROR 后 begin 还要靠 forceCleanup。
|
||||||
DEBUG_ERR(TAG, "忙状态 %d 卡死超过 %" PRIu32 " ms,强制自愈 → ERROR",
|
// REBOOTING 本应已 esp_restart;超时则同样中止,避免永久卡死。
|
||||||
|
DEBUG_ERR(TAG, "忙状态 %d 卡死超过 %" PRIu32 " ms,强制中止 → IDLE",
|
||||||
(int)m_sysState, (uint32_t)pdTICKS_TO_MS(kBusyWatchdogTicks));
|
(int)m_sysState, (uint32_t)pdTICKS_TO_MS(kBusyWatchdogTicks));
|
||||||
m_sysState = OTA_SYS_ERROR;
|
doAbortLocked();
|
||||||
m_busySinceTick = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -124,7 +124,7 @@ void OtaCoordinator::init()
|
|||||||
m_gd32StorageDone = false;
|
m_gd32StorageDone = false;
|
||||||
m_busySinceTick = 0;
|
m_busySinceTick = 0;
|
||||||
|
|
||||||
// ★ H4 解耦:注册协调器为 OtaRelay 的中继事件监听器
|
// 注册协调器为 OtaRelay 的中继事件监听器
|
||||||
OtaRelay::setRelayListener(this);
|
OtaRelay::setRelayListener(this);
|
||||||
|
|
||||||
// ★ 重启恢复:如果 NVS 里有完整固件元数据,开机时立即进入 STORED 状态,
|
// ★ 重启恢复:如果 NVS 里有完整固件元数据,开机时立即进入 STORED 状态,
|
||||||
@@ -221,12 +221,27 @@ esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 复位按需擦除水位(不在此处整段预擦)
|
// 3. 整分区预擦(不再边写边擦):prepare 阶段擦完 gd32_ota 分区,
|
||||||
// 旧实现在 prepare 阶段 erase 整包固件对应分区:HTTP 主线程阻塞数秒~数十秒,
|
// 收流时 flashWriteChunk 仅 write,吞吐更稳。会阻塞数秒~数十秒。
|
||||||
// 期间不 httpd_req_recv → TCP 窗口塞满后浏览器进度先到 100%,再空等擦+写+SHA。
|
|
||||||
// 现改为 flashWriteChunk 内按扇区 ensureErased,边收边擦写,进度与真实处理对齐。
|
|
||||||
Gd32FlashStore::instance().beginWriteSession();
|
Gd32FlashStore::instance().beginWriteSession();
|
||||||
DEBUG_INFO(TAG, "prepareGd32Storage: 按需擦除会话已复位 (fw=%" PRIu32 ")", total_size);
|
{
|
||||||
|
const esp_partition_t *gd32_part = Gd32FlashStore::instance().getPartition();
|
||||||
|
const uint32_t erase_bytes = gd32_part ? (uint32_t)gd32_part->size : 0U;
|
||||||
|
if (erase_bytes == 0U) {
|
||||||
|
DEBUG_ERR(TAG, "prepareGd32Storage: 找不到 gd32_ota 分区");
|
||||||
|
abortCurrent();
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
DEBUG_INFO(TAG, "prepareGd32Storage: 整分区预擦 %u 字节 (fw=%" PRIu32 ")...",
|
||||||
|
(unsigned)erase_bytes, total_size);
|
||||||
|
err = Gd32FlashStore::instance().erasePartition(erase_bytes);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
DEBUG_ERR(TAG, "prepareGd32Storage: 整分区预擦失败: %s", esp_err_to_name(err));
|
||||||
|
abortCurrent();
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
DEBUG_INFO(TAG, "prepareGd32Storage: 整分区预擦完成");
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
|
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
|
||||||
clearGd32Meta();
|
clearGd32Meta();
|
||||||
@@ -270,7 +285,7 @@ esp_err_t OtaCoordinator::finishGd32StorageAfterReceive()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
|
// GD32 存储操作(传输层统一入口)
|
||||||
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
|
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
|
||||||
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
|
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -394,7 +409,7 @@ void OtaCoordinator::notifyRelayComplete(bool success)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)
|
// IRelayListener 接口实现
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/** @brief IRelayListener 回调:中继已开始,委托给 notifyRelayStarted()。 */
|
/** @brief IRelayListener 回调:中继已开始,委托给 notifyRelayStarted()。 */
|
||||||
void OtaCoordinator::onRelayStarted()
|
void OtaCoordinator::onRelayStarted()
|
||||||
@@ -409,7 +424,7 @@ void OtaCoordinator::onRelayComplete(bool success)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// BLE 射频共存管理(L1: 消除 ble_ota → wifi_ota 直接依赖)
|
// BLE 射频共存管理
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void OtaCoordinator::setBleCoexListener(IBleCoexListener *listener)
|
void OtaCoordinator::setBleCoexListener(IBleCoexListener *listener)
|
||||||
{
|
{
|
||||||
@@ -443,7 +458,6 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
|
|||||||
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
|
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
|
||||||
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
|
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
|
||||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||||
// FINALIZING/REBOOTING 是不可逆阶段,也拒绝。
|
|
||||||
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||||||
if (m_otaChannel != channel) {
|
if (m_otaChannel != channel) {
|
||||||
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
|
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
|
||||||
@@ -458,14 +472,23 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
|
|||||||
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
|
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
|
||||||
}
|
}
|
||||||
|
|
||||||
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 收尾/重启中,均拒绝。
|
// ★ FINALIZING 残留自愈:SHA 不匹配时 core 已 abort,但旧固件可能未把协调器
|
||||||
// ★ 必须显式拒绝 ESP32_FINALIZING/REBOOTING:
|
// 复位到 IDLE。core 已空闲 ⇒ 协调器状态是假占用,允许清掉后开新会话。
|
||||||
// 若放行,本调用会落到 Esp32OtaCore::begin 去抢全局互斥失败而返回 INVALID_STATE,
|
// core 仍 active ⇒ 真正在收尾,拒绝(不可 forceCleanup,防并发写分区)。
|
||||||
// 随后 beginEsp32OtaWithRetry 的 forceCleanup 会把另一通道的活跃会话
|
if (h && m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||||
// (esp_ota_abort + 释放互斥)误中止 → 两通道并发写同一 OTA 分区。
|
if (!Esp32OtaCore::instance().isActive()) {
|
||||||
|
DEBUG_WARN(TAG, "ESP32_FINALIZING 残留(core 已空闲),强制复位后允许新会话");
|
||||||
|
doAbortLocked();
|
||||||
|
} else {
|
||||||
|
DEBUG_WARN(TAG, "系统忙 (ESP32_FINALIZING, core 活跃),拒绝 ESP32 OTA");
|
||||||
|
unlock(h);
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 重启中,均拒绝。
|
||||||
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
|
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
|
||||||
m_sysState == OTA_SYS_GD32_RELAYING ||
|
m_sysState == OTA_SYS_GD32_RELAYING ||
|
||||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
|
||||||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
|
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
|
||||||
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
|
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
|
||||||
(int)m_sysState, stateToString(m_sysState));
|
(int)m_sysState, stateToString(m_sysState));
|
||||||
@@ -586,6 +609,13 @@ Esp32FinalizeResult OtaCoordinator::finalizeForReboot(const char *frontend_sha_h
|
|||||||
r.device_sha_hex);
|
r.device_sha_hex);
|
||||||
if (r.err == ESP_OK) {
|
if (r.err == ESP_OK) {
|
||||||
notifyEsp32FinalizeComplete();
|
notifyEsp32FinalizeComplete();
|
||||||
|
} else {
|
||||||
|
// ★ SHA 不匹配 / end 失败:Esp32OtaCore 已 abort+释放互斥,但协调器若仍停在
|
||||||
|
// ESP32_FINALIZING,WiFi 前端会一直被“设备 OTA 占用”拒绝(BLE 路径曾单独
|
||||||
|
// 补 abortCurrent;统一放到这里,WiFi/BLE 共用)。
|
||||||
|
DEBUG_ERR(TAG, "ESP32 finalize 失败: %s,复位协调器 → IDLE",
|
||||||
|
esp_err_to_name(r.err));
|
||||||
|
abortCurrent();
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@@ -632,6 +662,26 @@ void OtaCoordinator::abortCurrent()
|
|||||||
wifiOtaResumeBleIfNeeded();
|
wifiOtaResumeBleIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OtaCoordinator::healOrphanStates()
|
||||||
|
{
|
||||||
|
bool need_resume = false;
|
||||||
|
bool h = lock();
|
||||||
|
if (h) {
|
||||||
|
watchdogCheckLocked();
|
||||||
|
// finalize 失败后 core 已空、协调器仍 FINALIZING 的假占用:status 轮询即可自愈
|
||||||
|
if (m_sysState == OTA_SYS_ESP32_FINALIZING &&
|
||||||
|
!Esp32OtaCore::instance().isActive()) {
|
||||||
|
DEBUG_WARN(TAG, "status 路径发现 ESP32_FINALIZING 残留,自愈 → IDLE");
|
||||||
|
doAbortLocked();
|
||||||
|
need_resume = true;
|
||||||
|
}
|
||||||
|
unlock(h);
|
||||||
|
}
|
||||||
|
if (need_resume) {
|
||||||
|
wifiOtaResumeBleIfNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t OtaCoordinator::getOtaEpoch() const
|
uint32_t OtaCoordinator::getOtaEpoch() const
|
||||||
{
|
{
|
||||||
bool h = lock();
|
bool h = lock();
|
||||||
@@ -665,7 +715,7 @@ bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
|
|||||||
ota_system_state_t OtaCoordinator::getSystemState() const
|
ota_system_state_t OtaCoordinator::getSystemState() const
|
||||||
{
|
{
|
||||||
// 对于纯读取,短暂加锁保证一致性
|
// 对于纯读取,短暂加锁保证一致性
|
||||||
// ★ M1/L3: m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast
|
// m_mutex 为 mutable + lock()/unlock() 为 const,无需 const_cast
|
||||||
bool h = lock();
|
bool h = lock();
|
||||||
ota_system_state_t s = m_sysState;
|
ota_system_state_t s = m_sysState;
|
||||||
unlock(h);
|
unlock(h);
|
||||||
@@ -720,7 +770,7 @@ bool OtaCoordinator::getGd32AppAlive() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// OTA 状态快照(PR1: 收口状态读取)
|
// OTA 状态快照
|
||||||
// ============================================================
|
// ============================================================
|
||||||
OtaSnapshot OtaCoordinator::getSnapshot() const
|
OtaSnapshot OtaCoordinator::getSnapshot() const
|
||||||
{
|
{
|
||||||
@@ -754,7 +804,7 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// GD32 APP 探测(PR2: 从 /system/status 移出)
|
// GD32 APP 探测
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
|
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
|
||||||
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
|
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
|
||||||
|
|||||||
+24
-14
@@ -21,7 +21,7 @@
|
|||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "app_config.h" // OTA_MAX_FIRMWARE_SIZE / GD32_OTA_PACKET_SIZE(validateGd32Size 用)
|
#include "app_config.h" // OTA_MAX_FIRMWARE_SIZE / GD32_OTA_PACKET_SIZE(validateGd32Size 用)
|
||||||
#include "storage/gd32_flash_store.h"
|
#include "storage/gd32_flash_store.h"
|
||||||
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot(H4 解耦)
|
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 系统级 OTA 状态枚举
|
// 系统级 OTA 状态枚举
|
||||||
@@ -47,7 +47,7 @@ typedef enum {
|
|||||||
} ota_channel_t;
|
} ota_channel_t;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// OTA 系统全量只读快照(PR1: 收口状态读取)
|
// OTA 系统全量只读快照
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* @brief OTA 系统全量只读快照。
|
* @brief OTA 系统全量只读快照。
|
||||||
@@ -64,11 +64,11 @@ struct OtaSnapshot {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// IBleCoexListener - BLE 射频共存通知接口(解决 L1 横向耦合)
|
// IBleCoexListener - BLE 射频共存通知接口
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// WifiOta 实现此接口并注册到 OtaCoordinator,
|
// WifiOta 实现此接口并注册到 OtaCoordinator,
|
||||||
// BLE OTA 开始/结束时通过协调器通知 WiFi 层,
|
// BLE OTA 开始/结束时通过协调器通知 WiFi 层,
|
||||||
// 消除 ble_ota → wifi_ota 的直接依赖。
|
// 避免 ble_ota → wifi_ota 的直接依赖。
|
||||||
/**
|
/**
|
||||||
* @brief BLE 射频共存监听器接口。
|
* @brief BLE 射频共存监听器接口。
|
||||||
* BLE OTA 开始/结束时,协调器通过此接口通知 WiFi 层暂停/恢复射频活动。
|
* BLE OTA 开始/结束时,协调器通过此接口通知 WiFi 层暂停/恢复射频活动。
|
||||||
@@ -124,8 +124,8 @@ public:
|
|||||||
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
|
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 复位按需擦除水位 + 清空 meta + 写入初始 meta。
|
* @brief 统一 GD32 OTA 写入前准备:校验大小 + begin + 整分区预擦 + 清空 meta + 写入初始 meta。
|
||||||
* 扇区擦除在 writeGd32Chunk/flashWriteChunk 内按写入范围推进(非整段预擦)。
|
* 收流时 write 路径只写 Flash(ensureErased 仅作水位兜底)。
|
||||||
* WiFi/BLE 上传入口共用,任意步骤失败时内部 abortCurrent,调用方无需再清理。
|
* WiFi/BLE 上传入口共用,任意步骤失败时内部 abortCurrent,调用方无需再清理。
|
||||||
* @param total_size 固件总字节数。
|
* @param total_size 固件总字节数。
|
||||||
* @param channel 发起通道(WiFi/BLE)。
|
* @param channel 发起通道(WiFi/BLE)。
|
||||||
@@ -146,7 +146,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
esp_err_t finishGd32StorageAfterReceive();
|
esp_err_t finishGd32StorageAfterReceive();
|
||||||
|
|
||||||
// ── GD32 存储操作(传输层通过协调器操作 Flash,解决 H3 绕过问题)──
|
// ── GD32 存储操作(传输层通过协调器操作 Flash)──
|
||||||
/**
|
/**
|
||||||
* @brief 向 GD32 Flash 分区写入固件数据块。
|
* @brief 向 GD32 Flash 分区写入固件数据块。
|
||||||
* 传输层(WiFi/BLE)通过此接口写入,不再直接调用 Gd32FlashStore。
|
* 传输层(WiFi/BLE)通过此接口写入,不再直接调用 Gd32FlashStore。
|
||||||
@@ -312,7 +312,9 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief ESP32 OTA 收尾(统一封装 BLE/WiFi 两端重复的收尾序列)。
|
* @brief ESP32 OTA 收尾(统一封装 BLE/WiFi 两端重复的收尾序列)。
|
||||||
* 步骤:notifyEsp32ReceiveComplete → Esp32OtaCore::finalize(reboot=false)
|
* 步骤:notifyEsp32ReceiveComplete → Esp32OtaCore::finalize(reboot=false)
|
||||||
* → 成功则 notifyEsp32FinalizeComplete。
|
* → 成功则 notifyEsp32FinalizeComplete;
|
||||||
|
* → 失败(SHA 不匹配 / end 失败等)则 abortCurrent() 复位到 IDLE,
|
||||||
|
* 避免协调器卡在 ESP32_FINALIZING 导致后续上传被假拒绝。
|
||||||
* 不重启;调用方负责把 device_sha_hex 上报前端后再调 restartEsp32()。
|
* 不重启;调用方负责把 device_sha_hex 上报前端后再调 restartEsp32()。
|
||||||
* @param frontend_sha_hex 前端 64 字符 hex SHA-256,nullptr 表示跳过比对。
|
* @param frontend_sha_hex 前端 64 字符 hex SHA-256,nullptr 表示跳过比对。
|
||||||
* @return Esp32FinalizeResult,err 为 ESP_OK/ESP_ERR_INVALID_VERSION/其他。
|
* @return Esp32FinalizeResult,err 为 ESP_OK/ESP_ERR_INVALID_VERSION/其他。
|
||||||
@@ -333,6 +335,14 @@ public:
|
|||||||
*/
|
*/
|
||||||
void abortCurrent();
|
void abortCurrent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 自愈孤儿忙态(供 /system/status 等轮询路径调用)。
|
||||||
|
* - 跑忙状态 watchdog(超时完整 abort → IDLE)
|
||||||
|
* - ESP32_FINALIZING 但 Esp32OtaCore 已空闲:判定为 SHA 失败等残留,复位 IDLE
|
||||||
|
* 不影响真正正在 finalize(core.isActive())的会话。
|
||||||
|
*/
|
||||||
|
void healOrphanStates();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 获取当前 OTA 会话的 epoch(代次)。
|
* @brief 获取当前 OTA 会话的 epoch(代次)。
|
||||||
* 每次 beginEsp32Ota/beginGd32Storage 成功时递增。
|
* 每次 beginEsp32Ota/beginGd32Storage 成功时递增。
|
||||||
@@ -354,7 +364,7 @@ public:
|
|||||||
|
|
||||||
// ── 状态查询 ──
|
// ── 状态查询 ──
|
||||||
/**
|
/**
|
||||||
* @brief 一次性读取系统 OTA 全量状态快照(PR1: 收口状态读取)。
|
* @brief 一次性读取系统 OTA 全量状态快照。
|
||||||
* coordinator mutex + relay mutex 各取一次,
|
* coordinator mutex + relay mutex 各取一次,
|
||||||
* 供 /system/status 等只读接口消费。
|
* 供 /system/status 等只读接口消费。
|
||||||
* @return 包含 sys_state、meta、relay 进度的完整快照。
|
* @return 包含 sys_state、meta、relay 进度的完整快照。
|
||||||
@@ -362,7 +372,7 @@ public:
|
|||||||
OtaSnapshot getSnapshot() const;
|
OtaSnapshot getSnapshot() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 单次 SPI 探测 GD32 APP 是否存活(PR2: 从 status 移出)。
|
* @brief 单次 SPI 探测 GD32 APP 是否存活。
|
||||||
* 成功时更新 OtaRelay 缓存的 alive/version,
|
* 成功时更新 OtaRelay 缓存的 alive/version,
|
||||||
* 失败时清除 alive 标志。
|
* 失败时清除 alive 标志。
|
||||||
* @param out_version 输出版本信息,可为 nullptr。
|
* @param out_version 输出版本信息,可为 nullptr。
|
||||||
@@ -421,7 +431,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// ── IRelayListener 接口实现(H4: OtaRelay 通过接口回调通知协调器)──
|
// ── IRelayListener 接口实现 ──
|
||||||
/**
|
/**
|
||||||
* @brief IRelayListener 回调:中继已开始。
|
* @brief IRelayListener 回调:中继已开始。
|
||||||
* 由 OtaRelay task 在 forwardFirmware() 前通过 s_relayListener 调用。
|
* 由 OtaRelay task 在 forwardFirmware() 前通过 s_relayListener 调用。
|
||||||
@@ -435,7 +445,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void onRelayComplete(bool success) override;
|
void onRelayComplete(bool success) override;
|
||||||
|
|
||||||
// ── BLE 射频共存管理(L1: 替代 ble_ota → wifi_ota 直接调用)──
|
// ── BLE 射频共存管理 ──
|
||||||
/**
|
/**
|
||||||
* @brief 注册 BLE 共存监听器(由 main 在初始化时调用)。
|
* @brief 注册 BLE 共存监听器(由 main 在初始化时调用)。
|
||||||
* @param listener WifiOta 实现的监听器指针,nullptr 取消注册。
|
* @param listener WifiOta 实现的监听器指针,nullptr 取消注册。
|
||||||
@@ -498,10 +508,10 @@ private:
|
|||||||
// 仅在同类型活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义。
|
// 仅在同类型活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义。
|
||||||
ota_channel_t m_otaChannel = OTA_CHANNEL_WIFI;
|
ota_channel_t m_otaChannel = OTA_CHANNEL_WIFI;
|
||||||
|
|
||||||
// 互斥锁(M1/L3: mutable 允许在 const 方法中加锁,消除 const_cast)
|
// 互斥锁(mutable 允许在 const 方法中加锁,无需 const_cast)
|
||||||
mutable SemaphoreHandle_t m_mutex = nullptr;
|
mutable SemaphoreHandle_t m_mutex = nullptr;
|
||||||
|
|
||||||
// BLE 共存监听器(L1: 由 WifiOta 实现并注册)
|
// BLE 共存监听器(由 WifiOta 实现并注册)
|
||||||
IBleCoexListener *m_bleCoexListener = nullptr;
|
IBleCoexListener *m_bleCoexListener = nullptr;
|
||||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100);
|
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(100);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* ota_relay.cpp - OTA 中继事件驱动状态机实现
|
* ota_relay.cpp - OTA 中继事件驱动状态机实现
|
||||||
*
|
*
|
||||||
* 将原 17 状态"声明式赋值"重构为 7 状态事件驱动架构:
|
* 7 状态事件驱动架构:
|
||||||
* - 每个协议阶段封装为一个状态 handler
|
* - 每个协议阶段封装为一个状态 handler
|
||||||
* - processEvent() 根据当前状态分发事件
|
* - processEvent() 根据当前状态分发事件
|
||||||
* - 状态转换显式通过 setState() 完成
|
* - 状态转换显式通过 setState() 完成
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "health_monitor.h"
|
#include "health_monitor.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
// ★ H4 解耦:OtaRelay 不再直接 #include "ota_coordinator.h",
|
// OtaRelay 不再直接 #include "ota_coordinator.h",
|
||||||
// 通过 IRelayListener 接口回调通知协调器,依赖方向变为单向。
|
// 通过 IRelayListener 接口回调通知协调器,依赖方向为单向。
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 公共拒绝事件判断(文件内工具)
|
// 公共拒绝事件判断(文件内工具)
|
||||||
@@ -42,10 +42,9 @@ static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// doRelayPackets 重试阈值(与历史代码字面值保持一致,仅命名化)
|
// doRelayPackets 重试阈值(仅命名化字面值)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 原代码中 retry_count 阈值:行 1015/1075/1084 用 10U,行 1032/1050 用 3U。
|
// retry_count 阈值:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
|
||||||
// 含义:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
|
|
||||||
// 结构化错误属于"GD32 报告具体错误",较少重试(3次)。
|
// 结构化错误属于"GD32 报告具体错误",较少重试(3次)。
|
||||||
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
|
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
|
||||||
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
|
||||||
@@ -67,7 +66,7 @@ SemaphoreHandle_t OtaRelay::s_stateMutex = NULL;
|
|||||||
TaskHandle_t OtaRelay::taskHandle = NULL;
|
TaskHandle_t OtaRelay::taskHandle = NULL;
|
||||||
IRelayListener *OtaRelay::s_relayListener = nullptr;
|
IRelayListener *OtaRelay::s_relayListener = nullptr;
|
||||||
|
|
||||||
// ── 中继事件监听器(解决 H4 双向耦合)──
|
// ── 中继事件监听器 ──
|
||||||
void OtaRelay::setRelayListener(IRelayListener *listener)
|
void OtaRelay::setRelayListener(IRelayListener *listener)
|
||||||
{
|
{
|
||||||
s_relayListener = listener;
|
s_relayListener = listener;
|
||||||
@@ -78,7 +77,7 @@ IRelayListener* OtaRelay::getRelayListener()
|
|||||||
return s_relayListener;
|
return s_relayListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 队列访问器(M3: 封装 public static 成员)──
|
// ── 队列访问器 ──
|
||||||
QueueHandle_t OtaRelay::getDispatchQueue()
|
QueueHandle_t OtaRelay::getDispatchQueue()
|
||||||
{
|
{
|
||||||
return dispatchQueue;
|
return dispatchQueue;
|
||||||
@@ -1415,7 +1414,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
|
// 通过 IRelayListener 接口回调通知协调器
|
||||||
IRelayListener *listener = getRelayListener();
|
IRelayListener *listener = getRelayListener();
|
||||||
if (listener) listener->onRelayStarted();
|
if (listener) listener->onRelayStarted();
|
||||||
ota_result_t result = instance().forwardFirmware();
|
ota_result_t result = instance().forwardFirmware();
|
||||||
@@ -1432,7 +1431,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
|||||||
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
||||||
}
|
}
|
||||||
instance().reset();
|
instance().reset();
|
||||||
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
|
// 通过 IRelayListener 接口回调通知协调器中继结束
|
||||||
listener = getRelayListener();
|
listener = getRelayListener();
|
||||||
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
|
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-11
@@ -63,15 +63,15 @@ typedef enum {
|
|||||||
} ota_result_t;
|
} ota_result_t;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// IRelayListener - 中继事件监听接口(解决 H4 双向耦合)
|
// IRelayListener - 中继事件监听接口
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// OtaCoordinator 实现此接口并注册到 OtaRelay,
|
// OtaCoordinator 实现此接口并注册到 OtaRelay,
|
||||||
// OtaRelay 通过接口回调通知协调器,不再直接 #include ota_coordinator.h。
|
// OtaRelay 通过接口回调通知协调器,不再直接 #include ota_coordinator.h。
|
||||||
// 依赖方向变为单向:OtaCoordinator → OtaRelay(通过接口)。
|
// 依赖方向为单向:OtaCoordinator → OtaRelay(通过接口)。
|
||||||
/**
|
/**
|
||||||
* @brief 中继事件监听器接口。
|
* @brief 中继事件监听器接口。
|
||||||
* OtaCoordinator 实现此接口,OtaRelay 通过回调通知状态变更,
|
* OtaCoordinator 实现此接口,OtaRelay 通过回调通知状态变更,
|
||||||
* 消除 OtaRelay → OtaCoordinator 的直接依赖(解决 H4 双向耦合)。
|
* 避免 OtaRelay → OtaCoordinator 的直接依赖。
|
||||||
*/
|
*/
|
||||||
class IRelayListener {
|
class IRelayListener {
|
||||||
public:
|
public:
|
||||||
@@ -181,7 +181,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
// ── 中继事件监听器(解决 H4 双向耦合)──
|
// ── 中继事件监听器 ──
|
||||||
/**
|
/**
|
||||||
* @brief 注册中继事件监听器(由 OtaCoordinator 在 init 时调用)。
|
* @brief 注册中继事件监听器(由 OtaCoordinator 在 init 时调用)。
|
||||||
* @param listener 监听器指针,传 nullptr 取消注册。
|
* @param listener 监听器指针,传 nullptr 取消注册。
|
||||||
@@ -193,7 +193,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
static IRelayListener* getRelayListener();
|
static IRelayListener* getRelayListener();
|
||||||
|
|
||||||
// ── 队列访问器(解决 M3: 封装 public static 成员)──
|
// ── 队列访问器 ──
|
||||||
/**
|
/**
|
||||||
* @brief 获取事件分发队列句柄。
|
* @brief 获取事件分发队列句柄。
|
||||||
* 外部模块(OtaCoordinator / BLE)通过 getter 访问,不再直接引用静态成员。
|
* 外部模块(OtaCoordinator / BLE)通过 getter 访问,不再直接引用静态成员。
|
||||||
@@ -207,8 +207,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
static QueueHandle_t getResultQueue();
|
static QueueHandle_t getResultQueue();
|
||||||
|
|
||||||
// ── Flash 操作已迁移至 Gd32FlashStore(storage/gd32_flash_store.h)──
|
|
||||||
|
|
||||||
// ── 线程安全锁 ──
|
// ── 线程安全锁 ──
|
||||||
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
|
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
|
||||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
||||||
@@ -239,11 +237,11 @@ public:
|
|||||||
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// ── 队列(M3: 由 getter 访问,外部不再直接引用)──
|
// ── 队列(由 getter 访问)──
|
||||||
static QueueHandle_t dispatchQueue; ///< 事件分发队列
|
static QueueHandle_t dispatchQueue; ///< 事件分发队列
|
||||||
static QueueHandle_t resultQueue; ///< 结果回传队列
|
static QueueHandle_t resultQueue; ///< 结果回传队列
|
||||||
|
|
||||||
// ── 状态成员(全部改 private,统一通过 public setter/getter 访问)──
|
// ── 状态成员(统一通过 public setter/getter 访问)──
|
||||||
// 外部代码使用 getState()/getRelaySeq()/... 读取,setState()/setRelaySeq()/... 写入。
|
// 外部代码使用 getState()/getRelaySeq()/... 读取,setState()/setRelaySeq()/... 写入。
|
||||||
// 内部代码:若已在 StateLockGuard 块内(reset/getSnapshot/启动探测),可直接访问;
|
// 内部代码:若已在 StateLockGuard 块内(reset/getSnapshot/启动探测),可直接访问;
|
||||||
// 否则必须使用 setter/getter,禁止裸读写(消除数据竞争)。
|
// 否则必须使用 setter/getter,禁止裸读写(消除数据竞争)。
|
||||||
@@ -252,7 +250,7 @@ private:
|
|||||||
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
static uint32_t relayTotalPackets; ///< 本次中继总包数
|
||||||
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
static volatile bool gd32AppAlive; ///< GD32 APP 存活标志
|
||||||
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
static gd32_version_t gd32Version; ///< GD32 固件版本信息
|
||||||
static IRelayListener *s_relayListener; ///< 中继事件监听器(解决 H4)
|
static IRelayListener *s_relayListener; ///< 中继事件监听器
|
||||||
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁(由 StateLockGuard 使用)
|
static SemaphoreHandle_t s_stateMutex; ///< 状态成员互斥锁(由 StateLockGuard 使用)
|
||||||
|
|
||||||
// taskHandle 为 write-once-at-init 字段:仅 taskEntry 启动首行写入一次,
|
// taskHandle 为 write-once-at-init 字段:仅 taskEntry 启动首行写入一次,
|
||||||
@@ -331,7 +329,7 @@ private:
|
|||||||
/**
|
/**
|
||||||
* @brief 发送单包并等待 ACK(doRelayPackets 子步骤)。
|
* @brief 发送单包并等待 ACK(doRelayPackets 子步骤)。
|
||||||
*
|
*
|
||||||
* 内含重试循环,使用以下命名常量(与历史阈值保持一致):
|
* 内含重试循环,使用以下命名常量:
|
||||||
* kMaxResendOnNack=10(ACK 失败/轮询耗尽重发上限)
|
* kMaxResendOnNack=10(ACK 失败/轮询耗尽重发上限)
|
||||||
* kMaxResendOnStructError=3(结构化错误重发上限)
|
* kMaxResendOnStructError=3(结构化错误重发上限)
|
||||||
*
|
*
|
||||||
|
|||||||
+32
-16
@@ -66,7 +66,8 @@
|
|||||||
|
|
||||||
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
|
// ── 数据队列:将 OTA 数据处理从 NimBLE host task 卸载到工作线程 ──
|
||||||
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
|
// 避免 flash 操作(esp_ota_begin / esp_ota_write)阻塞 BLE 协议栈导致连接断开。
|
||||||
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW + 4)
|
// 深度略大于窗口:seq=0 整分区预擦期间前端若仍误发,可多扛几批,减少“队列满丢包→重传风暴”。
|
||||||
|
#define BLE_OTA_DATA_QUEUE_LEN (BLE_OTA_ACK_WINDOW * 2 + 8)
|
||||||
|
|
||||||
struct BleOtaDataBatch {
|
struct BleOtaDataBatch {
|
||||||
uint8_t *data;
|
uint8_t *data;
|
||||||
@@ -83,7 +84,7 @@ struct BleEsp32FinishCtx {
|
|||||||
uint16_t session_id;
|
uint16_t session_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免历史多次漏 free。
|
// RAII 包装:保证任务所有 return 路径自动释放 ctx,避免漏 free。
|
||||||
// 注意:若调用 restartEsp32()(内部 esp_restart 不返回),析构不会执行,
|
// 注意:若调用 restartEsp32()(内部 esp_restart 不返回),析构不会执行,
|
||||||
// 但这是设备重启路径,资源由系统回收。
|
// 但这是设备重启路径,资源由系统回收。
|
||||||
struct BleFinishCtxDeleter {
|
struct BleFinishCtxDeleter {
|
||||||
@@ -236,13 +237,10 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// WiFi 共存策略:BLE OTA 传输期间的处理
|
// WiFi 共存策略:BLE OTA 传输期间的处理
|
||||||
//
|
//
|
||||||
// 历史:曾在此处把 WiFi 切到 WIFI_PS_MAX_MODEM 以把空中时间让给 BLE。
|
// 不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
|
||||||
// 但 MAX_MODEM 下 STA 监听间隔被拉到极大,HTTP server 的 TCP 握手/数据包
|
|
||||||
// 会大量丢失或延迟到超时 → BLE OTA 期间任何 WiFi 请求(包括互斥拒绝应返回的
|
|
||||||
// 403)都到不了设备,前端 XHR 长时间无响应,表现为"Wifi 上传既不成功也不报错"。
|
|
||||||
//
|
|
||||||
// 现策略:不切换 WiFi 省电模式(保持 WIFI_PS_NONE),让 STA 全程可用,
|
|
||||||
// 协调器照常以 403 拒绝并发 OTA。ESP32-C3 coex 默认调度已能保证 BLE OTA 可用。
|
// 协调器照常以 403 拒绝并发 OTA。ESP32-C3 coex 默认调度已能保证 BLE OTA 可用。
|
||||||
|
// (曾尝试切到 WIFI_PS_MAX_MODEM 让出空中时间,但 STA 监听间隔被拉到极大,
|
||||||
|
// HTTP server 的 TCP 握手/数据包大量丢失或超时,前端 XHR 长时间无响应。)
|
||||||
// 仍保留 setBleOtaActive() 暂停 RSSI 轮询(避免 get_ap_info 的射频活动干扰 BLE)。
|
// 仍保留 setBleOtaActive() 暂停 RSSI 轮询(避免 get_ap_info 的射频活动干扰 BLE)。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
static bool s_wifi_ps_boosted = false;
|
static bool s_wifi_ps_boosted = false;
|
||||||
@@ -254,7 +252,7 @@ static void ble_ota_boost_wifi_ps()
|
|||||||
{
|
{
|
||||||
if (s_wifi_ps_boosted) return;
|
if (s_wifi_ps_boosted) return;
|
||||||
s_wifi_ps_boosted = true;
|
s_wifi_ps_boosted = true;
|
||||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
// 通过协调器通知 WiFi 层
|
||||||
OtaCoordinator::instance().notifyBleCoexStart();
|
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)");
|
||||||
}
|
}
|
||||||
@@ -266,7 +264,7 @@ static void ble_ota_restore_wifi_ps()
|
|||||||
{
|
{
|
||||||
if (!s_wifi_ps_boosted) return;
|
if (!s_wifi_ps_boosted) return;
|
||||||
s_wifi_ps_boosted = false;
|
s_wifi_ps_boosted = false;
|
||||||
// ★ L1: 通过协调器通知 WiFi 层(替代直接调用 WifiOta)
|
// 通过协调器通知 WiFi 层
|
||||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||||
DEBUG_INFO(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
|
DEBUG_INFO(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
|
||||||
}
|
}
|
||||||
@@ -933,7 +931,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
ctx->session_id = session_id;
|
ctx->session_id = session_id;
|
||||||
BaseType_t ok = xTaskCreate(ble_gd32_relay_wait_task,
|
BaseType_t ok = xTaskCreate(ble_gd32_relay_wait_task,
|
||||||
"ble_gd32_wait",
|
"ble_gd32_wait",
|
||||||
3072,
|
TASK_STACK_BLE_GD32_WAIT,
|
||||||
ctx,
|
ctx,
|
||||||
5,
|
5,
|
||||||
NULL);
|
NULL);
|
||||||
@@ -992,7 +990,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
BleOta::instance().m_esp32RebootPending = true;
|
BleOta::instance().m_esp32RebootPending = true;
|
||||||
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
||||||
"ble_ota_finish",
|
"ble_ota_finish",
|
||||||
4096,
|
TASK_STACK_BLE_FINISH,
|
||||||
finish_ctx, // heap context; task 负责 free
|
finish_ctx, // heap context; task 负责 free
|
||||||
5,
|
5,
|
||||||
NULL);
|
NULL);
|
||||||
@@ -1015,7 +1013,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
|||||||
BleOta::instance().m_esp32RebootPending = true;
|
BleOta::instance().m_esp32RebootPending = true;
|
||||||
BaseType_t ok = xTaskCreate(ble_esp32_restart_task,
|
BaseType_t ok = xTaskCreate(ble_esp32_restart_task,
|
||||||
"ble_ota_restart",
|
"ble_ota_restart",
|
||||||
2048,
|
TASK_STACK_BLE_RESTART,
|
||||||
NULL,
|
NULL,
|
||||||
5,
|
5,
|
||||||
NULL);
|
NULL);
|
||||||
@@ -1223,17 +1221,25 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
|||||||
// ACK 按累计连续进度触发,而不是按当前到达包的 seq 触发。缺口包到达后
|
// ACK 按累计连续进度触发,而不是按当前到达包的 seq 触发。缺口包到达后
|
||||||
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
|
// reorder 可能一次 flush 到下个窗口边界,例如 seq=99 使 expected 直达128;
|
||||||
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
|
// 此时必须立即 ACK 127,否则后续重复包都不产生 progress,前端只能超时。
|
||||||
|
//
|
||||||
|
// ★ seq=0 强制 ACK:begin 含整分区预擦,可能耗时数秒~数十秒。
|
||||||
|
// 前端必须等“首包已落盘”ACK 后再发窗口内其余包,否则队列满丢包→重传极慢。
|
||||||
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
|
uint32_t completedPackets = (afterReceived + BLE_OTA_CHUNK_PAYLOAD - 1U) /
|
||||||
BLE_OTA_CHUNK_PAYLOAD;
|
BLE_OTA_CHUNK_PAYLOAD;
|
||||||
bool reachedAckBoundary = completedPackets > 0U &&
|
bool reachedAckBoundary = completedPackets > 0U &&
|
||||||
(completedPackets % BLE_OTA_ACK_WINDOW) == 0U;
|
(completedPackets % BLE_OTA_ACK_WINDOW) == 0U;
|
||||||
|
bool firstPacketReady = (seq == 0U) && progressMade;
|
||||||
if (progressMade &&
|
if (progressMade &&
|
||||||
(reachedAckBoundary ||
|
(reachedAckBoundary ||
|
||||||
transferComplete ||
|
transferComplete ||
|
||||||
isLastPacket)) {
|
isLastPacket ||
|
||||||
|
firstPacketReady)) {
|
||||||
if (isLastPacket || transferComplete) {
|
if (isLastPacket || transferComplete) {
|
||||||
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
DEBUG_INFO(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||||
seq, data_len, (unsigned int)afterReceived, total_size);
|
seq, data_len, (unsigned int)afterReceived, total_size);
|
||||||
|
} else if (firstPacketReady) {
|
||||||
|
DEBUG_INFO(TAG, "ESP32 begin 就绪 ACK: seq=0 received=%u total=%" PRIu32,
|
||||||
|
(unsigned)afterReceived, total_size);
|
||||||
}
|
}
|
||||||
uint16_t ackSeq = s_reorder.expected > 0U
|
uint16_t ackSeq = s_reorder.expected > 0U
|
||||||
? (uint16_t)(s_reorder.expected - 1U)
|
? (uint16_t)(s_reorder.expected - 1U)
|
||||||
@@ -1384,7 +1390,9 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
|||||||
meta.stored_size = stored_size;
|
meta.stored_size = stored_size;
|
||||||
meta.last_seq = seq;
|
meta.last_seq = seq;
|
||||||
const bool transfer_complete = stored_size >= meta.total_size;
|
const bool transfer_complete = stored_size >= meta.total_size;
|
||||||
|
// seq=0:prepare 含整分区预擦,写完首包必须 ACK,前端才能继续发后续窗口
|
||||||
const bool checkpoint =
|
const bool checkpoint =
|
||||||
|
(seq == 0U) ||
|
||||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
|
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U) ||
|
||||||
transfer_complete ||
|
transfer_complete ||
|
||||||
(expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
(expected_payload < BLE_OTA_CHUNK_PAYLOAD);
|
||||||
@@ -1681,6 +1689,14 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
|||||||
s_conn_fallback_requested = true;
|
s_conn_fallback_requested = true;
|
||||||
ble_ota_request_conn_params(BleOta::instance().m_connHandle, true);
|
ble_ota_request_conn_params(BleOta::instance().m_connHandle, true);
|
||||||
}
|
}
|
||||||
|
/* 优先 LE 2M PHY(空中速率约翻倍);对端不支持则保持 1M,失败仅打日志 */
|
||||||
|
{
|
||||||
|
const uint8_t phy_mask = BLE_GAP_LE_PHY_1M_MASK | BLE_GAP_LE_PHY_2M_MASK;
|
||||||
|
int phy_rc = ble_gap_set_prefered_le_phy(
|
||||||
|
BleOta::instance().m_connHandle, phy_mask, phy_mask,
|
||||||
|
BLE_GAP_LE_PHY_CODED_ANY);
|
||||||
|
DEBUG_INFO(TAG, "请求 LE PHY 1M|2M: rc=%d", phy_rc);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
|
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
|
||||||
BleOta::instance().m_bleConnected = false;
|
BleOta::instance().m_bleConnected = false;
|
||||||
@@ -2040,7 +2056,7 @@ esp_err_t BleOta::resumeStack()
|
|||||||
m_suspendedForWifiOta = false;
|
m_suspendedForWifiOta = false;
|
||||||
|
|
||||||
if (!m_stackRunning) {
|
if (!m_stackRunning) {
|
||||||
/* 仅当历史上曾 hard-deinit 过才需要完整 serverStart */
|
/* 仅当曾 hard-deinit 过才需要完整 serverStart */
|
||||||
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
|
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
|
||||||
(unsigned long)esp_get_free_heap_size());
|
(unsigned long)esp_get_free_heap_size());
|
||||||
esp_err_t err = serverStart();
|
esp_err_t err = serverStart();
|
||||||
@@ -2125,7 +2141,7 @@ void BleOta::taskEntry(void *pvParameters)
|
|||||||
if (s_otaDataQueue) {
|
if (s_otaDataQueue) {
|
||||||
BaseType_t wk_ok = xTaskCreate(ble_ota_data_worker_task,
|
BaseType_t wk_ok = xTaskCreate(ble_ota_data_worker_task,
|
||||||
"ble_ota_worker",
|
"ble_ota_worker",
|
||||||
4096, // 峰值仅 ~1.9KB,4KB 留有充足余量
|
TASK_STACK_BLE_WORKER, // 实测 peak~1.1KB,负载峰值曾~1.9KB
|
||||||
NULL,
|
NULL,
|
||||||
10, // 中等优先级,避免被 NimBLE host task 过度抢占
|
10, // 中等优先级,避免被 NimBLE host task 过度抢占
|
||||||
NULL);
|
NULL);
|
||||||
|
|||||||
+15
-181
@@ -1,6 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* http_handlers.cpp - HTTP 请求处理器实现
|
* http_handlers.cpp - HTTP 请求处理器实现
|
||||||
* 从 wifi_ota.cpp 提取的 HTTP handler 函数
|
|
||||||
*/
|
*/
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
@@ -28,6 +27,7 @@
|
|||||||
#include "storage/gd32_flash_store.h"
|
#include "storage/gd32_flash_store.h"
|
||||||
#include "health_monitor.h"
|
#include "health_monitor.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
|
#include "monitor/cpu_stats.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "canfd_bridge.h"
|
#include "canfd_bridge.h"
|
||||||
#include "monitor/can_feedback_task.h"
|
#include "monitor/can_feedback_task.h"
|
||||||
@@ -208,7 +208,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
|
||||||
|
|
||||||
esp_wifi_set_ps(WIFI_PS_NONE);
|
esp_wifi_set_ps(WIFI_PS_NONE);
|
||||||
// ★ L1: WiFi OTA 开始时恢复 BLE 共存状态(通过协调器)
|
// WiFi OTA 开始时恢复 BLE 共存状态(通过协调器)
|
||||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||||
|
|
||||||
char peek;
|
char peek;
|
||||||
@@ -307,7 +307,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
const uint32_t free_pre = esp_get_free_heap_size();
|
const uint32_t free_pre = esp_get_free_heap_size();
|
||||||
if (free_pre < 8192U) {
|
if (free_pre < 8192U) {
|
||||||
(void)esp_task_wdt_reset();
|
(void)esp_task_wdt_reset();
|
||||||
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 2U));
|
vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,12 +615,10 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
|
|||||||
valid[i] = getLatestCanFeedback(i, &results[i]);
|
valid[i] = getLatestCanFeedback(i, &results[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 估算缓冲大小:每通道 ~2KB (统计+ID条目) */
|
/* ★ 修复:改用静态缓冲区,避免每次请求 malloc 8KB 加剧堆碎片化 */
|
||||||
char *resp = (char *)calloc(1, 8192);
|
static char s_feedback_json[8192];
|
||||||
if (!resp) {
|
char *resp = s_feedback_json;
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "no memory");
|
memset(resp, 0, sizeof(s_feedback_json));
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *p = resp;
|
char *p = resp;
|
||||||
p += sprintf(p, "{\"channels\":[");
|
p += sprintf(p, "{\"channels\":[");
|
||||||
@@ -670,7 +668,6 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
|
|||||||
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
httpd_resp_send(req, resp, (int)(p - resp));
|
httpd_resp_send(req, resp, (int)(p - resp));
|
||||||
free(resp);
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -882,7 +879,6 @@ esp_err_t spi_send_handler(httpd_req_t *req)
|
|||||||
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||||
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
|
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
|
||||||
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144;
|
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 6144;
|
||||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
|
|
||||||
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
|
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
|
||||||
static SemaphoreHandle_t s_status_json_mux = nullptr;
|
static SemaphoreHandle_t s_status_json_mux = nullptr;
|
||||||
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
|
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||||
@@ -899,167 +895,6 @@ static SemaphoreHandle_t statusJsonMutex(void)
|
|||||||
return s_status_json_mux;
|
return s_status_json_mux;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CpuTaskSample {
|
|
||||||
UBaseType_t task_number;
|
|
||||||
configRUN_TIME_COUNTER_TYPE runtime;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CpuTaskMetric {
|
|
||||||
const char* name;
|
|
||||||
UBaseType_t task_number;
|
|
||||||
uint64_t runtime_delta;
|
|
||||||
double usage_pct;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CpuUsageSnapshot {
|
|
||||||
bool available;
|
|
||||||
bool valid;
|
|
||||||
uint32_t sample_ms;
|
|
||||||
double usage_pct;
|
|
||||||
double idle_pct;
|
|
||||||
CpuTaskMetric tasks[CPU_TASK_SNAPSHOT_MAX];
|
|
||||||
size_t task_count;
|
|
||||||
};
|
|
||||||
|
|
||||||
static portMUX_TYPE s_cpu_stats_mux = portMUX_INITIALIZER_UNLOCKED;
|
|
||||||
static CpuTaskSample s_cpu_last_samples[CPU_TASK_SNAPSHOT_MAX];
|
|
||||||
static size_t s_cpu_last_count = 0;
|
|
||||||
static configRUN_TIME_COUNTER_TYPE s_cpu_last_total = 0;
|
|
||||||
static int64_t s_cpu_last_time_us = 0;
|
|
||||||
static bool s_cpu_has_last = false;
|
|
||||||
|
|
||||||
static uint64_t runtime_delta(configRUN_TIME_COUNTER_TYPE current, configRUN_TIME_COUNTER_TYPE previous)
|
|
||||||
{
|
|
||||||
return current >= previous ? (uint64_t)(current - previous) : 0U;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool task_name_is_idle(const char* name)
|
|
||||||
{
|
|
||||||
return name && strncmp(name, "IDLE", 4) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void reset_cpu_usage_baseline_locked(const TaskStatus_t* tasks, size_t task_count,
|
|
||||||
configRUN_TIME_COUNTER_TYPE total_runtime,
|
|
||||||
int64_t now_us)
|
|
||||||
{
|
|
||||||
size_t copy_count = std::min(task_count, CPU_TASK_SNAPSHOT_MAX);
|
|
||||||
for (size_t i = 0; i < copy_count; ++i) {
|
|
||||||
s_cpu_last_samples[i].task_number = tasks[i].xTaskNumber;
|
|
||||||
s_cpu_last_samples[i].runtime = tasks[i].ulRunTimeCounter;
|
|
||||||
}
|
|
||||||
s_cpu_last_count = copy_count;
|
|
||||||
s_cpu_last_total = total_runtime;
|
|
||||||
s_cpu_last_time_us = now_us;
|
|
||||||
s_cpu_has_last = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ 改为输出参数模式:CpuUsageSnapshot 608B + CpuTaskSample[24] 192B = 800B
|
|
||||||
// 之前全在栈上,system_status_handler 又拷一份 608B → 叠加 1.4KB,
|
|
||||||
// 加上 OtaSnapshot/TaskStackMetric/wifi_ap_record_t 约 2.3KB 栈消耗。
|
|
||||||
// HTTP 主线程上传 100% 后调用此 handler 时极易栈溢出。改堆分配。
|
|
||||||
static void sample_cpu_usage(CpuUsageSnapshot& result)
|
|
||||||
{
|
|
||||||
result = {};
|
|
||||||
#if defined(CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS)
|
|
||||||
result.available = true;
|
|
||||||
|
|
||||||
UBaseType_t task_count = uxTaskGetNumberOfTasks();
|
|
||||||
if (task_count == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
task_count = std::min<UBaseType_t>(task_count + 2U, CPU_TASK_SNAPSHOT_MAX);
|
|
||||||
|
|
||||||
TaskStatus_t* tasks = (TaskStatus_t*)calloc(task_count, sizeof(TaskStatus_t));
|
|
||||||
if (!tasks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
configRUN_TIME_COUNTER_TYPE total_runtime = 0;
|
|
||||||
UBaseType_t captured_count = uxTaskGetSystemState(tasks, task_count, &total_runtime);
|
|
||||||
int64_t now_us = esp_timer_get_time();
|
|
||||||
if (captured_count == 0 || captured_count > task_count) {
|
|
||||||
DEBUG_WARN("STATUS", "CPU stats: uxTaskGetSystemState 返回 0 (actual_tasks=%lu, buf_slots=%lu)",
|
|
||||||
(unsigned long)uxTaskGetNumberOfTasks(), (unsigned long)task_count);
|
|
||||||
free(tasks);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ★ previous_samples[24] = 192B,改堆分配减少栈压力
|
|
||||||
CpuTaskSample* previous_samples = (CpuTaskSample*)calloc(CPU_TASK_SNAPSHOT_MAX, sizeof(CpuTaskSample));
|
|
||||||
if (!previous_samples) {
|
|
||||||
free(tasks);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t previous_count = 0;
|
|
||||||
configRUN_TIME_COUNTER_TYPE previous_total = 0;
|
|
||||||
int64_t previous_time_us = 0;
|
|
||||||
bool had_previous = false;
|
|
||||||
|
|
||||||
portENTER_CRITICAL(&s_cpu_stats_mux);
|
|
||||||
previous_count = s_cpu_last_count;
|
|
||||||
previous_total = s_cpu_last_total;
|
|
||||||
previous_time_us = s_cpu_last_time_us;
|
|
||||||
had_previous = s_cpu_has_last;
|
|
||||||
for (size_t i = 0; i < previous_count; ++i) {
|
|
||||||
previous_samples[i] = s_cpu_last_samples[i];
|
|
||||||
}
|
|
||||||
reset_cpu_usage_baseline_locked(tasks, captured_count, total_runtime, now_us);
|
|
||||||
portEXIT_CRITICAL(&s_cpu_stats_mux);
|
|
||||||
|
|
||||||
bool can_compute = had_previous && total_runtime >= previous_total && now_us > previous_time_us;
|
|
||||||
uint64_t total_delta = can_compute ? runtime_delta(total_runtime, previous_total) : 0U;
|
|
||||||
uint64_t idle_delta = 0U;
|
|
||||||
if (can_compute && total_delta > 0U) {
|
|
||||||
for (UBaseType_t i = 0; i < captured_count; ++i) {
|
|
||||||
configRUN_TIME_COUNTER_TYPE last_runtime = 0;
|
|
||||||
bool found_previous = false;
|
|
||||||
for (size_t j = 0; j < previous_count; ++j) {
|
|
||||||
if (previous_samples[j].task_number == tasks[i].xTaskNumber) {
|
|
||||||
last_runtime = previous_samples[j].runtime;
|
|
||||||
found_previous = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found_previous) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
uint64_t delta = runtime_delta(tasks[i].ulRunTimeCounter, last_runtime);
|
|
||||||
double pct = (double)delta * 100.0 / (double)total_delta;
|
|
||||||
if (task_name_is_idle(tasks[i].pcTaskName)) {
|
|
||||||
idle_delta += delta;
|
|
||||||
}
|
|
||||||
if (result.task_count < CPU_TASK_SNAPSHOT_MAX) {
|
|
||||||
CpuTaskMetric& metric = result.tasks[result.task_count++];
|
|
||||||
metric.name = tasks[i].pcTaskName ? tasks[i].pcTaskName : "";
|
|
||||||
metric.task_number = tasks[i].xTaskNumber;
|
|
||||||
metric.runtime_delta = delta;
|
|
||||||
metric.usage_pct = pct;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.valid = true;
|
|
||||||
result.sample_ms = (uint32_t)((now_us - previous_time_us) / 1000);
|
|
||||||
result.idle_pct = std::min(100.0, (double)idle_delta * 100.0 / (double)total_delta);
|
|
||||||
result.usage_pct = std::max(0.0, 100.0 - result.idle_pct);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.valid) {
|
|
||||||
DEBUG_DBG("STATUS", "CPU stats: valid=false had_prev=%d total=%lu prev_total=%lu "
|
|
||||||
"can_compute=%d delta=%lu captured=%lu",
|
|
||||||
(int)had_previous,
|
|
||||||
(unsigned long)total_runtime, (unsigned long)previous_total,
|
|
||||||
(int)can_compute, (unsigned long)total_delta,
|
|
||||||
(unsigned long)captured_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(previous_samples);
|
|
||||||
free(tasks);
|
|
||||||
#else
|
|
||||||
result.available = false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
|
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
|
||||||
{
|
{
|
||||||
if (handle == NULL) return 0;
|
if (handle == NULL) return 0;
|
||||||
@@ -1089,6 +924,7 @@ static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...)
|
|||||||
return offset + (int)written;
|
return offset + (int)written;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样) */
|
||||||
static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu)
|
static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu)
|
||||||
{
|
{
|
||||||
offset = append_jsonf(buf, cap, offset,
|
offset = append_jsonf(buf, cap, offset,
|
||||||
@@ -1168,7 +1004,9 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
connected = true;
|
connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ★ PR1: 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter
|
// 先自愈孤儿忙态(如 SHA 失败后卡在 ESP32_FINALIZING),再取快照给 UI
|
||||||
|
OtaCoordinator::instance().healOrphanStates();
|
||||||
|
// 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter
|
||||||
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
|
OtaSnapshot snap = OtaCoordinator::instance().getSnapshot();
|
||||||
|
|
||||||
size_t free_heap = esp_get_free_heap_size();
|
size_t free_heap = esp_get_free_heap_size();
|
||||||
@@ -1190,20 +1028,17 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CPU 采样在堆上;失败则跳过(available=false),不再 500 */
|
/* CPU:只读 cpu_stats 任务的 1Hz 快照,禁止在 httpd 里 uxTaskGetSystemState */
|
||||||
CpuUsageSnapshot cpu_local = {};
|
CpuUsageSnapshot cpu_local = {};
|
||||||
CpuUsageSnapshot *cpu = &cpu_local;
|
CpuUsageSnapshot *cpu = &cpu_local;
|
||||||
CpuUsageSnapshot *cpu_heap = nullptr;
|
CpuUsageSnapshot *cpu_heap = nullptr;
|
||||||
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 1024U)) {
|
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 512U)) {
|
||||||
cpu_heap = (CpuUsageSnapshot *)calloc(1, sizeof(CpuUsageSnapshot));
|
cpu_heap = (CpuUsageSnapshot *)calloc(1, sizeof(CpuUsageSnapshot));
|
||||||
if (cpu_heap) {
|
if (cpu_heap) {
|
||||||
cpu = cpu_heap;
|
cpu = cpu_heap;
|
||||||
sample_cpu_usage(*cpu);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
cpu_local.available = false;
|
|
||||||
cpu_local.valid = false;
|
|
||||||
}
|
}
|
||||||
|
CpuStats::instance().getSnapshot(*cpu);
|
||||||
|
|
||||||
char *resp = s_status_json;
|
char *resp = s_status_json;
|
||||||
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
|
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
|
||||||
@@ -1309,11 +1144,10 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// GET /system/probe — GD32 APP 单次 SPI 探测(PR2)
|
// GET /system/probe — GD32 APP 单次 SPI 探测
|
||||||
// ============================================================
|
// ============================================================
|
||||||
/**
|
/**
|
||||||
* @brief 显式触发一次 GD32 APP 存活探测。
|
* @brief 显式触发一次 GD32 APP 存活探测。
|
||||||
* 从 system_status_handler 移出的副作用,改为独立端点。
|
|
||||||
* 成功时更新 OtaRelay 缓存的 alive/version 并返回版本信息。
|
* 成功时更新 OtaRelay 缓存的 alive/version 并返回版本信息。
|
||||||
*/
|
*/
|
||||||
esp_err_t system_probe_handler(httpd_req_t *req)
|
esp_err_t system_probe_handler(httpd_req_t *req)
|
||||||
|
|||||||
+15
-13
@@ -166,7 +166,7 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
|
|||||||
inst.setStaRetryCount(0);
|
inst.setStaRetryCount(0);
|
||||||
inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志
|
inst.setStaRetryExhausted(false); // ★ 连接成功,重置耗尽标志
|
||||||
inst.stateUnlock();
|
inst.stateUnlock();
|
||||||
// ★ M2: 推送 WiFi 连接状态给 HealthMonitor(解耦监控层主动查询)
|
// 推送 WiFi 连接状态给 HealthMonitor
|
||||||
HealthMonitor::instance().setWifiConnected(true);
|
HealthMonitor::instance().setWifiConnected(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ void WifiOta::wifiEventHandler(void* arg, esp_event_base_t event_base,
|
|||||||
bool exhausted = inst.getStaRetryExhausted();
|
bool exhausted = inst.getStaRetryExhausted();
|
||||||
int retryCount = inst.getStaRetryCount();
|
int retryCount = inst.getStaRetryCount();
|
||||||
inst.stateUnlock();
|
inst.stateUnlock();
|
||||||
// ★ M2: 推送 WiFi 断开状态给 HealthMonitor
|
// 推送 WiFi 断开状态给 HealthMonitor
|
||||||
HealthMonitor::instance().setWifiConnected(false);
|
HealthMonitor::instance().setWifiConnected(false);
|
||||||
|
|
||||||
if (!exhausted) {
|
if (!exhausted) {
|
||||||
@@ -401,15 +401,17 @@ esp_err_t WifiOta::httpServerStart()
|
|||||||
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
||||||
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
||||||
/* LWIP_MAX_SOCKETS=12;HTTP 内部约 3 + UDP 日志 1 → 业务 socket 不宜过大。
|
/* LWIP_MAX_SOCKETS=12;HTTP 内部约 3 + UDP 日志 1 → 业务 socket 不宜过大。
|
||||||
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。 */
|
* 上传期间前端必须停 status/logs(见 app.js pausePollingForUpload)。
|
||||||
config.max_open_sockets = 4;
|
* ★ 修复:增加到 7,防止浏览器超时 abort 后旧连接占满所有槽位。 */
|
||||||
/* 上传中途若再开 status 连接,LRU 会踢掉 /ota/upload → 浏览器卡在 100%。
|
config.max_open_sockets = 7;
|
||||||
* 关闭 LRU 踢连接:宁可新连接失败,也不拆正在写 Flash 的长传。 */
|
/* ★ 修复:启用 LRU 淘汰,当所有 socket 被占满时主动关闭最旧空闲连接。
|
||||||
config.lru_purge_enable = false;
|
* OTA 上传期间前端已暂停轮询(pausePollingForUpload),不会踢掉 upload 连接。
|
||||||
/* 1MB sequential 擦写时,httpd 在 write 阶段可能 >60s 不调 recv;
|
* 若仍担心极端情况,可在 ota_upload_handler 入口设置该 session 的更长超时。 */
|
||||||
* 超时后 handler 内会重试,但过短会放大“卡 100%”观感。 */
|
config.lru_purge_enable = true;
|
||||||
config.recv_wait_timeout = 120;
|
/* ★ 修复:缩短超时,加速释放废弃连接。
|
||||||
config.send_wait_timeout = 60;
|
* OTA 写 Flash 期间 handler 本身持有连接不会被超时踢(httpd 只在 idle 时检测)。 */
|
||||||
|
config.recv_wait_timeout = 10;
|
||||||
|
config.send_wait_timeout = 10;
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
|
||||||
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
|
||||||
@@ -508,7 +510,7 @@ bool WifiOta::isConnected()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)
|
// IBleCoexListener 接口实现
|
||||||
// ============================================================
|
// ============================================================
|
||||||
void WifiOta::onBleOtaStart()
|
void WifiOta::onBleOtaStart()
|
||||||
{
|
{
|
||||||
@@ -603,7 +605,7 @@ bool WifiOta::earlyInit()
|
|||||||
// 创建后台重连任务:避免在系统事件任务中调用 esp_wifi_connect()/vTaskDelay 导致死锁
|
// 创建后台重连任务:避免在系统事件任务中调用 esp_wifi_connect()/vTaskDelay 导致死锁
|
||||||
if (WifiOta::instance().m_reconnectTask == nullptr) {
|
if (WifiOta::instance().m_reconnectTask == nullptr) {
|
||||||
BaseType_t ret = xTaskCreate(&WifiOta::reconnectTask, "wifi_reconnect",
|
BaseType_t ret = xTaskCreate(&WifiOta::reconnectTask, "wifi_reconnect",
|
||||||
4096, NULL, 5,
|
TASK_STACK_WIFI_RECONNECT, NULL, 5,
|
||||||
&WifiOta::instance().m_reconnectTask);
|
&WifiOta::instance().m_reconnectTask);
|
||||||
if (ret != pdPASS) {
|
if (ret != pdPASS) {
|
||||||
DEBUG_ERR("WIFI", "创建重连任务失败");
|
DEBUG_ERR("WIFI", "创建重连任务失败");
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public:
|
|||||||
/** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */
|
/** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */
|
||||||
void setBleOtaActive(bool v) { m_bleOtaActive.store(v); }
|
void setBleOtaActive(bool v) { m_bleOtaActive.store(v); }
|
||||||
|
|
||||||
// ── IBleCoexListener 接口实现(L1: 通过协调器接收 BLE 共存通知)──
|
// ── IBleCoexListener 接口实现 ──
|
||||||
/** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询。 */
|
/** @brief BLE OTA 开始,暂停 WiFi RSSI 轮询。 */
|
||||||
void onBleOtaStart() override;
|
void onBleOtaStart() override;
|
||||||
/** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */
|
/** @brief BLE OTA 结束,恢复 WiFi 射频活动。 */
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
|||||||
return ESP_ERR_INVALID_SIZE;
|
return ESP_ERR_INVALID_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入前按需擦除目标扇区(替代 prepare 阶段整段预擦)
|
// 正常路径:prepare 已整分区预擦;此处仅兜底(水位未覆盖时才擦)
|
||||||
esp_err_t erase_err = ensureErasedForWrite(aligned_offset, aligned_len);
|
esp_err_t erase_err = ensureErasedForWrite(aligned_offset, aligned_len);
|
||||||
if (erase_err != ESP_OK) {
|
if (erase_err != ESP_OK) {
|
||||||
return erase_err;
|
return erase_err;
|
||||||
@@ -457,7 +457,7 @@ esp_err_t Gd32FlashStore::metaVerifyChecksum()
|
|||||||
|
|
||||||
// 检查 checksum 是否全零(未计算过)
|
// 检查 checksum 是否全零(未计算过)
|
||||||
// 注意:返回 INVALID_STATE 而非 ESP_OK,由调用方决定是否容忍。
|
// 注意:返回 INVALID_STATE 而非 ESP_OK,由调用方决定是否容忍。
|
||||||
// 历史上这里直接返回 OK 跳过校验,但 meta 被意外清零时会掩盖固件损坏。
|
// 若直接返回 OK 跳过校验,meta 被意外清零时会掩盖固件损坏。
|
||||||
static const uint8_t kZeroSha[32] = {0};
|
static const uint8_t kZeroSha[32] = {0};
|
||||||
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
|
if (memcmp(meta.checksum, kZeroSha, sizeof(meta.checksum)) == 0) {
|
||||||
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,返回 INVALID_STATE(调用方决定容忍或失败)");
|
DEBUG_WARN("OTA", "metaVerify: checksum 未计算,返回 INVALID_STATE(调用方决定容忍或失败)");
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* gd32_flash_store.h - GD32 OTA 固件 Flash 存储抽象层
|
* gd32_flash_store.h - GD32 OTA 固件 Flash 存储抽象层
|
||||||
*
|
*
|
||||||
* 从 OtaRelay 中剥离的 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验职责。
|
* 封装 Flash 分区读写、NVS 元数据持久化和 SHA-256 校验。
|
||||||
* 传输层(WiFi/BLE)和 OtaRelay 均通过本类操作 GD32 固件存储,
|
* 传输层(WiFi/BLE)和 OtaRelay 均通过本类操作 GD32 固件存储。
|
||||||
* 不再由 OtaRelay 承担存储职责(解决 H1 职责超载)。
|
|
||||||
*
|
*
|
||||||
* 线程模型:
|
* 线程模型:
|
||||||
* - 通过 instance() 单例访问,NVS 元数据读写由内部 mutex 保护
|
* - 通过 instance() 单例访问,NVS 元数据读写由内部 mutex 保护
|
||||||
@@ -72,8 +71,8 @@ public:
|
|||||||
esp_err_t erasePartition(uint32_t size);
|
esp_err_t erasePartition(uint32_t size);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 新一轮 GD32 固件写入会话开始时调用:复位按需擦除水位。
|
* @brief 新一轮 GD32 固件写入会话开始时调用:复位擦除水位。
|
||||||
* prepareGd32Storage 在开始收流前调用,避免整段预擦阻塞 HTTP/BLE 收包。
|
* prepareGd32Storage 随后会 erasePartition(整分区)。
|
||||||
*/
|
*/
|
||||||
void beginWriteSession();
|
void beginWriteSession();
|
||||||
|
|
||||||
@@ -153,9 +152,8 @@ private:
|
|||||||
|
|
||||||
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
const esp_partition_t *m_partition = nullptr; ///< gd32_ota 分区指针(懒初始化)
|
||||||
|
|
||||||
// ── 按需擦除水位:已保证 [0, m_erasedUpTo) 为擦除态(扇区对齐)──
|
// ── 擦除水位:prepare 整分区预擦后 m_erasedUpTo==part->size;
|
||||||
// 收流时 flashWriteChunk 按写入范围推进擦除,替代上传前整段预擦。
|
// flashWriteChunk 内 ensureErased 为安全兜底(水位已覆盖则立即返回)。
|
||||||
// 这样 HTTP 主线程可边收边擦写,浏览器进度不会在“100% 发完”后空等整段擦除。
|
|
||||||
static constexpr uint32_t kFlashSectorSize = 4096U;
|
static constexpr uint32_t kFlashSectorSize = 4096U;
|
||||||
uint32_t m_erasedUpTo = 0;
|
uint32_t m_erasedUpTo = 0;
|
||||||
esp_err_t ensureErasedForWrite(uint32_t offset, size_t len);
|
esp_err_t ensureErasedForWrite(uint32_t offset, size_t len);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* canfd_bridge.cpp - CANFD 3通道通信桥接实现(M4: 从 SpiMaster 提取)
|
* canfd_bridge.cpp - CANFD 3通道通信桥接实现
|
||||||
*/
|
*/
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "canfd_bridge.h"
|
#include "canfd_bridge.h"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* canfd_bridge.h - CANFD 3通道通信桥接(M4: 从 SpiMaster 提取)
|
* canfd_bridge.h - CANFD 3通道通信桥接
|
||||||
*
|
*
|
||||||
* 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责解耦。
|
* 封装 CANFD 帧格式构建 + SPI 传输,与 SpiMaster 的 OTA 协议职责分离。
|
||||||
* 通过 SpiMaster 的公共 SPI 接口完成底层传输。
|
* 通过 SpiMaster 的公共 SPI 接口完成底层传输。
|
||||||
*/
|
*/
|
||||||
#ifndef CANFD_BRIDGE_H
|
#ifndef CANFD_BRIDGE_H
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "spi_link_v2.h"
|
#include "spi_link_v2.h"
|
||||||
|
#include "app_config.h"
|
||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ SpiMaster& SpiMaster::instance()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// L4 SPI Worker:唯一持 SPI 的任务;ISR 只通知本任务
|
// SPI Worker:唯一持 SPI 的任务;ISR 只通知本任务
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
bool SpiMaster::onWorker() const
|
bool SpiMaster::onWorker() const
|
||||||
@@ -171,7 +172,7 @@ esp_err_t SpiMaster::serviceRingOnce()
|
|||||||
m_gd32RequestPending = false;
|
m_gd32RequestPending = false;
|
||||||
m_linkState.enterBusy(HandshakeState::SERVING_GD32_RING);
|
m_linkState.enterBusy(HandshakeState::SERVING_GD32_RING);
|
||||||
|
|
||||||
if (waitSpiDataReady(5U) != ESP_OK) {
|
if (waitSpiDataReady(3U) != ESP_OK) {
|
||||||
m_linkState.exitBusy();
|
m_linkState.exitBusy();
|
||||||
return ESP_ERR_NOT_FOUND;
|
return ESP_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -295,7 +296,7 @@ void SpiMaster::workerLoop()
|
|||||||
{
|
{
|
||||||
m_workerTask = xTaskGetCurrentTaskHandle();
|
m_workerTask = xTaskGetCurrentTaskHandle();
|
||||||
m_spiTaskHandle = m_workerTask;
|
m_spiTaskHandle = m_workerTask;
|
||||||
DEBUG_INFO("SPI", "SPI worker started (L4), handle=%p", (void *)m_workerTask);
|
DEBUG_INFO("SPI", "SPI worker started, handle=%p", (void *)m_workerTask);
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
/* ★ 只在 ISR 置位 m_gd32RequestPending 时读 EVENT。
|
/* ★ 只在 ISR 置位 m_gd32RequestPending 时读 EVENT。
|
||||||
@@ -309,7 +310,7 @@ void SpiMaster::workerLoop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Job *job = nullptr;
|
Job *job = nullptr;
|
||||||
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(20)) == pdTRUE && job != nullptr) {
|
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
|
||||||
executeJob(*job);
|
executeJob(*job);
|
||||||
if (job->done) {
|
if (job->done) {
|
||||||
xSemaphoreGive(job->done);
|
xSemaphoreGive(job->done);
|
||||||
@@ -317,9 +318,12 @@ void SpiMaster::workerLoop()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 无 job:等 GPIO / 提交唤醒 */
|
/* 无 job:等 GPIO / 提交唤醒。
|
||||||
|
* 超时只是无事件时的兜底回扫周期;ISR 的 xTaskNotifyFromISR 会立即唤醒
|
||||||
|
* 本任务,响应延迟不由这里决定。过短会令 spi_worker(优先级6) 忙转压住
|
||||||
|
* httpd(优先级5),导致 /system/status、/logs 响应不来 → 网页收不到消息。 */
|
||||||
uint32_t bits = 0U;
|
uint32_t bits = 0U;
|
||||||
(void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(50));
|
(void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(25));
|
||||||
if (m_suppressRingService) {
|
if (m_suppressRingService) {
|
||||||
m_gd32RequestPending = false;
|
m_gd32RequestPending = false;
|
||||||
} else if ((bits & 0x01U) || m_gd32RequestPending) {
|
} else if ((bits & 0x01U) || m_gd32RequestPending) {
|
||||||
@@ -386,7 +390,7 @@ esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms)
|
|||||||
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
||||||
|
|
||||||
/* PB6 低:可能有未读响应。短等升高;BOOT 修好交付清 pending 后应很快。
|
/* PB6 低:可能有未读响应。短等升高;BOOT 修好交付清 pending 后应很快。
|
||||||
* L3:超时返回 ESP_ERR_TIMEOUT,禁止调用方假装“已就绪”后盲发。 */
|
* 超时返回 ESP_ERR_TIMEOUT,禁止调用方假装“已就绪”后盲发。 */
|
||||||
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||||
while (xTaskGetTickCount() < deadline) {
|
while (xTaskGetTickCount() < deadline) {
|
||||||
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
if (readReadyLevelRaw() == 1) { return ESP_OK; }
|
||||||
@@ -800,7 +804,7 @@ esp_err_t SpiMaster::init()
|
|||||||
m_stats = SpiLinkStats{};
|
m_stats = SpiLinkStats{};
|
||||||
m_eventHead = m_eventTail = m_eventCount = 0;
|
m_eventHead = m_eventTail = m_eventCount = 0;
|
||||||
|
|
||||||
/* L4:job 队列 + 事件信号量 + SPI worker。
|
/* job 队列 + 事件信号量 + SPI worker。
|
||||||
* worker 启动后可能立刻抢 m_mutex;init 持锁时 worker 会阻塞在 LockGuard,
|
* worker 启动后可能立刻抢 m_mutex;init 持锁时 worker 会阻塞在 LockGuard,
|
||||||
* init 返回释放锁后 worker 继续 —— 不会死锁。 */
|
* init 返回释放锁后 worker 继续 —— 不会死锁。 */
|
||||||
if (m_jobQueue == nullptr) {
|
if (m_jobQueue == nullptr) {
|
||||||
@@ -818,13 +822,13 @@ esp_err_t SpiMaster::init()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_workerTask == nullptr) {
|
if (m_workerTask == nullptr) {
|
||||||
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", 6144, this, 6, &m_workerTask);
|
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 6, &m_workerTask);
|
||||||
if (ok != pdPASS || m_workerTask == nullptr) {
|
if (ok != pdPASS || m_workerTask == nullptr) {
|
||||||
DEBUG_ERR("SPI", "SPI worker task create failed");
|
DEBUG_ERR("SPI", "SPI worker task create failed");
|
||||||
m_workerTask = nullptr;
|
m_workerTask = nullptr;
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
DEBUG_INFO("SPI", "SPI worker task created (L4)");
|
DEBUG_INFO("SPI", "SPI worker task created");
|
||||||
}
|
}
|
||||||
m_spiTaskHandle = m_workerTask;
|
m_spiTaskHandle = m_workerTask;
|
||||||
|
|
||||||
@@ -855,14 +859,15 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
|
|||||||
return ESP_ERR_INVALID_SIZE;
|
return ESP_ERR_INVALID_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* L5:remoteCredit==0 时禁止发新 REQUEST;等 CREDIT_UPDATE 或 poll 读到新 credit。
|
/* CREDIT 背压(包间节奏):
|
||||||
|
* remoteCredit==0 禁止发新 REQUEST;等 CREDIT_UPDATE / 帧头 credit。
|
||||||
* HELLO / 链路控制不受 credit 限制(session 建立前 credit 可能为 0)。 */
|
* HELLO / 链路控制不受 credit 限制(session 建立前 credit 可能为 0)。 */
|
||||||
if (type != V2_TYPE_LINK_HELLO && m_remoteCredit == 0U) {
|
if (type != V2_TYPE_LINK_HELLO && m_remoteCredit == 0U) {
|
||||||
const TickType_t cred_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(
|
const TickType_t cred_deadline = xTaskGetTickCount() + pdMS_TO_TICKS(
|
||||||
timeout_ms > 0U ? timeout_ms : 500U);
|
timeout_ms > 0U ? timeout_ms : 500U);
|
||||||
while (m_remoteCredit == 0U && xTaskGetTickCount() < cred_deadline) {
|
while (m_remoteCredit == 0U && xTaskGetTickCount() < cred_deadline) {
|
||||||
/* 有 DATA_READY 时变长读一帧(可能是 CREDIT_UPDATE 或陈旧响应) */
|
/* 有 DATA_READY 时变长读一帧(可能是 CREDIT_UPDATE 或陈旧响应) */
|
||||||
if (waitSpiDataReady(20U) == ESP_OK) {
|
if (waitSpiDataReady(5U) == ESP_OK) {
|
||||||
uint8_t drain[SPI_OTA_FRAME_MAX];
|
uint8_t drain[SPI_OTA_FRAME_MAX];
|
||||||
size_t drain_len = 0U;
|
size_t drain_len = 0U;
|
||||||
if (readV2FrameVarlen(drain, sizeof(drain), &drain_len, 100U) == ESP_OK) {
|
if (readV2FrameVarlen(drain, sizeof(drain), &drain_len, 100U) == ESP_OK) {
|
||||||
@@ -1195,7 +1200,7 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
|||||||
return send_ret;
|
return send_ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 等 GD32 完成 onCsRelease;poll 内仅在 DATA_READY 低时变长读(L3 不盲 CS)。 */
|
/* 等 GD32 完成 onCsRelease;poll 内仅在 DATA_READY 低时变长读(不盲 CS)。 */
|
||||||
{
|
{
|
||||||
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
|
const TickType_t gap = (pdMS_TO_TICKS(1) > 0) ? pdMS_TO_TICKS(1) : 1;
|
||||||
vTaskDelay(gap);
|
vTaskDelay(gap);
|
||||||
@@ -1334,13 +1339,13 @@ esp_err_t SpiMaster::appRequestBootloader()
|
|||||||
|
|
||||||
/* 先变长读最多 2 次,吞掉残留 0xC1(仅 DATA_READY 低时读,禁止固定长度盲读)。 */
|
/* 先变长读最多 2 次,吞掉残留 0xC1(仅 DATA_READY 低时读,禁止固定长度盲读)。 */
|
||||||
for (uint32_t d = 0U; d < 2U; ++d) {
|
for (uint32_t d = 0U; d < 2U; ++d) {
|
||||||
if (waitSpiDataReady(10U) != ESP_OK) {
|
if (waitSpiDataReady(5U) != ESP_OK) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
|
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
|
||||||
size_t rx_len = 0U;
|
size_t rx_len = 0U;
|
||||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||||
(void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 50U);
|
(void)readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 20U);
|
||||||
m_linkState.exitBusy();
|
m_linkState.exitBusy();
|
||||||
vTaskDelay((pdMS_TO_TICKS(5) > 0) ? pdMS_TO_TICKS(5) : 1);
|
vTaskDelay((pdMS_TO_TICKS(5) > 0) ? pdMS_TO_TICKS(5) : 1);
|
||||||
}
|
}
|
||||||
@@ -1354,7 +1359,7 @@ esp_err_t SpiMaster::appRequestBootloader()
|
|||||||
|
|
||||||
for (uint32_t retry = 0U; retry < kMaxSends; ++retry) {
|
for (uint32_t retry = 0U; retry < kMaxSends; ++retry) {
|
||||||
if (retry > 0U) {
|
if (retry > 0U) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(50));
|
vTaskDelay(pdMS_TO_TICKS(2));
|
||||||
/* 序号/会话错乱时重同步一次再发 0xB0 */
|
/* 序号/会话错乱时重同步一次再发 0xB0 */
|
||||||
if (retry == 1U) {
|
if (retry == 1U) {
|
||||||
m_v2Active = false;
|
m_v2Active = false;
|
||||||
@@ -1412,11 +1417,11 @@ esp_err_t SpiMaster::appRequestBootloader()
|
|||||||
while (xTaskGetTickCount() <= deadline) {
|
while (xTaskGetTickCount() <= deadline) {
|
||||||
++poll_i;
|
++poll_i;
|
||||||
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
m_linkState.enterBusy(HandshakeState::WAITING_RESPONSE);
|
||||||
(void)waitSpiDataReady(poll_i <= 2U ? 40U : 10U);
|
(void)waitSpiDataReady(poll_i <= 2U ? 20U : 5U);
|
||||||
|
|
||||||
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
|
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
|
||||||
size_t rx_len = 0U;
|
size_t rx_len = 0U;
|
||||||
ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 80U);
|
ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, 40U);
|
||||||
m_linkState.exitBusy();
|
m_linkState.exitBusy();
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
vTaskDelay(gap);
|
vTaskDelay(gap);
|
||||||
@@ -1470,7 +1475,7 @@ esp_err_t SpiMaster::appRequestBootloader()
|
|||||||
if (command_sent) {
|
if (command_sent) {
|
||||||
/* 静默:APP 擦配置页(可达数十 ms) + 软复位 + BOOT 起机。
|
/* 静默:APP 擦配置页(可达数十 ms) + 软复位 + BOOT 起机。
|
||||||
* 此间禁止 HELLO,否则 role 会一直像卡在 APP。 */
|
* 此间禁止 HELLO,否则 role 会一直像卡在 APP。 */
|
||||||
const uint32_t kQuietMs = got_ack ? 900U : 500U;
|
const uint32_t kQuietMs = got_ack ? 100U : 30U;
|
||||||
DEBUG_INFO("SPI", "ENTER_BOOT 已上总线 (ack=%s),静默 %lums 等待 APP 复位...",
|
DEBUG_INFO("SPI", "ENTER_BOOT 已上总线 (ack=%s),静默 %lums 等待 APP 复位...",
|
||||||
got_ack ? "yes" : "no", (unsigned long)kQuietMs);
|
got_ack ? "yes" : "no", (unsigned long)kQuietMs);
|
||||||
vTaskDelay(pdMS_TO_TICKS(kQuietMs));
|
vTaskDelay(pdMS_TO_TICKS(kQuietMs));
|
||||||
@@ -1551,7 +1556,7 @@ esp_err_t SpiMaster::waitForBootloaderReady(uint32_t timeout_ms)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 向 GD32 APP 发送存活探测(L3:帧长发送 + DATA_READY 变长读)。
|
* @brief 向 GD32 APP 发送存活探测(帧长发送 + DATA_READY 变长读)。
|
||||||
*
|
*
|
||||||
* 两步法(持锁):otaTransferUnlocked(expect_stale) + otaPollResponseUnlocked。
|
* 两步法(持锁):otaTransferUnlocked(expect_stale) + otaPollResponseUnlocked。
|
||||||
* GD32 App PING 响应 type=kRspAck(0x80),
|
* GD32 App PING 响应 type=kRspAck(0x80),
|
||||||
@@ -1789,5 +1794,3 @@ esp_err_t SpiMaster::hello(uint32_t timeout_ms, uint8_t role_hint)
|
|||||||
return ESP_ERR_INVALID_RESPONSE;
|
return ESP_ERR_INVALID_RESPONSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦)
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@
|
|||||||
#define SPI_OTA_RSP_WAIT (0x81)
|
#define SPI_OTA_RSP_WAIT (0x81)
|
||||||
#define SPI_OTA_RSP_ERROR (0x82)
|
#define SPI_OTA_RSP_ERROR (0x82)
|
||||||
|
|
||||||
// CANFD 类型已迁移至 transport/canfd_bridge.h(M4 解耦)
|
|
||||||
|
|
||||||
// GD32 版本信息(从 APP PING 响应解析)
|
// GD32 版本信息(从 APP PING 响应解析)
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint16_t app_major;
|
uint16_t app_major;
|
||||||
@@ -121,14 +119,14 @@ public:
|
|||||||
void confirmTxSeqAccepted();
|
void confirmTxSeqAccepted();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief L4 统一请求入口:经 SPI worker 串行执行(send+poll)。
|
* @brief 统一请求入口:经 SPI worker 串行执行(send+poll)。
|
||||||
*/
|
*/
|
||||||
esp_err_t transact(uint8_t type, const uint8_t *payload, size_t payload_len,
|
esp_err_t transact(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||||
uint8_t *resp_type, uint8_t *resp_payload,
|
uint8_t *resp_type, uint8_t *resp_payload,
|
||||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief L4 取出 GD32 EVENT 帧(worker 已读入事件队列)。
|
* @brief 取出 GD32 EVENT 帧(worker 已读入事件队列)。
|
||||||
* @param timeout_ms 等待超时(ms)。0 = 非阻塞(原语义)。
|
* @param timeout_ms 等待超时(ms)。0 = 非阻塞(原语义)。
|
||||||
* @return true 取到事件;false 队列空且超时未到达。
|
* @return true 取到事件;false 队列空且超时未到达。
|
||||||
*/
|
*/
|
||||||
@@ -196,8 +194,6 @@ public:
|
|||||||
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
||||||
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
|
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
|
||||||
|
|
||||||
// ── CANFD 接口已迁移至 transport/canfd_bridge.h(M4 解耦)──
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* @brief 私有默认构造函数。
|
* @brief 私有默认构造函数。
|
||||||
@@ -271,7 +267,7 @@ private:
|
|||||||
/** 事件计数信号量(worker push / takeEvent pop)。 */
|
/** 事件计数信号量(worker push / takeEvent pop)。 */
|
||||||
SemaphoreHandle_t m_eventSem = NULL;
|
SemaphoreHandle_t m_eventSem = NULL;
|
||||||
|
|
||||||
// ── L4 多槽事件队列(worker 写入,业务 takeEvent 读出)──
|
// ── 多槽事件队列(worker 写入,业务 takeEvent 读出)──
|
||||||
static constexpr uint8_t kEventSlots = 4U;
|
static constexpr uint8_t kEventSlots = 4U;
|
||||||
struct EventSlot {
|
struct EventSlot {
|
||||||
uint8_t type;
|
uint8_t type;
|
||||||
@@ -283,7 +279,7 @@ private:
|
|||||||
uint8_t m_eventTail = 0;
|
uint8_t m_eventTail = 0;
|
||||||
uint8_t m_eventCount = 0;
|
uint8_t m_eventCount = 0;
|
||||||
|
|
||||||
// ── L4 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
|
// ── 作业提交(调用方栈上 Job + StaticSemaphore,worker 执行)──
|
||||||
enum class JobKind : uint8_t {
|
enum class JobKind : uint8_t {
|
||||||
Transact = 0,
|
Transact = 0,
|
||||||
Poll,
|
Poll,
|
||||||
@@ -390,7 +386,7 @@ private:
|
|||||||
uint32_t timeout_ms, uint32_t trans_flags);
|
uint32_t timeout_ms, uint32_t trans_flags);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief L3 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
|
* @brief 变长读:同 CS 先读 16B Header,再按 LEN 读 payload+CRC。
|
||||||
* 调用者须已持锁;仅在 DATA_READY 已低(有数据)时调用,禁止盲读。
|
* 调用者须已持锁;仅在 DATA_READY 已低(有数据)时调用,禁止盲读。
|
||||||
* @param out_frame 完整帧输出(含头+payload+CRC)
|
* @param out_frame 完整帧输出(含头+payload+CRC)
|
||||||
* @param out_max 输出缓冲容量
|
* @param out_max 输出缓冲容量
|
||||||
|
|||||||
+18
-2
@@ -4,11 +4,19 @@ CONFIG_ESP_WIFI_TX_BA_WIN=6
|
|||||||
CONFIG_DCU_OTA_POLL_BACKOFF_MS=5
|
CONFIG_DCU_OTA_POLL_BACKOFF_MS=5
|
||||||
CONFIG_DCU_OTA_FINISH_TIMEOUT_MS=10000
|
CONFIG_DCU_OTA_FINISH_TIMEOUT_MS=10000
|
||||||
|
|
||||||
|
# Flash mode DIO(锁定):本板 PCB 实测 QIO 不可用。
|
||||||
|
# 2026-07 尝试过 CONFIG_ESPTOOLPY_FLASHMODE_QIO=y 加速 OTA,结果:
|
||||||
|
# 开机 boot 日志 SPI Mode : QIO 看似正常,但 WiFi OTA 写 flash 后
|
||||||
|
# 从分区读回计算 SHA-256 就与上传前 SHA 不一致(每次都不过),
|
||||||
|
# 属于“能启动但数据不可靠”。推断:当前 flash 芯片/PCB 走线/WP+HOLD 不支持 QIO。
|
||||||
|
# 不要再开 QIO,除非确认 flash 型号与硬件连接均兼容。
|
||||||
|
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
|
||||||
|
|
||||||
# DCU SPI clock -- 4 MHz(GD32 SPI0 RX DMA 武装后可安全提速;8/12MHz 须零错误验证后再开)
|
# DCU SPI clock -- 4 MHz(GD32 SPI0 RX DMA 武装后可安全提速;8/12MHz 须零错误验证后再开)
|
||||||
CONFIG_DCU_SPI_CLOCK_HZ=4000000
|
CONFIG_DCU_SPI_CLOCK_HZ=4000000
|
||||||
|
|
||||||
# DCU OTA packet interval -- 给 GD32 主循环 AES/Flash drain 留时间(原 1ms 易在中后期卡死)
|
# GD32 OTA 包间节奏:不用固定 PACKET_INTERVAL;依赖 SpiMaster CREDIT 背压
|
||||||
CONFIG_DCU_OTA_PACKET_INTERVAL_MS=3
|
# (flash 槽满时 CREDIT=0,等 CREDIT_UPDATE / 帧头 credit 恢复后再发)
|
||||||
|
|
||||||
# LWIP: HTTP server max_open_sockets=4 + WiFi/LWIP 内部 ~3 + UDP debug log + 余量
|
# LWIP: HTTP server max_open_sockets=4 + WiFi/LWIP 内部 ~3 + UDP debug log + 余量
|
||||||
# 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE)
|
# 原值 7 与 httpd max_open_sockets=4 仅余 3 个 socket,一旦 handler 阻塞即 accept(EMFILE)
|
||||||
@@ -35,6 +43,14 @@ CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
|
|||||||
CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U32=y
|
CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U32=y
|
||||||
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
|
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
|
||||||
|
|
||||||
|
# ── BLE / NimBLE 主机栈(set-target 会重生 sdkconfig,必须写在 defaults 里)──
|
||||||
|
CONFIG_BT_ENABLED=y
|
||||||
|
CONFIG_BT_NIMBLE_ENABLED=y
|
||||||
|
CONFIG_BT_CONTROLLER_ENABLED=y
|
||||||
|
# CONFIG_BT_BLUEDROID_ENABLED is not set
|
||||||
|
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
|
||||||
|
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y
|
||||||
|
|
||||||
# ── NimBLE: 禁用未使用的标准 GATT 服务(只保留自定义 OTA service)──
|
# ── NimBLE: 禁用未使用的标准 GATT 服务(只保留自定义 OTA service)──
|
||||||
# 每项 service 含 characteristic + descriptor + UUID 注册,合计约占 8-15 KB
|
# 每项 service 含 characteristic + descriptor + UUID 注册,合计约占 8-15 KB
|
||||||
CONFIG_BT_NIMBLE_PROX_SERVICE=n
|
CONFIG_BT_NIMBLE_PROX_SERVICE=n
|
||||||
|
|||||||
+3
-3
@@ -431,8 +431,8 @@
|
|||||||
<span id="toastMsg">操作成功</span>
|
<span id="toastMsg">操作成功</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="js/app.js?v=20260715a"></script>
|
<script type="module" src="js/app.js?v=20260716c"></script>
|
||||||
<script type="module" src="js/ota/console-mode.js?v=20260715a"></script>
|
<script type="module" src="js/ota/console-mode.js?v=20260716c"></script>
|
||||||
<script type="module" src="js/ble_ota.js?v=20260715a"></script>
|
<script type="module" src="js/ble_ota.js?v=20260716c"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+11
-4
@@ -12,11 +12,11 @@ import { showToast } from './common/toast.js';
|
|||||||
import { addLog, clearLogs, setLogFilter, toggleAutoScroll } from './common/log.js';
|
import { addLog, clearLogs, setLogFilter, toggleAutoScroll } from './common/log.js';
|
||||||
import {
|
import {
|
||||||
updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi,
|
updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi,
|
||||||
testConnection, refreshSystemStatusNow
|
testConnection, refreshSystemStatusNow, diagnoseNetwork
|
||||||
} from './common/connection.js';
|
} from './common/connection.js';
|
||||||
import {
|
import {
|
||||||
scheduleStatusPoll, startPolling, pausePollingForUpload,
|
scheduleStatusPoll, startPolling, pausePollingForUpload,
|
||||||
resumePollingAfterUpload, pollStatus, pollLogs
|
resumePollingAfterUpload, pollStatus, pollLogs, setStatusPollIntervalMs
|
||||||
} from './common/polling.js';
|
} from './common/polling.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -40,9 +40,12 @@ import {
|
|||||||
} from './ota/file-handle.js';
|
} from './ota/file-handle.js';
|
||||||
import { sendSpiFrame, clearSpiFields, updateSpiHint } from './ota/spi.js';
|
import { sendSpiFrame, clearSpiFields, updateSpiHint } from './ota/spi.js';
|
||||||
import { startUpload, verifyEsp32Firmware, restartEsp32 } from './ota/upload-esp32.js';
|
import { startUpload, verifyEsp32Firmware, restartEsp32 } from './ota/upload-esp32.js';
|
||||||
import { triggerGd32Relay, markGd32OtaUiComplete, stopRelayProgressWatch, startRelayProgressWatch } from './ota/upload-gd32.js';
|
import {
|
||||||
|
triggerGd32Relay, markGd32OtaUiComplete, stopRelayProgressWatch,
|
||||||
|
startRelayProgressWatch, emitRelayProgress, ensureRelayProgressComplete
|
||||||
|
} from './ota/upload-gd32.js';
|
||||||
|
|
||||||
// ==================== 工具函数(部分未拆分的辅助函数) ====================
|
// ==================== 工具函数 ====================
|
||||||
function formatPercent(value) {
|
function formatPercent(value) {
|
||||||
if (!Number.isFinite(value)) return '--';
|
if (!Number.isFinite(value)) return '--';
|
||||||
return value.toFixed(1).replace(/\.0$/, '') + '%';
|
return value.toFixed(1).replace(/\.0$/, '') + '%';
|
||||||
@@ -178,6 +181,8 @@ window.triggerGd32Relay = triggerGd32Relay;
|
|||||||
window.markGd32OtaUiComplete = markGd32OtaUiComplete;
|
window.markGd32OtaUiComplete = markGd32OtaUiComplete;
|
||||||
window.stopRelayProgressWatch = stopRelayProgressWatch;
|
window.stopRelayProgressWatch = stopRelayProgressWatch;
|
||||||
window.startRelayProgressWatch = startRelayProgressWatch;
|
window.startRelayProgressWatch = startRelayProgressWatch;
|
||||||
|
window.emitRelayProgress = emitRelayProgress;
|
||||||
|
window.ensureRelayProgressComplete = ensureRelayProgressComplete;
|
||||||
|
|
||||||
// ==================== 挂载 common/* 函数到 window(供 polling.js 等通过 window.* 调用) ====================
|
// ==================== 挂载 common/* 函数到 window(供 polling.js 等通过 window.* 调用) ====================
|
||||||
window.formatBytes = formatBytes;
|
window.formatBytes = formatBytes;
|
||||||
@@ -196,11 +201,13 @@ window.updateHeapAndTaskStatusUi = updateHeapAndTaskStatusUi;
|
|||||||
window.testConnection = testConnection;
|
window.testConnection = testConnection;
|
||||||
window.refreshSystemStatusNow = refreshSystemStatusNow;
|
window.refreshSystemStatusNow = refreshSystemStatusNow;
|
||||||
window.scheduleStatusPoll = scheduleStatusPoll;
|
window.scheduleStatusPoll = scheduleStatusPoll;
|
||||||
|
window.setStatusPollIntervalMs = setStatusPollIntervalMs;
|
||||||
window.startPolling = startPolling;
|
window.startPolling = startPolling;
|
||||||
window.pausePollingForUpload = pausePollingForUpload;
|
window.pausePollingForUpload = pausePollingForUpload;
|
||||||
window.resumePollingAfterUpload = resumePollingAfterUpload;
|
window.resumePollingAfterUpload = resumePollingAfterUpload;
|
||||||
window.pollStatus = pollStatus;
|
window.pollStatus = pollStatus;
|
||||||
window.pollLogs = pollLogs;
|
window.pollLogs = pollLogs;
|
||||||
|
window.diagnoseNetwork = diagnoseNetwork;
|
||||||
|
|
||||||
// ==================== 挂载本入口的辅助函数到 window ====================
|
// ==================== 挂载本入口的辅助函数到 window ====================
|
||||||
window.getEspIp = getEspIp;
|
window.getEspIp = getEspIp;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const UUIDS = {
|
|||||||
version: 0x0004,
|
version: 0x0004,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UI_BUILD = '2026-07-12-ble-reorder-v6';
|
export const UI_BUILD = '2026-07-16-ble-begin-ready';
|
||||||
export const BLE_OTA_NOTIFY_ACK = 0xA1;
|
export const BLE_OTA_NOTIFY_ACK = 0xA1;
|
||||||
export const BLE_OTA_NOTIFY_FINISH = 0xB1;
|
export const BLE_OTA_NOTIFY_FINISH = 0xB1;
|
||||||
export const BLE_OTA_NOTIFY_SHA256 = 0xC1;
|
export const BLE_OTA_NOTIFY_SHA256 = 0xC1;
|
||||||
|
|||||||
+40
-16
@@ -189,6 +189,9 @@ export async function uploadFirmware() {
|
|||||||
bleLog('OTA', `开始写入 DATA_IN: build=${UI_BUILD}, target=${cfg.label}, file=${file.name}, total=${totalSize}, packets=${packetTotal}, payload=${payloadSize}, windowAck=${BLE_OTA_ACK_WINDOW}`);
|
bleLog('OTA', `开始写入 DATA_IN: build=${UI_BUILD}, target=${cfg.label}, file=${file.name}, total=${totalSize}, packets=${packetTotal}, payload=${payloadSize}, windowAck=${BLE_OTA_ACK_WINDOW}`);
|
||||||
|
|
||||||
const MAX_WINDOW_RETRIES = 3;
|
const MAX_WINDOW_RETRIES = 3;
|
||||||
|
// 整分区预擦可长达数十秒;首包 ACK 超时必须覆盖 begin/erase
|
||||||
|
const BEGIN_READY_TIMEOUT_MS = 120000;
|
||||||
|
const WINDOW_ACK_TIMEOUT_MS = 8000;
|
||||||
for (let windowStart = 0; windowStart < packetTotal; windowStart += BLE_OTA_ACK_WINDOW) {
|
for (let windowStart = 0; windowStart < packetTotal; windowStart += BLE_OTA_ACK_WINDOW) {
|
||||||
if (state.abortRequested) throw new Error('用户中止');
|
if (state.abortRequested) throw new Error('用户中止');
|
||||||
|
|
||||||
@@ -197,37 +200,50 @@ export async function uploadFirmware() {
|
|||||||
// Windows Web Bluetooth 同一时刻只允许一个 GATT 操作。逐包等待仅保证
|
// Windows Web Bluetooth 同一时刻只允许一个 GATT 操作。逐包等待仅保证
|
||||||
// 提交顺序;实际吞吐由设备请求的高速 BLE 连接参数决定。
|
// 提交顺序;实际吞吐由设备请求的高速 BLE 连接参数决定。
|
||||||
// ★ 不再在循环内 setProgress:进度完全由设备 ACK 后的 state.lastAckBytes 驱动
|
// ★ 不再在循环内 setProgress:进度完全由设备 ACK 后的 state.lastAckBytes 驱动
|
||||||
|
//
|
||||||
|
// ★ 首窗特殊流控:seq=0 触发设备 begin+整分区预擦(worker 线程,可阻塞很久)。
|
||||||
|
// 旧逻辑 sleep(200) 后立刻 withoutResponse 连发 → 队列满丢包 → 整窗重传,体感极慢。
|
||||||
|
// 现改为:seq=0 送达后等设备「首包落盘 ACK」再发 1..endSeq。
|
||||||
for (let seq = windowStart; seq <= endSeq; seq++) {
|
for (let seq = windowStart; seq <= endSeq; seq++) {
|
||||||
// ★ 中止检查放在循环内:abortTask 可能在本窗口发送过程中触发
|
|
||||||
// (windowStart 处的检查无法覆盖窗口中途的 abort)
|
|
||||||
if (state.abortRequested) throw new Error('用户中止');
|
if (state.abortRequested) throw new Error('用户中止');
|
||||||
const start = seq * payloadSize;
|
const start = seq * payloadSize;
|
||||||
const end = Math.min(start + payloadSize, totalSize);
|
const end = Math.min(start + payloadSize, totalSize);
|
||||||
const payload = fileBytes.slice(start, end);
|
const payload = fileBytes.slice(start, end);
|
||||||
const packet = buildPacket(seq, totalSize, payload, cfg.packetCmd);
|
const packet = buildPacket(seq, totalSize, payload, cfg.packetCmd);
|
||||||
|
|
||||||
// ★ seq=0 必须用 writeValueWithResponse(带响应):
|
|
||||||
// seq=0 触发设备端 esp_ota_begin()(flash 擦除 + OTA 会话创建),
|
|
||||||
// 若用 writeValueWithoutResponse 在 BLE 链路层丢失,
|
|
||||||
// 设备永远不会启动 OTA 会话,后续所有包都被丢弃。
|
|
||||||
if (seq === 0) {
|
if (seq === 0) {
|
||||||
try {
|
try {
|
||||||
await state.dataInChar.writeValueWithResponse(packet);
|
await state.dataInChar.writeValueWithResponse(packet);
|
||||||
bleLog('OTA', 'seq=0 已用 writeValueWithResponse 确认送达', 'I');
|
bleLog('OTA', 'seq=0 已用 writeValueWithResponse 确认送达,等待设备 begin/预擦完成…', 'I');
|
||||||
|
setBleStageText('设备预擦 Flash 中,请等待…');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bleLog('OTA', `seq=0 写入失败: ${e.message}`, 'E');
|
bleLog('OTA', `seq=0 写入失败: ${e.message}`, 'E');
|
||||||
throw new Error('seq=0 写入失败,OTA 会话无法启动: ' + e.message);
|
throw new Error('seq=0 写入失败,OTA 会话无法启动: ' + e.message);
|
||||||
}
|
}
|
||||||
// seq=0 写入后等待设备完成 esp_ota_begin()(flash 擦除较耗时)
|
state.ackGeneration++;
|
||||||
await sleep(200);
|
const firstBytes = Math.min(payloadSize, totalSize);
|
||||||
} else {
|
try {
|
||||||
await state.dataInChar.writeValueWithoutResponse(packet);
|
await waitForAck(cfg.packetCmd, 0, BEGIN_READY_TIMEOUT_MS, {
|
||||||
|
expectedTotalSize: totalSize,
|
||||||
|
payloadSize: payloadSize
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('等待设备 begin/预擦完成超时: ' + e.message);
|
||||||
|
}
|
||||||
|
if (state.lastAckBytes < firstBytes) {
|
||||||
|
throw new Error('设备 begin 就绪 ACK 字节不足: got=' + state.lastAckBytes + ' need=' + firstBytes);
|
||||||
|
}
|
||||||
|
bleLog('OTA', '设备 begin 就绪 (ackBytes=' + state.lastAckBytes + '),继续发送窗口数据', 'I');
|
||||||
|
setBleStageText('正在写入 ' + packetTotal + ' 包数据...');
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await state.dataInChar.writeValueWithoutResponse(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.ackGeneration++;
|
state.ackGeneration++;
|
||||||
try {
|
try {
|
||||||
await waitForAck(cfg.packetCmd, endSeq, 5000, { expectedTotalSize: totalSize, payloadSize });
|
await waitForAck(cfg.packetCmd, endSeq, WINDOW_ACK_TIMEOUT_MS, { expectedTotalSize: totalSize, payloadSize });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bleLog('OTA', `ACK 等待失败: ${e.message},进入重试`, 'W');
|
bleLog('OTA', `ACK 等待失败: ${e.message},进入重试`, 'W');
|
||||||
}
|
}
|
||||||
@@ -270,15 +286,23 @@ export async function uploadFirmware() {
|
|||||||
const end = Math.min(start + payloadSize, totalSize);
|
const end = Math.min(start + payloadSize, totalSize);
|
||||||
const payload = fileBytes.slice(start, end);
|
const payload = fileBytes.slice(start, end);
|
||||||
const packet = buildPacket(seq, totalSize, payload, cfg.packetCmd);
|
const packet = buildPacket(seq, totalSize, payload, cfg.packetCmd);
|
||||||
// ★ 重试路径中 seq=0 也必须用 writeValueWithResponse
|
// ★ 重试路径中 seq=0 也必须用 writeValueWithResponse,并等 begin 就绪 ACK
|
||||||
if (seq === 0) {
|
if (seq === 0) {
|
||||||
try {
|
try {
|
||||||
await state.dataInChar.writeValueWithResponse(packet);
|
await state.dataInChar.writeValueWithResponse(packet);
|
||||||
bleLog('OTA', '重试 seq=0 已用 writeValueWithResponse 确认', 'I');
|
bleLog('OTA', '重试 seq=0 已送达,等待 begin 就绪 ACK…', 'I');
|
||||||
|
state.ackGeneration++;
|
||||||
|
try {
|
||||||
|
await waitForAck(cfg.packetCmd, 0, BEGIN_READY_TIMEOUT_MS, {
|
||||||
|
expectedTotalSize: totalSize,
|
||||||
|
payloadSize: payloadSize
|
||||||
|
});
|
||||||
|
} catch (e2) {
|
||||||
|
bleLog('OTA', '重试 seq=0 等待 begin ACK 失败: ' + e2.message, 'W');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bleLog('OTA', `重试 seq=0 写入失败: ${e.message}`, 'E');
|
bleLog('OTA', `重试 seq=0 写入失败: ${e.message}`, 'E');
|
||||||
}
|
}
|
||||||
await sleep(200);
|
|
||||||
} else {
|
} else {
|
||||||
await state.dataInChar.writeValueWithoutResponse(packet);
|
await state.dataInChar.writeValueWithoutResponse(packet);
|
||||||
}
|
}
|
||||||
@@ -286,7 +310,7 @@ export async function uploadFirmware() {
|
|||||||
|
|
||||||
state.ackGeneration++;
|
state.ackGeneration++;
|
||||||
try {
|
try {
|
||||||
await waitForAck(cfg.packetCmd, endSeq, 3000, { expectedTotalSize: totalSize, payloadSize });
|
await waitForAck(cfg.packetCmd, endSeq, WINDOW_ACK_TIMEOUT_MS, { expectedTotalSize: totalSize, payloadSize });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bleLog('OTA', `重试 ACK 等待失败: ${e.message}`, 'W');
|
bleLog('OTA', `重试 ACK 等待失败: ${e.message}`, 'W');
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* can.js - CAN 工具页面入口(ESM 组装层)
|
* can.js - CAN 工具页面入口(ESM 组装层)
|
||||||
*
|
*
|
||||||
* 本文件对应原 tools/can.js 中未拆分到子模块的编排逻辑:
|
* CAN 页面的编排逻辑:
|
||||||
* - cacheElements:缓存 DOM 元素到 el(dbc.js 导出的共享 el 对象)
|
* - cacheElements:缓存 DOM 元素到 el(dbc.js 导出的共享 el 对象)
|
||||||
* - setConnectionState:CAN 页面专用连接状态更新(canConnPill 等元素)
|
* - setConnectionState:CAN 页面专用连接状态更新(canConnPill 等元素)
|
||||||
* - testConnection:CAN 页面专用连接检测(GET /system/status,操作 el.connectionNote 等)
|
* - testConnection:CAN 页面专用连接检测(GET /system/status,操作 el.connectionNote 等)
|
||||||
|
|||||||
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* dbc.js - DBC 解析器 + 共享状态/元素缓存/基础工具
|
* dbc.js - DBC 解析器 + 共享状态/元素缓存/基础工具
|
||||||
*
|
*
|
||||||
* 从 tools/can.js 拆分。对应原 can.js 约 4~21 行(state/el)、78~104 行(基础工具)、
|
* 包含:共享 state/el、基础工具、DBC 解析、cssEscape、readDbcText。
|
||||||
* 178~335 行(DBC 解析)、830~833 行(cssEscape)、1335~1352 行(readDbcText)。
|
|
||||||
*
|
*
|
||||||
* 本模块是 can 子系统的基础层,不依赖其他 can 子模块;state/el 在此导出,
|
* 本模块是 can 子系统的基础层,不依赖其他 can 子模块;state/el 在此导出,
|
||||||
* 供 messages/signals/send/feedback 共享(ES Module 的 live binding 保证引用一致)。
|
* 供 messages/signals/send/feedback 共享(ES Module 的 live binding 保证引用一致)。
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* feedback.js - 1Hz 反馈轮询(预留模块)
|
* feedback.js - 1Hz 反馈轮询(预留模块)
|
||||||
*
|
*
|
||||||
* ⚠️ 说明:原 tools/can.js(1526 行)中不存在 1Hz 反馈轮询 / ID 快照表渲染相关代码。
|
* 重新导出 common/connection.js 的连接管理函数,供 can 入口文件按需使用。
|
||||||
* 这些功能实际位于 tools/can_monitor.js(CAN 监控页面),操作的是 can_monitor.html
|
* 1Hz 轮询 /can/feedback 或 ID 快照功能可在本模块按需实现。
|
||||||
* 的 DOM 元素(fb0State / fb0Rx / idsnapBody 等),与 can.html 页面的元素体系不同,
|
|
||||||
* 因此无法直接迁移。
|
|
||||||
*
|
|
||||||
* can.js 中与"连接状态反馈"相关的代码为 setConnectionState / testConnection
|
|
||||||
* (一次性 GET /system/status),属于入口文件(js/can.js)的 init/cacheElements/bindEvents
|
|
||||||
* 编排范畴,不在本子模块内。
|
|
||||||
*
|
|
||||||
* 本模块按任务要求 import common/connection.js,供入口文件在需要时引入连接管理能力。
|
|
||||||
* 若后续 can 页面需要增加 1Hz 轮询 /can/feedback 或 ID 快照功能,可在此模块实现。
|
|
||||||
*/
|
*/
|
||||||
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi, testConnection, refreshSystemStatusNow } from '../common/connection.js';
|
import { updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi, testConnection, refreshSystemStatusNow } from '../common/connection.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* messages.js - 报文列表管理(渲染、过滤、节点过滤、批量 UI)
|
* messages.js - 报文列表管理(渲染、过滤、节点过滤、批量 UI)
|
||||||
*
|
*
|
||||||
* 从 tools/can.js 拆分。对应原 can.js 约 337~686 行(DBC 文件信息、报文过滤、
|
* 包含:DBC 文件信息、报文过滤、节点过滤面板、报文列表渲染、报文选择/展开、批量 UI。
|
||||||
* 节点过滤面板、报文列表渲染、报文选择/展开、批量 UI)。
|
|
||||||
*
|
*
|
||||||
* 依赖:dbc.js(state/el/padHex/normalizeWhitespace/cssEscape)、
|
* 依赖:dbc.js(state/el/padHex/normalizeWhitespace/cssEscape)、
|
||||||
* common/utils.js(escapeHtml/formatBytes)、
|
* common/utils.js(escapeHtml/formatBytes)、
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* send.js - CAN 帧构建与发送(DBC 报文 / 原始帧 / 批量发送 / 发送历史)
|
* send.js - CAN 帧构建与发送(DBC 报文 / 原始帧 / 批量发送 / 发送历史)
|
||||||
*
|
*
|
||||||
* 从 tools/can.js 拆分。对应原 can.js 约 1030~1333 行(sendCanRequest、响应框、
|
* 包含:sendCanRequest、响应框、历史、批量/选中/手动发送、
|
||||||
* 历史、批量/选中/手动发送、手动帧校验/重置/载入预览)及 1491~1523 行
|
* 手动帧校验/重置/载入预览、CAN_SEND_ERR_DESC / GD32_ERR_DESC / buildSendResponseText。
|
||||||
* (CAN_SEND_ERR_DESC / GD32_ERR_DESC / buildSendResponseText)。
|
|
||||||
*
|
*
|
||||||
* 依赖:dbc.js(state/el/getBaseUrl/padHex/bytesToHex/parseNumericId)、
|
* 依赖:dbc.js(state/el/getBaseUrl/padHex/bytesToHex/parseNumericId)、
|
||||||
* common/toast.js(showToast)、common/utils.js(escapeHtml)、
|
* common/toast.js(showToast)、common/utils.js(escapeHtml)、
|
||||||
* messages.js(updateBatchUi)、signals.js(ensureMessageDefaults/getSignalValueMap/
|
* messages.js(updateBatchUi)、signals.js(ensureMessageDefaults/getSignalValueMap/
|
||||||
* getSelectedMessage/updateEncodedPreview/encodeMessageData)。
|
* getSelectedMessage/updateEncodedPreview/encodeMessageData)。
|
||||||
*
|
*
|
||||||
* 注意:formatTime 保留 can.js 本地实现(HH:MM:SS,不含毫秒),与 common/utils.js
|
* 注意:formatTime 保留本地实现(HH:MM:SS,不含毫秒),与 common/utils.js
|
||||||
* 的 formatTime(含毫秒)不同,以保证发送历史时间戳输出与原版完全一致。
|
* 的 formatTime(含毫秒)不同,以保证发送历史时间戳输出格式一致。
|
||||||
*/
|
*/
|
||||||
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js';
|
import { state, el, getBaseUrl, padHex, bytesToHex, parseNumericId } from './dbc.js';
|
||||||
import { escapeHtml as sanitizeHtml } from '../common/utils.js';
|
import { escapeHtml as sanitizeHtml } from '../common/utils.js';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* signals.js - 信号编辑器渲染 + 信号编解码(raw ↔ physical)
|
* signals.js - 信号编辑器渲染 + 信号编解码(raw ↔ physical)
|
||||||
*
|
*
|
||||||
* 从 tools/can.js 拆分。对应原 can.js 约 629~1028 行(信号行渲染、信号值存取、
|
* 包含:信号行渲染、信号值存取、编码写入、浮点/整数/Motorola/Intel 位操作、
|
||||||
* 编码写入、浮点/整数/Motorola/Intel 位操作)及 761 行的 renderSelectedMessage shim。
|
* renderSelectedMessage shim。
|
||||||
*
|
*
|
||||||
* 依赖:dbc.js(state/el/padHex/bytesToHex/cssEscape/findSignal)、common/utils.js(escapeHtml)。
|
* 依赖:dbc.js(state/el/padHex/bytesToHex/cssEscape/findSignal)、common/utils.js(escapeHtml)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* connection.js - 连接管理 + 系统状态显示
|
* connection.js - 连接管理 + 系统状态显示
|
||||||
*
|
*
|
||||||
* 从 app.js 的「连接管理」段 + testConnection / refreshSystemStatusNow 提取。
|
|
||||||
* 操作 DOM 元素 #pillConn / #pillConnTxt / #connStatus / #connStatusTxt / #bConn 等。
|
* 操作 DOM 元素 #pillConn / #pillConnTxt / #connStatus / #connStatusTxt / #bConn 等。
|
||||||
*
|
*
|
||||||
* 共享状态(通过 window.* 引用,后续 app.js 拆分时会统一挂载):
|
* 共享状态(通过 window.* 引用):
|
||||||
* window.latestSystemStatus — 最近一次 /system/status 响应
|
* window.latestSystemStatus — 最近一次 /system/status 响应
|
||||||
* window.lastSysStateCode — 上次成功的 sys_state_code(polling.js 读取)
|
* window.lastSysStateCode — 上次成功的 sys_state_code(polling.js 读取)
|
||||||
* window.lastRelayState — 上次成功的 relay_state(polling.js 读取)
|
* window.lastRelayState — 上次成功的 relay_state(polling.js 读取)
|
||||||
* window.consecutivePollFailures — 连续轮询失败计数(polling.js 读取)
|
* window.consecutivePollFailures — 连续轮询失败计数(polling.js 读取)
|
||||||
*
|
*
|
||||||
* 未拆分的 app.js 函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.getBaseUrl / window.updateSystemStatusUi / window.diagnoseNetwork / window.formatPercent
|
* window.getBaseUrl / window.updateSystemStatusUi / window.formatPercent
|
||||||
*/
|
*/
|
||||||
import { formatBytes, formatTime, formatUptime, escapeHtml } from './utils.js';
|
import { formatBytes, formatTime, formatUptime, escapeHtml } from './utils.js';
|
||||||
import { addLog } from './log.js';
|
import { addLog } from './log.js';
|
||||||
@@ -51,15 +50,18 @@ export function updateHealthUi(healthCode, freeHeap) {
|
|||||||
mh.className = 'v ' + (healthCode === 0 ? 'ok' : healthCode === 1 ? 'warn' : 'err');
|
mh.className = 'v ' + (healthCode === 0 ? 'ok' : healthCode === 1 ? 'warn' : 'err');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 每次 pollStatus 成功后刷新堆/栈/CPU(固定周期约 1000ms,中继期 400ms;
|
||||||
|
* sample_ms 由设备两次采样墙钟差给出;WiFi 上传 ESP32 期间 pause 不刷)。
|
||||||
|
*/
|
||||||
export function updateHeapAndTaskStatusUi(data) {
|
export function updateHeapAndTaskStatusUi(data) {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
var ota = data.ota || {};
|
var ota = data.ota || {};
|
||||||
var wifi = data.wifi || {};
|
|
||||||
var targetHost = window.getBaseUrl().replace(/^http:\/\//, '');
|
|
||||||
var setText = function (id, value) {
|
var setText = function (id, value) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
if (el) el.textContent = value;
|
if (el) el.textContent = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 堆概况:已用 / 总计 + 占用条 ----
|
// ---- 堆概况:已用 / 总计 + 占用条 ----
|
||||||
function setFill(id, pct) {
|
function setFill(id, pct) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
@@ -85,10 +87,10 @@ export function updateHeapAndTaskStatusUi(data) {
|
|||||||
el.style.width = safePct + '%';
|
el.style.width = safePct + '%';
|
||||||
el.className = 'heap-bar-fill cpu-fill' + (safePct >= 85 ? ' danger' : safePct >= 70 ? ' warn' : '');
|
el.className = 'heap-bar-fill cpu-fill' + (safePct >= 85 ? ' danger' : safePct >= 70 ? ' warn' : '');
|
||||||
}
|
}
|
||||||
function updateCpuUsageUi(cpu) {
|
function updateCpuUsageUi(cpuData) {
|
||||||
cpu = cpu || {};
|
cpuData = cpuData || {};
|
||||||
var taskList = document.getElementById('cpuTaskList');
|
var taskList = document.getElementById('cpuTaskList');
|
||||||
if (!cpu.available) {
|
if (!cpuData.available) {
|
||||||
setText('cpuUsageText', '未启用 FreeRTOS 统计');
|
setText('cpuUsageText', '未启用 FreeRTOS 统计');
|
||||||
setText('cpuUsagePct', '--');
|
setText('cpuUsagePct', '--');
|
||||||
setText('cpuIdlePct', '--');
|
setText('cpuIdlePct', '--');
|
||||||
@@ -98,7 +100,7 @@ export function updateHeapAndTaskStatusUi(data) {
|
|||||||
if (taskList) taskList.innerHTML = '<div class="status-empty">固件未启用 CPU runtime stats</div>';
|
if (taskList) taskList.innerHTML = '<div class="status-empty">固件未启用 CPU runtime stats</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!cpu.valid) {
|
if (!cpuData.valid) {
|
||||||
setText('cpuUsageText', '等待下一次采样...');
|
setText('cpuUsageText', '等待下一次采样...');
|
||||||
setText('cpuUsagePct', '--');
|
setText('cpuUsagePct', '--');
|
||||||
setText('cpuIdlePct', '--');
|
setText('cpuIdlePct', '--');
|
||||||
@@ -108,10 +110,10 @@ export function updateHeapAndTaskStatusUi(data) {
|
|||||||
if (taskList) taskList.innerHTML = '<div class="status-empty">首次刷新只建立基线,下一次刷新后显示 CPU 占用</div>';
|
if (taskList) taskList.innerHTML = '<div class="status-empty">首次刷新只建立基线,下一次刷新后显示 CPU 占用</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var usagePct = Number(cpu.usage_pct) || 0;
|
var usagePct = Number(cpuData.usage_pct) || 0;
|
||||||
var idlePct = Number(cpu.idle_pct) || 0;
|
var idlePct = Number(cpuData.idle_pct) || 0;
|
||||||
var sampleMs = Number(cpu.sample_ms) || 0;
|
var sampleMs = Number(cpuData.sample_ms) || 0;
|
||||||
var tasks = Array.isArray(cpu.tasks) ? cpu.tasks.slice() : [];
|
var tasks = Array.isArray(cpuData.tasks) ? cpuData.tasks.slice() : [];
|
||||||
tasks.sort(function (a, b) {
|
tasks.sort(function (a, b) {
|
||||||
return (Number(b.usage_pct) || 0) - (Number(a.usage_pct) || 0);
|
return (Number(b.usage_pct) || 0) - (Number(a.usage_pct) || 0);
|
||||||
});
|
});
|
||||||
@@ -159,7 +161,6 @@ export function updateHeapAndTaskStatusUi(data) {
|
|||||||
|
|
||||||
setText('sOtaState', (ota.sys_state || '--') + (Number.isFinite(ota.sys_state_code) ? ' (' + ota.sys_state_code + ')' : ''));
|
setText('sOtaState', (ota.sys_state || '--') + (Number.isFinite(ota.sys_state_code) ? ' (' + ota.sys_state_code + ')' : ''));
|
||||||
setText('sRelayState', Number.isFinite(ota.relay_state) ? String(ota.relay_state) : '--');
|
setText('sRelayState', Number.isFinite(ota.relay_state) ? String(ota.relay_state) : '--');
|
||||||
// 缓存最近一次成功的 sys_state_code / relay_state,供 pollStatus 失败时判断设备是否在忙
|
|
||||||
if (Number.isFinite(ota.sys_state_code)) window.lastSysStateCode = ota.sys_state_code;
|
if (Number.isFinite(ota.sys_state_code)) window.lastSysStateCode = ota.sys_state_code;
|
||||||
if (Number.isFinite(ota.relay_state)) window.lastRelayState = ota.relay_state;
|
if (Number.isFinite(ota.relay_state)) window.lastRelayState = ota.relay_state;
|
||||||
var storedText = '--';
|
var storedText = '--';
|
||||||
@@ -205,6 +206,45 @@ export function updateHeapAndTaskStatusUi(data) {
|
|||||||
statusJson.textContent = JSON.stringify(data, null, 2);
|
statusJson.textContent = JSON.stringify(data, null, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** 从设备 IP 推导网关 IP(取前三段 + .1) */
|
||||||
|
function getGatewayIp() {
|
||||||
|
var parts = (window.getEspIp ? window.getEspIp() : '192.168.31.127').split('.');
|
||||||
|
if (parts.length === 4) return parts[0] + '.' + parts[1] + '.' + parts[2] + '.1';
|
||||||
|
return '192.168.31.1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络诊断:区分浏览器离线 / 网关不通 / 仅设备不通。
|
||||||
|
* 放在 connection.js,避免与 polling.js 形成循环 import。
|
||||||
|
*/
|
||||||
|
export async function diagnoseNetwork() {
|
||||||
|
var result = [];
|
||||||
|
if (navigator.onLine === false) {
|
||||||
|
result.push('浏览器无网络连接');
|
||||||
|
return result.join(';');
|
||||||
|
}
|
||||||
|
var gwIp = getGatewayIp();
|
||||||
|
var gwOk = false;
|
||||||
|
try {
|
||||||
|
await fetch('http://' + gwIp + '/', { mode: 'no-cors', signal: AbortSignal.timeout(2000) });
|
||||||
|
gwOk = true;
|
||||||
|
} catch (_) {}
|
||||||
|
if (!gwOk) {
|
||||||
|
try {
|
||||||
|
await fetch('http://' + gwIp + '/favicon.ico', { mode: 'no-cors', signal: AbortSignal.timeout(1500) });
|
||||||
|
gwOk = true;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
if (!gwOk) {
|
||||||
|
result.push('网关 ' + gwIp + ' 不可达(本地网络不通)');
|
||||||
|
} else {
|
||||||
|
result.push('网关 ' + gwIp + ' 可达');
|
||||||
|
var espIp = window.getEspIp ? window.getEspIp() : '设备';
|
||||||
|
result.push('设备 ' + espIp + ' 不响应(可能断电、重启中或 WiFi 断开)');
|
||||||
|
}
|
||||||
|
return result.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
/** @description 测试与 ESP32 的连接(GET /status)。 */
|
||||||
export async function testConnection() {
|
export async function testConnection() {
|
||||||
// 立即发一次请求检测
|
// 立即发一次请求检测
|
||||||
@@ -232,7 +272,7 @@ export async function testConnection() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
addLog('E', '连接', '连接失败: ' + e.message);
|
addLog('E', '连接', '连接失败: ' + e.message);
|
||||||
// 连接失败时自动诊断
|
// 连接失败时自动诊断
|
||||||
var diag = await window.diagnoseNetwork();
|
var diag = await diagnoseNetwork();
|
||||||
addLog('W', '诊断', diag);
|
addLog('W', '诊断', diag);
|
||||||
showToast('无法连接: ' + diag, 'error');
|
showToast('无法连接: ' + diag, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* log.js - 日志系统(带去重 + 自动滚动)
|
* log.js - 日志系统(带去重 + 自动滚动)
|
||||||
*
|
*
|
||||||
* 从 app.js 提取的日志环形缓冲、去重、过滤、自动滚动逻辑。
|
* 日志环形缓冲、去重、过滤、自动滚动逻辑。
|
||||||
* 操作 DOM 元素 #logScroll / #logCount / #autoScrollBtn / .log-filter。
|
* 操作 DOM 元素 #logScroll / #logCount / #autoScrollBtn / .log-filter。
|
||||||
*/
|
*/
|
||||||
import { formatTime } from './utils.js';
|
import { formatTime } from './utils.js';
|
||||||
|
|||||||
+41
-57
@@ -1,14 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* polling.js - 轮询状态 + Page Visibility 补偿 + ESP32 实时日志拉取
|
* polling.js - 轮询状态 + Page Visibility 补偿 + ESP32 实时日志拉取
|
||||||
*
|
*
|
||||||
* 从 app.js 的「轮询状态」「Page Visibility 补偿」「ESP32 实时日志拉取 (增量)」段提取。
|
|
||||||
* 递归 setTimeout 调度 /system/status + setInterval 调度 /logs,避免 setInterval 慢响应被 abort。
|
* 递归 setTimeout 调度 /system/status + setInterval 调度 /logs,避免 setInterval 慢响应被 abort。
|
||||||
*
|
*
|
||||||
* 模块级状态(仅本模块使用,不导出):
|
* 模块级状态(仅本模块使用,不导出):
|
||||||
* statusTimerId / logsTimerId / statusAbortCtrl / statusPollInFlight / statusPollIntervalMs
|
* statusTimerId / logsTimerId / statusAbortCtrl / statusPollInFlight / statusPollIntervalMs
|
||||||
* lastDiagTime / diagResult / uploadPollChip / lastLogTimestamp / pageVisible
|
* uploadPollChip / lastLogTimestamp / pageVisible
|
||||||
*
|
*
|
||||||
* 共享状态(通过 window.* 引用,后续 app.js 拆分时会统一挂载):
|
* 共享状态(通过 window.* 引用):
|
||||||
* window.pollingPausedForUpload — 上传期间暂停轮询标志
|
* window.pollingPausedForUpload — 上传期间暂停轮询标志
|
||||||
* window.consecutivePollFailures — 连续轮询失败计数(connection.js 也写入)
|
* window.consecutivePollFailures — 连续轮询失败计数(connection.js 也写入)
|
||||||
* window.lastSysStateCode — 上次成功的 sys_state_code(connection.js 写入)
|
* window.lastSysStateCode — 上次成功的 sys_state_code(connection.js 写入)
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
* window.activeUploadChip — 当前上传目标芯片
|
* window.activeUploadChip — 当前上传目标芯片
|
||||||
* window.currentOtaTarget — 当前 OTA 目标
|
* window.currentOtaTarget — 当前 OTA 目标
|
||||||
*
|
*
|
||||||
* 未拆分的 app.js 函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.getEspIp / window.getBaseUrl
|
* window.getEspIp / window.getBaseUrl
|
||||||
* window.updateSystemStatusUi / window.updateEsp32StagesFromPhase
|
* window.updateSystemStatusUi / window.updateEsp32StagesFromPhase
|
||||||
* window.applyRebootingRuntimeState / window.applyDisconnectedRuntimeState / window.applyUploadingRuntimeState
|
* window.applyRebootingRuntimeState / window.applyDisconnectedRuntimeState / window.applyUploadingRuntimeState
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
*/
|
*/
|
||||||
import { addLog } from './log.js';
|
import { addLog } from './log.js';
|
||||||
import { showToast } from './toast.js';
|
import { showToast } from './toast.js';
|
||||||
import { updateConnectionUi } from './connection.js';
|
import { updateConnectionUi, diagnoseNetwork } from './connection.js';
|
||||||
|
|
||||||
// ==================== 轮询状态 ====================
|
// ==================== 轮询状态 ====================
|
||||||
// 共享状态(初始化到 window,供 connection.js 等其他模块读写)
|
// 共享状态(初始化到 window,供 connection.js 等其他模块读写)
|
||||||
@@ -41,66 +40,32 @@ window.lastRelayState = 0; // 上次成功的 relay_state
|
|||||||
// 模块级状态(仅本模块使用)
|
// 模块级状态(仅本模块使用)
|
||||||
var statusTimerId = null;
|
var statusTimerId = null;
|
||||||
var logsTimerId = null;
|
var logsTimerId = null;
|
||||||
var lastDiagTime = 0; // 上次诊断时间
|
|
||||||
var diagResult = ''; // 最近一次诊断结果
|
|
||||||
var statusAbortCtrl = null; // 当前 /system/status 请求的 AbortController(3s 超时用)
|
var statusAbortCtrl = null; // 当前 /system/status 请求的 AbortController(3s 超时用)
|
||||||
var statusPollIntervalMs = 1000; // 状态轮询目标间隔
|
var statusPollIntervalMs = 1000; // 状态轮询目标间隔(中继期可由 setStatusPollIntervalMs 降到 400ms)
|
||||||
var statusPollInFlight = false; // 是否正在请求 /system/status,防止重叠
|
var statusPollInFlight = false; // 是否正在请求 /system/status,防止重叠
|
||||||
var pageVisible = !document.hidden; // 页面可见性:切到后台/最小化/切标签会变 false
|
var pageVisible = !document.hidden; // 页面可见性:切到后台/最小化/切标签会变 false
|
||||||
var uploadPollChip = null; // 当前暂停轮询的上传目标,用于区分 WiFi/日志策略
|
var uploadPollChip = null; // 当前暂停轮询的上传目标,用于区分 WiFi/日志策略
|
||||||
|
|
||||||
// 从设备 IP 推导网关 IP(取前三段 + .1)
|
/**
|
||||||
function getGatewayIp() {
|
* 调整 /system/status 目标间隔。GD32 中继期用 400ms 缩小「转发 N/M」相对设备日志的滞后;
|
||||||
var parts = window.getEspIp().split('.');
|
* 结束后恢复 1000ms,避免常态堆/栈卡片过刷。
|
||||||
if (parts.length === 4) return parts[0] + '.' + parts[1] + '.' + parts[2] + '.1';
|
*/
|
||||||
return '192.168.31.1';
|
export function setStatusPollIntervalMs(ms) {
|
||||||
}
|
var n = Number(ms);
|
||||||
// 网络诊断:区分浏览器离线 / 网关不通 / 仅设备不通
|
if (!Number.isFinite(n) || n < 200) n = 1000;
|
||||||
async function diagnoseNetwork() {
|
if (n > 5000) n = 5000;
|
||||||
lastDiagTime = Date.now();
|
statusPollIntervalMs = n;
|
||||||
var result = [];
|
|
||||||
// 1) 浏览器是否在线
|
|
||||||
if (navigator.onLine === false) {
|
|
||||||
result.push('浏览器无网络连接');
|
|
||||||
diagResult = result.join(';');
|
|
||||||
return diagResult;
|
|
||||||
}
|
|
||||||
// 2) 尝试触达网关(超时 2s)
|
|
||||||
var gwIp = getGatewayIp();
|
|
||||||
var gwOk = false;
|
|
||||||
try {
|
|
||||||
await fetch('http://' + gwIp + '/', { mode: 'no-cors', signal: AbortSignal.timeout(2000) });
|
|
||||||
gwOk = true; // no-cors 模式下不抛异常即视为可达
|
|
||||||
} catch (_) {}
|
|
||||||
if (!gwOk) {
|
|
||||||
// 再试一次 favicon(有些路由器不响应 /)
|
|
||||||
try {
|
|
||||||
await fetch('http://' + gwIp + '/favicon.ico', { mode: 'no-cors', signal: AbortSignal.timeout(1500) });
|
|
||||||
gwOk = true;
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
if (!gwOk) {
|
|
||||||
result.push('网关 ' + gwIp + ' 不可达(本地网络不通)');
|
|
||||||
} else {
|
|
||||||
result.push('网关 ' + gwIp + ' 可达');
|
|
||||||
// 3) 网关通但设备不通 → 设备自身问题
|
|
||||||
result.push('设备 ' + window.getEspIp() + ' 不响应(可能断电、重启中或 WiFi 断开)');
|
|
||||||
}
|
|
||||||
diagResult = result.join(';');
|
|
||||||
return diagResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @description 调度下一次 /system/status 轮询(递归 setTimeout)。
|
/** @description 调度下一次 /system/status 轮询(递归 setTimeout)。
|
||||||
* 上一次请求完成(或 3s 超时)后才调度下一次,避免 setInterval 把慢响应
|
* 默认 delay = statusPollIntervalMs;pollStatus 完成后会按「固定周期」
|
||||||
* 直接 abort 而导致堆/任务栈/CPU 等卡片刷新率不稳。
|
* 传入 max(0, interval - 本轮耗时),使 CPU 采样窗口 sample_ms ≈ interval,
|
||||||
|
* 而不是「完成后 + interval」(会稳定偏到 1100+ ms)。
|
||||||
|
* 仍保证 in-flight 不重叠,避免 setInterval 慢响应被 abort。
|
||||||
*/
|
*/
|
||||||
export function scheduleStatusPoll(delayMs) {
|
export function scheduleStatusPoll(delayMs) {
|
||||||
if (statusTimerId) clearTimeout(statusTimerId);
|
if (statusTimerId) clearTimeout(statusTimerId);
|
||||||
// 中继期加快:进度条依赖 status.relay_seq,1s 间隔体感像卡死
|
|
||||||
var interval = statusPollIntervalMs;
|
var interval = statusPollIntervalMs;
|
||||||
// 防御性守卫:window.OtaSessionState 可能尚未挂载(模块加载早期),此时不加速
|
|
||||||
if (window.OtaSessionState && window.otaSessionState === window.OtaSessionState.RELAYING) {
|
|
||||||
interval = Math.min(interval, 400);
|
|
||||||
}
|
|
||||||
statusTimerId = setTimeout(function() {
|
statusTimerId = setTimeout(function() {
|
||||||
statusTimerId = null;
|
statusTimerId = null;
|
||||||
pollStatus();
|
pollStatus();
|
||||||
@@ -161,8 +126,9 @@ export async function pollStatus() {
|
|||||||
// 递归 setTimeout 调度,正常情况下不会重叠;做个保险避免并发请求
|
// 递归 setTimeout 调度,正常情况下不会重叠;做个保险避免并发请求
|
||||||
if (statusPollInFlight) return;
|
if (statusPollInFlight) return;
|
||||||
statusPollInFlight = true;
|
statusPollInFlight = true;
|
||||||
|
var pollStartedAt = Date.now();
|
||||||
statusAbortCtrl = new AbortController();
|
statusAbortCtrl = new AbortController();
|
||||||
var pollTimer = setTimeout(function() { try { statusAbortCtrl.abort(); } catch(_e) {} }, 3000);
|
var pollTimer = setTimeout(function() { try { statusAbortCtrl.abort(); } catch(_e) {} }, 5000);
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(window.getBaseUrl() + '/system/status', {
|
var resp = await fetch(window.getBaseUrl() + '/system/status', {
|
||||||
signal: statusAbortCtrl.signal
|
signal: statusAbortCtrl.signal
|
||||||
@@ -287,7 +253,13 @@ export async function pollStatus() {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
statusPollInFlight = false;
|
statusPollInFlight = false;
|
||||||
scheduleStatusPoll();
|
statusAbortCtrl = null;
|
||||||
|
/* 固定周期调度:interval 从本轮开始计时扣减请求耗时。
|
||||||
|
* 旧逻辑「完成后 + 1000ms」→ 窗口稳定 1100+;现 sample_ms 贴近 1000/400。
|
||||||
|
* 本轮超时/很慢时 delay=0,立即补下一轮,仍不重叠。 */
|
||||||
|
var elapsed = Date.now() - pollStartedAt;
|
||||||
|
var delay = Math.max(0, statusPollIntervalMs - elapsed);
|
||||||
|
scheduleStatusPoll(delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 注意:startPoll() 由入口模块(app.js / ble_ota.js)在所有模块加载完成后调用,
|
// 注意:startPoll() 由入口模块(app.js / ble_ota.js)在所有模块加载完成后调用,
|
||||||
@@ -316,7 +288,7 @@ export async function pollLogs() {
|
|||||||
// ★ ESP32 上传期间也允许日志轮询
|
// ★ ESP32 上传期间也允许日志轮询
|
||||||
try {
|
try {
|
||||||
var resp = await fetch(window.getBaseUrl() + '/logs', {
|
var resp = await fetch(window.getBaseUrl() + '/logs', {
|
||||||
signal: AbortSignal.timeout(2000)
|
signal: AbortSignal.timeout(5000)
|
||||||
});
|
});
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
var logs = await resp.json();
|
var logs = await resp.json();
|
||||||
@@ -338,6 +310,18 @@ export async function pollLogs() {
|
|||||||
showToast('ESP32 已恢复在线', 'success');
|
showToast('ESP32 已恢复在线', 'success');
|
||||||
if (window.otaSessionState === window.OtaSessionState.REBOOTING) window.otaSessionTransition(window.OtaSessionState.DONE);
|
if (window.otaSessionState === window.OtaSessionState.REBOOTING) window.otaSessionTransition(window.OtaSessionState.DONE);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* 设备已报「全部数据包已转发」时,立刻补 UI 100% 进度。
|
||||||
|
* status 轮询可能仍停在旧 relay_seq,否则会出现 FINISH 日志之后还刷 94%。
|
||||||
|
*/
|
||||||
|
if (m[2] === 'OTA' && /全部数据包已转发/.test(m[3]) &&
|
||||||
|
typeof window.emitRelayProgress === 'function') {
|
||||||
|
var otaSnap = (window.latestSystemStatus && window.latestSystemStatus.ota) || {};
|
||||||
|
var tot = Number(otaSnap.relay_total_packets) || 0;
|
||||||
|
if (tot > 0) {
|
||||||
|
window.emitRelayProgress(tot, tot, { force100: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
addLog('I', 'ESP32', line.trimEnd());
|
addLog('I', 'ESP32', line.trimEnd());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* toast.js - Toast 提示组件
|
* toast.js - Toast 提示组件
|
||||||
*
|
*
|
||||||
* 从 app.js 提取的 showToast 函数,操作 DOM 元素 #toast / #toastIco / #toastMsg。
|
* showToast 函数,操作 DOM 元素 #toast / #toastIco / #toastMsg。
|
||||||
* 支持 info / success / warning / error 类型,内置 5s 去重防抖。
|
* 支持 info / success / warning / error 类型,内置 5s 去重防抖。
|
||||||
*/
|
*/
|
||||||
/** @description 显示 Toast 提示消息,支持 info/success/warning/error 类型。 */
|
/** @description 显示 Toast 提示消息,支持 info/success/warning/error 类型。 */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* utils.js - 通用工具函数
|
* utils.js - 通用工具函数
|
||||||
*
|
*
|
||||||
* 从 app.js 提取的格式化 / 转义工具函数,供各子模块和 can.js / can_monitor.js 共享。
|
* 格式化 / 转义工具函数,供各子模块和 can.js / can_monitor.js 共享。
|
||||||
*/
|
*/
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
/** @description 格式化字节大小为人类可读字符串(B/KB/MB)。 */
|
/** @description 格式化字节大小为人类可读字符串(B/KB/MB)。 */
|
||||||
|
|||||||
+80
-23
@@ -1,14 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* file-handle.js - 文件处理 + Intel HEX → BIN 转换
|
* file-handle.js - 文件处理 + Intel HEX → BIN 转换
|
||||||
*
|
*
|
||||||
* 从 app.js 的「文件处理」「Intel HEX → BIN 转换」「文件处理」段提取。
|
|
||||||
* 包含:HEX 解析、文件选择、FSAPI、SHA-256 计算、文件信息 UI、清除。
|
* 包含:HEX 解析、文件选择、FSAPI、SHA-256 计算、文件信息 UI、清除。
|
||||||
*
|
*
|
||||||
* 共享状态(通过 window.* 引用):
|
* 共享状态(通过 window.* 引用):
|
||||||
* window.esp32File / window.gd32File / window.esp32FileHandle / window.gd32FileHandle
|
* window.esp32File / window.gd32File / window.esp32FileHandle / window.gd32FileHandle
|
||||||
* window._esp32FileSha256 / window._gd32FileSha256
|
* window._esp32FileSha256 / window._gd32FileSha256
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.updateUploadButtonLabel / window.showToast / window.addLog
|
* window.updateUploadButtonLabel / window.showToast / window.addLog
|
||||||
*/
|
*/
|
||||||
import { formatBytes } from '../common/utils.js';
|
import { formatBytes } from '../common/utils.js';
|
||||||
@@ -179,6 +178,73 @@ export async function convertHexFileToBin(file) {
|
|||||||
return { file: binFile, converted: true, baseAddr: result.baseAddr, endAddr: result.endAddr };
|
return { file: binFile, converted: true, baseAddr: result.baseAddr, endAddr: result.endAddr };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== SHA-256 缓存 ====================
|
||||||
|
// 选文件时就把 SHA-256 算好并缓存(同时记录 file meta),上传前
|
||||||
|
// 若 handle.getFile() 拿回的 fresh file meta 未变就直接复用,
|
||||||
|
// 避免每次点上传都重算 30–80ms 的 crypto.subtle.digest。
|
||||||
|
// 缓存 key:size + lastModified + name 三项均一致(lastModified 在
|
||||||
|
// 文件 rebuild 后必变,是浏览器给的最可靠 rebuild 信号)。
|
||||||
|
/**
|
||||||
|
* @description 计算 file 的 SHA-256 并写入缓存(sha + meta)。
|
||||||
|
* GD32 UI 无 SHA 显示行;ESP32 同步更新 #fileSha256Esp32。
|
||||||
|
* @param {File} file 目标文件
|
||||||
|
* @param {'esp32'|'gd32'} chip 通道
|
||||||
|
* @param {ArrayBuffer=} optArrayBuffer 已读的 arrayBuffer(避免重复读取)
|
||||||
|
* @returns {Promise<string|null>} hex 大写 SHA,失败返回 null
|
||||||
|
*/
|
||||||
|
function _computeAndCacheFileSha(file, chip, optArrayBuffer) {
|
||||||
|
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
||||||
|
var shaElem = document.getElementById('fileSha256' + suffix); // GD32 可能为 null
|
||||||
|
if (shaElem) shaElem.textContent = '计算中...';
|
||||||
|
var bufPromise = optArrayBuffer ? Promise.resolve(optArrayBuffer) : file.arrayBuffer();
|
||||||
|
return bufPromise.then(function(buf) {
|
||||||
|
return crypto.subtle.digest('SHA-256', buf);
|
||||||
|
}).then(function(hashBuffer) {
|
||||||
|
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('').toUpperCase();
|
||||||
|
if (shaElem) shaElem.textContent = hashHex;
|
||||||
|
var meta = { size: file.size, lastModified: file.lastModified, name: file.name };
|
||||||
|
if (chip === 'esp32') {
|
||||||
|
window._esp32FileSha256 = hashHex;
|
||||||
|
window._esp32FileShaMeta = meta;
|
||||||
|
} else {
|
||||||
|
window._gd32FileSha256 = hashHex;
|
||||||
|
window._gd32FileShaMeta = meta;
|
||||||
|
}
|
||||||
|
addLog('I', '文件', chip.toUpperCase() + ' 固件 SHA-256: ' + hashHex);
|
||||||
|
return hashHex;
|
||||||
|
}).catch(function(err) {
|
||||||
|
if (shaElem) shaElem.textContent = '计算失败: ' + err.message;
|
||||||
|
addLog('W', '文件', chip.toUpperCase() + ' SHA-256 计算失败: ' + err.message);
|
||||||
|
if (chip === 'esp32') { window._esp32FileSha256 = null; window._esp32FileShaMeta = null; }
|
||||||
|
else { window._gd32FileSha256 = null; window._gd32FileShaMeta = null; }
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 确保 file 的 SHA-256 已在缓存中:命中直接返回,不命中则重算并缓存。
|
||||||
|
* @param {File} file 目标文件
|
||||||
|
* @param {'esp32'|'gd32'} chip 通道
|
||||||
|
* @param {ArrayBuffer=} optArrayBuffer 命中时不用,未命中时传入可避免重复读文件
|
||||||
|
* @returns {Promise<string|null>} hex 大写 SHA
|
||||||
|
*/
|
||||||
|
export function ensureFileShaCached(file, chip, optArrayBuffer) {
|
||||||
|
var cachedMeta = chip === 'esp32' ? window._esp32FileShaMeta : window._gd32FileShaMeta;
|
||||||
|
var cachedSha = chip === 'esp32' ? window._esp32FileSha256 : window._gd32FileSha256;
|
||||||
|
if (cachedSha && cachedMeta &&
|
||||||
|
cachedMeta.size === file.size &&
|
||||||
|
cachedMeta.lastModified === file.lastModified &&
|
||||||
|
cachedMeta.name === file.name) {
|
||||||
|
// 命中:把 UI 上的 SHA 同步刷一下(幂等),避免其它代码误清
|
||||||
|
var shaElem = document.getElementById(chip === 'esp32' ? 'fileSha256Esp32' : 'fileSha256Gd32');
|
||||||
|
if (shaElem) shaElem.textContent = cachedSha;
|
||||||
|
addLog('I', '文件', chip.toUpperCase() + ' 复用已缓存 SHA-256(文件未变)');
|
||||||
|
return Promise.resolve(cachedSha);
|
||||||
|
}
|
||||||
|
return _computeAndCacheFileSha(file, chip, optArrayBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 文件处理 ====================
|
// ==================== 文件处理 ====================
|
||||||
/** @description 处理用户选择的固件文件(HEX→BIN 转换、SHA-256 计算、文件校验)。 */
|
/** @description 处理用户选择的固件文件(HEX→BIN 转换、SHA-256 计算、文件校验)。 */
|
||||||
export function processFile(file, chip) {
|
export function processFile(file, chip) {
|
||||||
@@ -196,6 +262,7 @@ export function processFile(file, chip) {
|
|||||||
document.getElementById('fileType' + suffix).textContent = 'Binary(?)';
|
document.getElementById('fileType' + suffix).textContent = 'Binary(?)';
|
||||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
document.getElementById('fileSize' + suffix).textContent = formatBytes(file.size);
|
||||||
window.updateUploadButtonLabel();
|
window.updateUploadButtonLabel();
|
||||||
|
ensureFileShaCached(file, 'gd32');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.gd32File = result.file;
|
window.gd32File = result.file;
|
||||||
@@ -208,6 +275,7 @@ export function processFile(file, chip) {
|
|||||||
document.getElementById('fileType' + suffix).textContent = 'Binary (HEX转换)';
|
document.getElementById('fileType' + suffix).textContent = 'Binary (HEX转换)';
|
||||||
document.getElementById('fileSize' + suffix).textContent = formatBytes(result.file.size);
|
document.getElementById('fileSize' + suffix).textContent = formatBytes(result.file.size);
|
||||||
window.updateUploadButtonLabel();
|
window.updateUploadButtonLabel();
|
||||||
|
ensureFileShaCached(result.file, 'gd32');
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
addLog('E', '文件', 'HEX 解析失败: ' + err.message);
|
addLog('E', '文件', 'HEX 解析失败: ' + err.message);
|
||||||
showToast('HEX 文件解析失败: ' + err.message, 'error');
|
showToast('HEX 文件解析失败: ' + err.message, 'error');
|
||||||
@@ -228,32 +296,21 @@ export function processFile(file, chip) {
|
|||||||
window.updateUploadButtonLabel();
|
window.updateUploadButtonLabel();
|
||||||
addLog('I', '文件', chip.toUpperCase() + ' 已选择: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
addLog('I', '文件', chip.toUpperCase() + ' 已选择: ' + file.name + ' (' + formatBytes(file.size) + ')');
|
||||||
showToast(chip.toUpperCase() + ' 文件已选择', 'success');
|
showToast(chip.toUpperCase() + ' 文件已选择', 'success');
|
||||||
// ESP32 文件: 计算并显示 SHA-256
|
// 选文件时就把 SHA-256 算好并缓存(ESP32/GD32 都算),
|
||||||
if (chip === 'esp32') {
|
// 上传时若文件未变 ensureFileShaCached 命中直接复用。
|
||||||
var shaElem = document.getElementById('fileSha256' + suffix);
|
ensureFileShaCached(file, chip);
|
||||||
if (shaElem) {
|
|
||||||
shaElem.textContent = '计算中...';
|
|
||||||
file.arrayBuffer().then(function(buf) {
|
|
||||||
return crypto.subtle.digest('SHA-256', buf);
|
|
||||||
}).then(function(hashBuffer) {
|
|
||||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
||||||
shaElem.textContent = hashHex.toUpperCase();
|
|
||||||
// 存储到全局变量供上传时使用
|
|
||||||
window._esp32FileSha256 = hashHex.toUpperCase();
|
|
||||||
addLog('I', '文件', 'ESP32 固件 SHA-256: ' + hashHex.toUpperCase());
|
|
||||||
}).catch(function(err) {
|
|
||||||
shaElem.textContent = '计算失败: ' + err.message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @description 清除已选固件文件并重置对应 UI 状态。 */
|
/** @description 清除已选固件文件并重置对应 UI 状态。 */
|
||||||
export function clearFile(chip) {
|
export function clearFile(chip) {
|
||||||
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
var suffix = chip === 'esp32' ? 'Esp32' : 'Gd32';
|
||||||
if (chip === 'esp32') { window.esp32File = null; window.esp32FileHandle = null; window._esp32FileSha256 = null; }
|
if (chip === 'esp32') {
|
||||||
else { window.gd32File = null; window.gd32FileHandle = null; }
|
window.esp32File = null; window.esp32FileHandle = null;
|
||||||
|
window._esp32FileSha256 = null; window._esp32FileShaMeta = null;
|
||||||
|
} else {
|
||||||
|
window.gd32File = null; window.gd32FileHandle = null;
|
||||||
|
window._gd32FileSha256 = null; window._gd32FileShaMeta = null;
|
||||||
|
}
|
||||||
document.getElementById('fileInput' + suffix).value = '';
|
document.getElementById('fileInput' + suffix).value = '';
|
||||||
document.getElementById('fileInfo' + suffix).classList.remove('show');
|
document.getElementById('fileInfo' + suffix).classList.remove('show');
|
||||||
var shaElem = document.getElementById('fileSha256' + suffix);
|
var shaElem = document.getElementById('fileSha256' + suffix);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* progress.js - 折叠面板 + 刷写进度 + 阶段节点 UI
|
* progress.js - 折叠面板 + 刷写进度 + 阶段节点 UI
|
||||||
*
|
*
|
||||||
* 从 app.js 的「折叠面板」「刷写进度」段提取。
|
|
||||||
* 操作 DOM: #esp32Stages / #gd32Stages / #stageStatusBar / #progressFill / #progressTxt / #progressPct
|
* 操作 DOM: #esp32Stages / #gd32Stages / #stageStatusBar / #progressFill / #progressTxt / #progressPct
|
||||||
*
|
*
|
||||||
* 共享状态(通过 window.* 引用):
|
* 共享状态(通过 window.* 引用):
|
||||||
* window.activePhase / window.currentOtaTarget / window.esp32OtaPhase
|
* window.activePhase / window.currentOtaTarget / window.esp32OtaPhase
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.updateStagesFromRelayState / window.cleanupAfterOtaFailure
|
* window.updateStagesFromRelayState / window.cleanupAfterOtaFailure
|
||||||
* window.resumePollingAfterUpload / window.addLog
|
* window.resumePollingAfterUpload / window.addLog
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* session.js - 前端 OTA 会话状态机
|
* session.js - 前端 OTA 会话状态机
|
||||||
*
|
*
|
||||||
* 从 app.js 的「全局状态」+「前端 OTA 会话状态机」段提取。
|
* 统一管理一次 OTA 会话的完整生命周期,避免散布的布尔标志
|
||||||
* 统一管理一次 OTA 会话的完整生命周期,消除散布的布尔标志
|
|
||||||
* (isUploading / currentOtaTarget / activeUploadChip / isEsp32Rebooting)
|
* (isUploading / currentOtaTarget / activeUploadChip / isEsp32Rebooting)
|
||||||
* 在网络错误后被轮询错误覆盖、状态混乱的问题。
|
* 在网络错误后被轮询错误覆盖、状态混乱的问题。
|
||||||
*
|
*
|
||||||
@@ -20,7 +19,7 @@
|
|||||||
* window.gd32CompleteAnnounced / window.relayWatchLastSeq / window.gd32RelaySessionLive
|
* window.gd32CompleteAnnounced / window.relayWatchLastSeq / window.gd32RelaySessionLive
|
||||||
* window.lastSysStateCode / window.lastRelayState
|
* window.lastSysStateCode / window.lastRelayState
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.stopRelayProgressWatch / window.setProgress / window.addLog
|
* window.stopRelayProgressWatch / window.setProgress / window.addLog
|
||||||
*/
|
*/
|
||||||
import { addLog } from '../common/log.js';
|
import { addLog } from '../common/log.js';
|
||||||
|
|||||||
+1
-2
@@ -1,10 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* spi.js - SPI 帧构建与发送
|
* spi.js - SPI 帧构建与发送
|
||||||
*
|
*
|
||||||
* 从 app.js 的「SPI 发送」段提取。
|
|
||||||
* 操作 DOM: #spiType / #spiData / #spiTimeout / #spiHint / #spiResp / #spiRespText
|
* 操作 DOM: #spiType / #spiData / #spiTimeout / #spiHint / #spiResp / #spiRespText
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.getBaseUrl / window.showToast / window.addLog
|
* window.getBaseUrl / window.showToast / window.addLog
|
||||||
*/
|
*/
|
||||||
import { showToast } from '../common/toast.js';
|
import { showToast } from '../common/toast.js';
|
||||||
|
|||||||
+19
-9
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* stages.js - OTA 阶段驱动 (真实状态)
|
* stages.js - OTA 阶段驱动 (真实状态)
|
||||||
*
|
*
|
||||||
* 从 app.js 的「OTA 阶段驱动 (真实状态)」段提取。
|
|
||||||
* 将设备端 relay_state / sys_state 映射到 GD32 阶段节点 UI,
|
* 将设备端 relay_state / sys_state 映射到 GD32 阶段节点 UI,
|
||||||
* 并驱动 updateSystemStatusUi 综合 UI 更新。
|
* 并驱动 updateSystemStatusUi 综合 UI 更新。
|
||||||
*
|
*
|
||||||
@@ -11,7 +10,7 @@
|
|||||||
* window.gd32CompleteAnnounced / window.gd32RelaySessionLive
|
* window.gd32CompleteAnnounced / window.gd32RelaySessionLive
|
||||||
* window.lastSysStateCode / window.lastRelayState
|
* window.lastSysStateCode / window.lastRelayState
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.updateHeapAndTaskStatusUi / window.updateConnectionUi / window.updateHealthUi
|
* window.updateHeapAndTaskStatusUi / window.updateConnectionUi / window.updateHealthUi
|
||||||
* window.updateStagesFromRelayState / window.setActivePhase / window.setProgress
|
* window.updateStagesFromRelayState / window.setActivePhase / window.setProgress
|
||||||
* window.markGd32OtaUiComplete / window.cleanupAfterOtaFailure / window.otaSessionReset
|
* window.markGd32OtaUiComplete / window.cleanupAfterOtaFailure / window.otaSessionReset
|
||||||
@@ -234,6 +233,16 @@ export function updateSystemStatusUi(data) {
|
|||||||
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
|
if (isGd32Relaying || (rs >= 1 && rs <= 4)) {
|
||||||
window.gd32RelaySessionLive = true;
|
window.gd32RelaySessionLive = true;
|
||||||
}
|
}
|
||||||
|
/* 包进度:进入 FINISHING/COMPLETE 时先补 100%,再走完成/收尾 UI */
|
||||||
|
if (window.gd32RelaySessionLive && relayTotal > 0 &&
|
||||||
|
(relaySeq >= relayTotal || rs >= 4 || isGd32Complete)) {
|
||||||
|
if (typeof window.emitRelayProgress === 'function') {
|
||||||
|
window.emitRelayProgress(relayTotal, relayTotal, { force100: true });
|
||||||
|
}
|
||||||
|
} else if (window.gd32RelaySessionLive && relayTotal > 0 && relaySeq > 0 &&
|
||||||
|
typeof window.emitRelayProgress === 'function') {
|
||||||
|
window.emitRelayProgress(relaySeq, relayTotal);
|
||||||
|
}
|
||||||
if (window.gd32RelaySessionLive &&
|
if (window.gd32RelaySessionLive &&
|
||||||
(isGd32Complete || rs === 5 ||
|
(isGd32Complete || rs === 5 ||
|
||||||
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal && rs >= 3))) {
|
(gd32AppAlive && relayTotal > 0 && relaySeq >= relayTotal && rs >= 3))) {
|
||||||
@@ -244,8 +253,14 @@ export function updateSystemStatusUi(data) {
|
|||||||
// HANDSHAKING
|
// HANDSHAKING
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
setProgress(50, '握手与认证中...');
|
setProgress(50, '握手与认证中...');
|
||||||
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0 && rs !== 5)) {
|
} else if (rs === 4) {
|
||||||
// TRANSFERRING
|
// FINISHING:转发已 100%,进度条进入收尾段(勿再落回中间百分比)
|
||||||
|
setActivePhase('gd32Ota');
|
||||||
|
if (!window.gd32CompleteAnnounced) {
|
||||||
|
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
||||||
|
}
|
||||||
|
} else if (rs === 3 || (relayTotal > 0 && relaySeq > 0 && rs !== 5 && rs !== 4)) {
|
||||||
|
// TRANSFERRING(不含 FINISHING)
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
var relayOverall = 60 + Math.round(relayProgress * 0.30);
|
||||||
var relayLabel = relayTotal > 0
|
var relayLabel = relayTotal > 0
|
||||||
@@ -258,11 +273,6 @@ export function updateSystemStatusUi(data) {
|
|||||||
setProgress(55, relayTotal > 0
|
setProgress(55, relayTotal > 0
|
||||||
? ('大小协商中(共 ' + relayTotal + ' 包)')
|
? ('大小协商中(共 ' + relayTotal + ' 包)')
|
||||||
: '固件大小协商中');
|
: '固件大小协商中');
|
||||||
} else if (rs === 4) {
|
|
||||||
setActivePhase('gd32Ota');
|
|
||||||
if (!window.gd32CompleteAnnounced) {
|
|
||||||
setProgress(95, gd32AppAlive ? '收尾中…' : '收尾中,等待 APP 启动...');
|
|
||||||
}
|
|
||||||
} else if (!window.gd32RelaySessionLive) {
|
} else if (!window.gd32RelaySessionLive) {
|
||||||
setActivePhase('gd32Ota');
|
setActivePhase('gd32Ota');
|
||||||
setProgress(48, '已触发中继,等待设备进入握手…');
|
setProgress(48, '已触发中继,等待设备进入握手…');
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* upload-esp32.js - ESP32 本机 OTA 上传流程 + 完成升级
|
* upload-esp32.js - ESP32 本机 OTA 上传流程 + 完成升级
|
||||||
*
|
*
|
||||||
* 从 app.js 的「上传固件」「ESP32 OTA 完成升级」段提取。
|
|
||||||
* 包含:startUpload 入口(处理 esp32 + gd32 共用互斥/重读逻辑)、
|
* 包含:startUpload 入口(处理 esp32 + gd32 共用互斥/重读逻辑)、
|
||||||
* XHR 上传、SHA 校验、verifyEsp32Firmware、restartEsp32。
|
* XHR 上传、SHA 校验、verifyEsp32Firmware、restartEsp32。
|
||||||
*
|
*
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
* window._esp32FileSha256 / window._gd32FileSha256 / window.activeXhr
|
* window._esp32FileSha256 / window._gd32FileSha256 / window.activeXhr
|
||||||
* window.isUploading / window.uploadBtnOrigText / window.activeUploadChip
|
* window.isUploading / window.uploadBtnOrigText / window.activeUploadChip
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.otaSessionActive / window.otaSessionTerminated / window.otaSessionReset
|
* window.otaSessionActive / window.otaSessionTerminated / window.otaSessionReset
|
||||||
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionTarget
|
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionTarget
|
||||||
* window.cleanupAfterOtaFailure / window.pausePollingForUpload
|
* window.cleanupAfterOtaFailure / window.pausePollingForUpload
|
||||||
@@ -31,6 +30,7 @@ import {
|
|||||||
startUploadWaitHeartbeat
|
startUploadWaitHeartbeat
|
||||||
} from './session.js';
|
} from './session.js';
|
||||||
import { updateEsp32StagesFromPhase, switchStageFlow, setActivePhase, setProgress, markPhaseDone, cleanupAfterOtaFailure } from './progress.js';
|
import { updateEsp32StagesFromPhase, switchStageFlow, setActivePhase, setProgress, markPhaseDone, cleanupAfterOtaFailure } from './progress.js';
|
||||||
|
import { ensureFileShaCached } from './file-handle.js';
|
||||||
|
|
||||||
function isOtaBusyError(status, detail) {
|
function isOtaBusyError(status, detail) {
|
||||||
var text = (detail || '').toLowerCase();
|
var text = (detail || '').toLowerCase();
|
||||||
@@ -104,9 +104,13 @@ export async function startUpload(chip) {
|
|||||||
var otaChannelName = ota.channel_name || (otaChannel === 1 ? 'BLE' : 'WIFI');
|
var otaChannelName = ota.channel_name || (otaChannel === 1 ? 'BLE' : 'WIFI');
|
||||||
// ESP32 RECEIVING(5) 可由同一 WiFi 通道的新上传接管;后台/休眠可能让 /ota/update 没发出,
|
// ESP32 RECEIVING(5) 可由同一 WiFi 通道的新上传接管;后台/休眠可能让 /ota/update 没发出,
|
||||||
// 若这里提前拦截,后端 beginEsp32OtaWithRetry 的同通道清理分支永远没有机会执行。
|
// 若这里提前拦截,后端 beginEsp32OtaWithRetry 的同通道清理分支永远没有机会执行。
|
||||||
// ERROR(8) 也放行给后端清理底层 OTA 句柄;FINALIZING(6)/REBOOTING(7) 是不可逆忙态。
|
// ERROR(8) 放行给后端清理;REBOOTING(7) 不可逆。
|
||||||
if (ssc === 5 && otaChannel === 1 && _pi < 3) continue;
|
// FINALIZING(6):真收尾时后端会 403;SHA 失败后可能假占用(core 已空),
|
||||||
var esp32Busy = (ssc === 6 || ssc === 7 || (ssc === 5 && otaChannel !== 0));
|
// status 自愈 / begin 残留清理需要请求触达设备,前端同通道不硬拦。
|
||||||
|
if ((ssc === 5 || ssc === 6) && otaChannel === 1 && _pi < 3) continue;
|
||||||
|
var esp32Busy = (ssc === 7 ||
|
||||||
|
(ssc === 6 && otaChannel !== 0) ||
|
||||||
|
(ssc === 5 && otaChannel !== 0));
|
||||||
// GD32 STORING(1) 可由同一 WiFi 通道的新上传接管;RELAYING(3) 不可中断。
|
// GD32 STORING(1) 可由同一 WiFi 通道的新上传接管;RELAYING(3) 不可中断。
|
||||||
var gd32Busy = (ssc === 3 || (ssc === 1 && otaChannel !== 0));
|
var gd32Busy = (ssc === 3 || (ssc === 1 && otaChannel !== 0));
|
||||||
if ((esp32Busy || gd32Busy)) {
|
if ((esp32Busy || gd32Busy)) {
|
||||||
@@ -183,31 +187,10 @@ export async function startUpload(chip) {
|
|||||||
showToast('开始上传固件到 ' + chip.toUpperCase(), 'success');
|
showToast('开始上传固件到 ' + chip.toUpperCase(), 'success');
|
||||||
try {
|
try {
|
||||||
var arrayBuffer = await file.arrayBuffer();
|
var arrayBuffer = await file.arrayBuffer();
|
||||||
// 刷写时重新计算 SHA-256(ESP32 和 GD32 都算)
|
// SHA-256:processFile 阶段已算好并缓存;这里若 handle.getFile() 拿回的
|
||||||
if (chip === 'esp32') {
|
// fresh file meta 未变就直接复用(命中 30–100ms/MB 的计算);
|
||||||
try {
|
// 用户 rebuild 使 lastModified 变化时 ensureFileShaCached 自动 fallback 到重算。
|
||||||
var hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
await ensureFileShaCached(file, chip, arrayBuffer);
|
||||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
||||||
window._esp32FileSha256 = hashHex.toUpperCase();
|
|
||||||
var shaElem = document.getElementById('fileSha256Esp32');
|
|
||||||
if (shaElem) shaElem.textContent = window._esp32FileSha256;
|
|
||||||
addLog('I', '文件', '刷写前重新计算 SHA-256: ' + window._esp32FileSha256);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('W', '文件', 'SHA-256 重新计算失败: ' + e.message);
|
|
||||||
}
|
|
||||||
} else if (chip === 'gd32') {
|
|
||||||
try {
|
|
||||||
var hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
|
||||||
var hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
var hashHex = hashArray.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
||||||
window._gd32FileSha256 = hashHex.toUpperCase();
|
|
||||||
addLog('I', '文件', 'GD32 SHA-256: ' + window._gd32FileSha256);
|
|
||||||
} catch (e) {
|
|
||||||
addLog('W', '文件', 'GD32 SHA-256 重新计算失败: ' + e.message);
|
|
||||||
window._gd32FileSha256 = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var fileSize = file.size;
|
var fileSize = file.size;
|
||||||
var body, url = window.getBaseUrl() + '/ota/upload';
|
var body, url = window.getBaseUrl() + '/ota/upload';
|
||||||
if (chip === 'esp32') {
|
if (chip === 'esp32') {
|
||||||
|
|||||||
+93
-48
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* upload-gd32.js - GD32 中继 OTA 触发 + relay 态跟踪
|
* upload-gd32.js - GD32 中继 OTA 触发 + relay 态跟踪
|
||||||
*
|
*
|
||||||
* 从 app.js 的「GD32 OTA 触发(带 SHA-256 校验,对齐 BLE 0xF7)」段提取。
|
|
||||||
* 包含:relay 进度观察定时器、markGd32OtaUiComplete、triggerGd32Relay。
|
* 包含:relay 进度观察定时器、markGd32OtaUiComplete、triggerGd32Relay。
|
||||||
*
|
*
|
||||||
* 共享状态(通过 window.* 引用):
|
* 共享状态(通过 window.* 引用):
|
||||||
@@ -9,7 +8,7 @@
|
|||||||
* window.pollingPausedForUpload / window.statusPollIntervalMs
|
* window.pollingPausedForUpload / window.statusPollIntervalMs
|
||||||
* window.otaSessionState / window.lastSysStateCode / window.lastRelayState
|
* window.otaSessionState / window.lastSysStateCode / window.lastRelayState
|
||||||
*
|
*
|
||||||
* 未拆分的函数(通过 window.* 引用):
|
* 复用的函数(通过 window.* 引用):
|
||||||
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionActive
|
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionActive
|
||||||
* window.markPhaseDone / window.setProgress / window.updateSystemStatusUi
|
* window.markPhaseDone / window.setProgress / window.updateSystemStatusUi
|
||||||
* window.cleanupAfterOtaFailure / window.scheduleStatusPoll / window.pollLogs
|
* window.cleanupAfterOtaFailure / window.scheduleStatusPoll / window.pollLogs
|
||||||
@@ -26,16 +25,61 @@ import { markPhaseDone, setProgress } from './progress.js';
|
|||||||
* GD32 中继进度观察:以 /system/status 为唯一真相源。
|
* GD32 中继进度观察:以 /system/status 为唯一真相源。
|
||||||
* 日志环只有 64 条,中继/擦除期间 INFO 会被挤掉,时间戳还是浏览器本地时间——
|
* 日志环只有 64 条,中继/擦除期间 INFO 会被挤掉,时间戳还是浏览器本地时间——
|
||||||
* 绝不能用「最后一条日志停在 SEND_SIZE」判断卡死。
|
* 绝不能用「最后一条日志停在 SEND_SIZE」判断卡死。
|
||||||
|
*
|
||||||
|
* 时序约定(避免「设备已 FINISH / 完成,UI 还在 94%」):
|
||||||
|
* 1) 包进度变化先刷进度条 + 日志;
|
||||||
|
* 2) 一旦 rseq>=rtot 或 rs>=4(FINISHING),先强制打出 100% 再进收尾/完成;
|
||||||
|
* 3) markGd32OtaUiComplete 前再兜底一次 100%。
|
||||||
*/
|
*/
|
||||||
let relayWatchTimerId = null;
|
let relayWatchTimerId = null;
|
||||||
let relayWatchInFlight = false;
|
|
||||||
window.gd32CompleteAnnounced = false;
|
window.gd32CompleteAnnounced = false;
|
||||||
window.relayWatchLastSeq = -1;
|
window.relayWatchLastSeq = -1;
|
||||||
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
|
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
|
||||||
window.gd32RelaySessionLive = false;
|
window.gd32RelaySessionLive = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包进度变更时更新进度条并写一条「转发」日志(同 seq 不重复)。
|
||||||
|
* @param {number} rseq
|
||||||
|
* @param {number} rtot
|
||||||
|
* @param {{force100?: boolean}} [opts]
|
||||||
|
* @returns {boolean} 是否实际输出了进度
|
||||||
|
*/
|
||||||
|
export function emitRelayProgress(rseq, rtot, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
rseq = Number(rseq) || 0;
|
||||||
|
rtot = Number(rtot) || 0;
|
||||||
|
if (rtot <= 0) return false;
|
||||||
|
if (opts.force100 || rseq >= rtot) {
|
||||||
|
rseq = rtot;
|
||||||
|
}
|
||||||
|
if (rseq <= 0) return false;
|
||||||
|
if (rseq === window.relayWatchLastSeq) return false;
|
||||||
|
window.relayWatchLastSeq = rseq;
|
||||||
|
var pct = Math.min(100, Math.round(rseq * 100 / rtot));
|
||||||
|
setProgress(60 + Math.round(pct * 0.30),
|
||||||
|
'已转发 ' + rseq + ' / ' + rtot + ' 包 (' + pct + '%)');
|
||||||
|
addLog('I', 'OTA', '转发 ' + rseq + '/' + rtot + ' (' + pct + '%)');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完成/收尾前:若尚未打出 100%,用 status 里的包数补一条 */
|
||||||
|
export function ensureRelayProgressComplete() {
|
||||||
|
var data = window.latestSystemStatus;
|
||||||
|
var o = (data && data.ota) || {};
|
||||||
|
var rtot = Number(o.relay_total_packets) || 0;
|
||||||
|
var rseq = Number(o.relay_seq) || 0;
|
||||||
|
if (rtot <= 0) return;
|
||||||
|
/* FINISHING/DONE 时后端 seq 已到 total;status 滞后时也强制 100% */
|
||||||
|
if (rseq < rtot && (Number(o.relay_state) >= 4 || Number(o.sys_state_code) === 4)) {
|
||||||
|
rseq = rtot;
|
||||||
|
}
|
||||||
|
if (rseq >= rtot) {
|
||||||
|
emitRelayProgress(rtot, rtot, { force100: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function markGd32OtaUiComplete(msg) {
|
export function markGd32OtaUiComplete(msg) {
|
||||||
// 已公告过:禁止再改 className / setProgress / toast,否则每 400ms 重触发动效会「疯狂跳 ✓」
|
// 已公告过:禁止再改 className / setProgress / toast,否则定时器重触发动效会「疯狂跳 ✓」
|
||||||
if (window.gd32CompleteAnnounced) {
|
if (window.gd32CompleteAnnounced) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,6 +88,8 @@ export function markGd32OtaUiComplete(msg) {
|
|||||||
window.otaSessionState !== OtaSessionState.FAILED) {
|
window.otaSessionState !== OtaSessionState.FAILED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* 先 100% 转发进度,再宣布刷写完成 —— 避免日志里「完成」早于「1089/1089」 */
|
||||||
|
ensureRelayProgressComplete();
|
||||||
window.gd32CompleteAnnounced = true;
|
window.gd32CompleteAnnounced = true;
|
||||||
stopRelayProgressWatch();
|
stopRelayProgressWatch();
|
||||||
|
|
||||||
@@ -67,18 +113,25 @@ export function stopRelayProgressWatch() {
|
|||||||
clearInterval(relayWatchTimerId);
|
clearInterval(relayWatchTimerId);
|
||||||
relayWatchTimerId = null;
|
relayWatchTimerId = null;
|
||||||
}
|
}
|
||||||
relayWatchInFlight = false;
|
/* 中继结束:恢复常态 1s status 轮询 */
|
||||||
if (window.statusPollIntervalMs < 1000) {
|
if (typeof window.setStatusPollIntervalMs === 'function') {
|
||||||
window.statusPollIntervalMs = 1000;
|
window.setStatusPollIntervalMs(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中继进度观察:只读 pollStatus 写入的 latestSystemStatus,不再另起 /system/status。
|
||||||
|
* 中继期 status 加速到 400ms,使「转发 N/M」更贴近设备真实进度。
|
||||||
|
*/
|
||||||
export function startRelayProgressWatch() {
|
export function startRelayProgressWatch() {
|
||||||
stopRelayProgressWatch();
|
stopRelayProgressWatch();
|
||||||
if (window.pollingPausedForUpload) {
|
if (window.pollingPausedForUpload) {
|
||||||
window.resumePollingAfterUpload();
|
window.resumePollingAfterUpload();
|
||||||
}
|
}
|
||||||
window.statusPollIntervalMs = 400;
|
/* 中继期加速 status,缩小 UI 进度相对设备日志的滞后 */
|
||||||
|
if (typeof window.setStatusPollIntervalMs === 'function') {
|
||||||
|
window.setStatusPollIntervalMs(400);
|
||||||
|
}
|
||||||
window.scheduleStatusPoll(0);
|
window.scheduleStatusPoll(0);
|
||||||
window.pollLogs();
|
window.pollLogs();
|
||||||
addLog('I', 'OTA', '跟踪中继进度:看进度条与 relay_seq(/system/status),勿只看日志');
|
addLog('I', 'OTA', '跟踪中继进度:看进度条与 relay_seq(/system/status),勿只看日志');
|
||||||
@@ -92,46 +145,38 @@ export function startRelayProgressWatch() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (relayWatchInFlight) return;
|
var data = window.latestSystemStatus;
|
||||||
relayWatchInFlight = true;
|
if (!data || !data.ota) return;
|
||||||
fetch(window.getBaseUrl() + '/system/status', { signal: AbortSignal.timeout(3000) })
|
var o = data.ota || {};
|
||||||
.then(function (resp) { return resp.ok ? resp.json() : null; })
|
var code = Number(o.sys_state_code);
|
||||||
.then(function (data) {
|
var rstate = Number(o.relay_state);
|
||||||
if (!data) return;
|
var alive = !!o.gd32_app_alive;
|
||||||
try { window.updateSystemStatusUi(data); } catch (e) { /* ignore */ }
|
var rseq = Number(o.relay_seq) || 0;
|
||||||
var o = data.ota || {};
|
var rtot = Number(o.relay_total_packets) || 0;
|
||||||
var code = Number(o.sys_state_code);
|
if (rstate >= 1 && rstate <= 4) {
|
||||||
var rstate = Number(o.relay_state);
|
window.gd32RelaySessionLive = true;
|
||||||
var alive = !!o.gd32_app_alive;
|
}
|
||||||
var rseq = Number(o.relay_seq) || 0;
|
if (code === 8 || rstate === 255) {
|
||||||
var rtot = Number(o.relay_total_packets) || 0;
|
stopRelayProgressWatch();
|
||||||
if (rstate >= 1 && rstate <= 4) {
|
window.pollLogs();
|
||||||
window.gd32RelaySessionLive = true;
|
return;
|
||||||
}
|
}
|
||||||
if (window.gd32RelaySessionLive &&
|
/*
|
||||||
(code === 4 || rstate === 5 ||
|
* 顺序:先刷包进度(含强制 100%),再判定完成。
|
||||||
(alive && rtot > 0 && rseq >= rtot && rstate >= 3))) {
|
* FINISHING/DONE 时若 status 滞后未到 total,仍按 total 打 100%。
|
||||||
markGd32OtaUiComplete('GD32 刷写完成');
|
*/
|
||||||
window.pollLogs();
|
var transferDone = rtot > 0 && (rseq >= rtot || rstate >= 4 || code === 4);
|
||||||
return;
|
if (transferDone) {
|
||||||
}
|
emitRelayProgress(rtot, rtot, { force100: true });
|
||||||
if (code === 8 || rstate === 255) {
|
} else if (rtot > 0 && rseq > 0) {
|
||||||
stopRelayProgressWatch();
|
emitRelayProgress(rseq, rtot);
|
||||||
window.pollLogs();
|
}
|
||||||
return;
|
if (window.gd32RelaySessionLive &&
|
||||||
}
|
(code === 4 || rstate === 5 ||
|
||||||
// 前端进度同步:包数变化时更新进度条(updateSystemStatusUi 已做)并写一条前端日志
|
(alive && rtot > 0 && rseq >= rtot && rstate >= 3))) {
|
||||||
// 后端不再打「转发进度」文字,只靠这里 + 进度条
|
markGd32OtaUiComplete('GD32 刷写完成');
|
||||||
if (rtot > 0 && rseq > 0 && rseq !== window.relayWatchLastSeq) {
|
window.pollLogs();
|
||||||
window.relayWatchLastSeq = rseq;
|
}
|
||||||
var pct = Math.round(rseq * 100 / rtot);
|
|
||||||
setProgress(60 + Math.round(pct * 0.30),
|
|
||||||
'已转发 ' + rseq + ' / ' + rtot + ' 包 (' + pct + '%)');
|
|
||||||
addLog('I', 'OTA', '转发 ' + rseq + '/' + rtot + ' (' + pct + '%)');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () { /* 下轮继续 */ })
|
|
||||||
.then(function () { relayWatchInFlight = false; });
|
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user