feat(ota): 添加 GD32 版本信息获取和 API 端点
- 实现了通过 SPI PING 命令获取 GD32 应用程序和引导加载程序版本信息 - 添加 /api/version HTTP 端点返回固件版本、编译时间和 Git 哈希 - 在系统状态 API 中包含 GD32 版本信息和存活状态 - 优化 SPI PING 协议解析以支持版本数据提取 refactor(ota): 移除压缩功能并简化 OTA 上传流程 - 移除 OTA 数据压缩和解压缩功能,简化数据处理流程 - 删除 utils/ota_compressor.cpp 文件及相关依赖 - 直接写入原始数据块而不是解压缩后写入 - 更新 OTA 上传处理器以处理原始二进制数据 build(cmakelists): 添加版本头文件自动生成机制 - 配置 CMake 构建时自动生成 version.h 头文件 - 集成 git 哈希和编译时间信息到固件版本 - 更新主 CMakeLists.txt 以包含版本生成目标 - 修改 app_config.h 以使用自动生成的版本字符串 fix(wifi_ota): 修复 OTA 中继状态管理和 SPI 通信问题 - 修复 OTA 中继完成后立即重置的问题,增加 3 秒延迟让前端轮询完成 - 修正 SPI CRC 错误检测和调试信息输出 - 优化 SPI PING 通信超时和重试机制 - 修复 OTA 上传大小计算和数据写入偏移跟踪
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
cmake_minimum_required(VERSION 3.22)
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
|
||||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
project(ESP32_COMM)
|
project(ESP32_COMM)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# gen_version.cmake - 自动生成版本信息头文件
|
||||||
|
# 包含版本号、编译时间、Git 信息
|
||||||
|
|
||||||
|
# 版本号(可手动修改)
|
||||||
|
set(FW_VERSION_MAJOR 0)
|
||||||
|
set(FW_VERSION_MINOR 3)
|
||||||
|
set(FW_VERSION_PATCH 10)
|
||||||
|
|
||||||
|
# 获取编译时间
|
||||||
|
string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# 获取 Git 信息(如果可用)
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND)
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
set(GIT_HASH "unknown")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 生成版本头文件
|
||||||
|
set(VERSION_HEADER_CONTENT
|
||||||
|
"// Auto-generated version info - DO NOT EDIT
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 固件版本
|
||||||
|
#define FW_VERSION_MAJOR ${FW_VERSION_MAJOR}
|
||||||
|
#define FW_VERSION_MINOR ${FW_VERSION_MINOR}
|
||||||
|
#define FW_VERSION_PATCH ${FW_VERSION_PATCH}
|
||||||
|
#define FW_VERSION_STRING \"${FW_VERSION_MAJOR}.${FW_VERSION_MINOR}.${FW_VERSION_PATCH}\"
|
||||||
|
|
||||||
|
// 编译时间
|
||||||
|
#define BUILD_TIME \"${BUILD_TIME}\"
|
||||||
|
|
||||||
|
// Git 信息
|
||||||
|
#define GIT_HASH \"${GIT_HASH}\"
|
||||||
|
")
|
||||||
|
|
||||||
|
# 检查文件是否需要更新
|
||||||
|
if(EXISTS ${VERSION_HEADER_PATH})
|
||||||
|
file(READ ${VERSION_HEADER_PATH} EXISTING_CONTENT)
|
||||||
|
if(NOT "${EXISTING_CONTENT}" STREQUAL "${VERSION_HEADER_CONTENT}")
|
||||||
|
file(WRITE ${VERSION_HEADER_PATH} "${VERSION_HEADER_CONTENT}")
|
||||||
|
message(STATUS "Version header updated: ${VERSION_HEADER_PATH}")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
file(WRITE ${VERSION_HEADER_PATH} "${VERSION_HEADER_CONTENT}")
|
||||||
|
message(STATUS "Version header generated: ${VERSION_HEADER_PATH}")
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# gen_version_script.cmake - 构建时执行的独立脚本
|
||||||
|
# 通过 add_custom_target 在每次 build 时调用,确保版本信息始终最新
|
||||||
|
#
|
||||||
|
# 需要传入的参数:
|
||||||
|
# SOURCE_DIR - 项目根目录 (ESP32_COMM)
|
||||||
|
# OUTPUT_FILE - 输出的 version.h 路径
|
||||||
|
|
||||||
|
# 版本号(可手动修改)
|
||||||
|
set(FW_VERSION_MAJOR 0)
|
||||||
|
set(FW_VERSION_MINOR 3)
|
||||||
|
set(FW_VERSION_PATCH 11)
|
||||||
|
|
||||||
|
# 获取编译时间
|
||||||
|
string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# 获取 Git 信息
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND)
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD
|
||||||
|
WORKING_DIRECTORY ${SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
set(GIT_HASH "unknown")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 生成版本头文件内容
|
||||||
|
set(VERSION_HEADER_CONTENT
|
||||||
|
"// Auto-generated version info - DO NOT EDIT
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 固件版本
|
||||||
|
#define FW_VERSION_MAJOR ${FW_VERSION_MAJOR}
|
||||||
|
#define FW_VERSION_MINOR ${FW_VERSION_MINOR}
|
||||||
|
#define FW_VERSION_PATCH ${FW_VERSION_PATCH}
|
||||||
|
#define FW_VERSION_STRING \"${FW_VERSION_MAJOR}.${FW_VERSION_MINOR}.${FW_VERSION_PATCH}\"
|
||||||
|
|
||||||
|
// 编译时间
|
||||||
|
#define BUILD_TIME \"${BUILD_TIME}\"
|
||||||
|
|
||||||
|
// Git 信息
|
||||||
|
#define GIT_HASH \"${GIT_HASH}\"
|
||||||
|
")
|
||||||
|
|
||||||
|
# 仅在内容变化时写入,避免不必要的重编译
|
||||||
|
if(EXISTS ${OUTPUT_FILE})
|
||||||
|
file(READ ${OUTPUT_FILE} EXISTING_CONTENT)
|
||||||
|
if(NOT "${EXISTING_CONTENT}" STREQUAL "${VERSION_HEADER_CONTENT}")
|
||||||
|
file(WRITE ${OUTPUT_FILE} "${VERSION_HEADER_CONTENT}")
|
||||||
|
message(STATUS "Version header updated: v${FW_VERSION_MAJOR}.${FW_VERSION_MINOR}.${FW_VERSION_PATCH} (${GIT_HASH})")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
file(WRITE ${OUTPUT_FILE} "${VERSION_HEADER_CONTENT}")
|
||||||
|
message(STATUS "Version header generated: v${FW_VERSION_MAJOR}.${FW_VERSION_MINOR}.${FW_VERSION_PATCH} (${GIT_HASH})")
|
||||||
|
endif()
|
||||||
+10
-1
@@ -6,7 +6,6 @@
|
|||||||
"transport/protocol.cpp"
|
"transport/protocol.cpp"
|
||||||
"server/wifi_ota.cpp"
|
"server/wifi_ota.cpp"
|
||||||
"server/ble_ota.cpp"
|
"server/ble_ota.cpp"
|
||||||
"utils/ota_compressor.cpp"
|
|
||||||
"monitor/health_monitor.cpp"
|
"monitor/health_monitor.cpp"
|
||||||
"monitor/debug_log.cpp"
|
"monitor/debug_log.cpp"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
@@ -34,4 +33,14 @@
|
|||||||
freertos
|
freertos
|
||||||
mbedtls
|
mbedtls
|
||||||
bt
|
bt
|
||||||
|
)
|
||||||
|
|
||||||
|
# 每次构建时自动刷新 version.h(BUILD_TIME / GIT_HASH)
|
||||||
|
add_custom_target(version_header ALL
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-DSOURCE_DIR=${CMAKE_SOURCE_DIR}
|
||||||
|
-DOUTPUT_FILE=${CMAKE_CURRENT_SOURCE_DIR}/include/version.h
|
||||||
|
-P ${CMAKE_SOURCE_DIR}/cmake/gen_version_script.cmake
|
||||||
|
COMMENT "Refreshing version.h"
|
||||||
|
VERBATIM
|
||||||
)
|
)
|
||||||
+15
-11
@@ -3,14 +3,18 @@
|
|||||||
* ESP32-C3 OTA 中继项目
|
* ESP32-C3 OTA 中继项目
|
||||||
*
|
*
|
||||||
* 使用 constexpr 代替 #define,提供类型安全
|
* 使用 constexpr 代替 #define,提供类型安全
|
||||||
|
* 版本号和编译时间由 version.h 自动生成
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 固件版本
|
// 固件版本(由 version.h 自动生成,此处提供 C++ constexpr 访问)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr const char *FW_VERSION = "0.3.1";
|
inline constexpr const char *FW_VERSION = FW_VERSION_STRING;
|
||||||
|
inline constexpr const char *FW_BUILD_TIME = BUILD_TIME;
|
||||||
|
inline constexpr const char *FW_GIT_HASH = GIT_HASH;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// WiFi 工作模式选择
|
// WiFi 工作模式选择
|
||||||
@@ -54,21 +58,20 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
|
|||||||
// OTA 配置
|
// OTA 配置
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr size_t OTA_BLOCK_SIZE = 8192;
|
inline constexpr size_t OTA_BLOCK_SIZE = 8192;
|
||||||
inline constexpr size_t OTA_DECOMP_OUT_BUF_SIZE = 2048; // 流式解压提示值,实际受堆限制自适应 // adaptive: init caps to available heap
|
|
||||||
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB,对齐 GD32 APP_MAX_SIZE (0x36000)
|
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB,对齐 GD32 APP_MAX_SIZE (0x36000)
|
||||||
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_POLL_RETRIES = 40; // 每包 ACK 轮询次数
|
||||||
inline constexpr uint32_t OTA_RESEND_RETRIES = 10; // 每包重发次数上限
|
inline constexpr uint32_t OTA_RESEND_RETRIES = 10; // 每包重发次数上限
|
||||||
inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间 // 每包重发次数上限
|
inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 健康监控阈值
|
// 健康监控阈值
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
|
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
|
||||||
inline constexpr int32_t WIFI_RSSI_WARNING = -75;
|
inline constexpr int32_t WIFI_RSSI_WARNING = -75;
|
||||||
inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; // OTA 期间更宽松,避免瞬时波动误判
|
inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88;
|
||||||
inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; // OTA 最多接受 15 秒内的 RSSI 样本
|
inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000;
|
||||||
inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; // 每 32 个 48B 包做一次 OTA 健康检查
|
inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32;
|
||||||
inline constexpr int32_t TEMP_WARNING = 80;
|
inline constexpr int32_t TEMP_WARNING = 80;
|
||||||
inline constexpr int32_t TEMP_CRITICAL = 100;
|
inline constexpr int32_t TEMP_CRITICAL = 100;
|
||||||
inline constexpr size_t HEAP_WARNING_MIN = 10240;
|
inline constexpr size_t HEAP_WARNING_MIN = 10240;
|
||||||
@@ -76,8 +79,8 @@ inline constexpr size_t HEAP_WARNING_MIN = 10240;
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// 调试配置(UDP 日志)
|
// 调试配置(UDP 日志)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志
|
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1;
|
||||||
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP
|
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3";
|
||||||
inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
|
inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -94,8 +97,8 @@ inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7;
|
|||||||
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8;
|
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8;
|
||||||
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00;
|
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00;
|
||||||
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01;
|
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01;
|
||||||
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; // 默认目标 payload;ATT Write Command 可写值≈(MTU-3),当前 512 MTU 下协议 payload 建议 ≤ 498B
|
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480;
|
||||||
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; // cmd(1)+seq(2)+size(4)+crc(4)
|
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 安全常量(需与 GD32 固件团队确认)
|
// 安全常量(需与 GD32 固件团队确认)
|
||||||
@@ -120,3 +123,4 @@ inline constexpr uint8_t GD32_CHIP_UID[] = {
|
|||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||||
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Auto-generated version info - DO NOT EDIT
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 固件版本
|
||||||
|
#define FW_VERSION_MAJOR 0
|
||||||
|
#define FW_VERSION_MINOR 3
|
||||||
|
#define FW_VERSION_PATCH 11
|
||||||
|
#define FW_VERSION_STRING "0.3.11"
|
||||||
|
|
||||||
|
// 编译时间
|
||||||
|
#define BUILD_TIME "2026-06-03 03:00:47"
|
||||||
|
|
||||||
|
// Git 信息
|
||||||
|
#define GIT_HASH "a59364e"
|
||||||
@@ -28,6 +28,7 @@ relay_state_t OtaRelay::relayState = RELAY_IDLE;
|
|||||||
uint32_t OtaRelay::relaySeq = 0;
|
uint32_t OtaRelay::relaySeq = 0;
|
||||||
uint32_t OtaRelay::relayTotalPackets = 0;
|
uint32_t OtaRelay::relayTotalPackets = 0;
|
||||||
volatile bool OtaRelay::gd32AppAlive = false;
|
volatile bool OtaRelay::gd32AppAlive = false;
|
||||||
|
gd32_version_t OtaRelay::gd32Version = {};
|
||||||
QueueHandle_t OtaRelay::dispatchQueue = NULL;
|
QueueHandle_t OtaRelay::dispatchQueue = NULL;
|
||||||
QueueHandle_t OtaRelay::resultQueue = NULL;
|
QueueHandle_t OtaRelay::resultQueue = NULL;
|
||||||
|
|
||||||
@@ -870,10 +871,15 @@ ota_result_t OtaRelay::finishOta()
|
|||||||
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(500));
|
vTaskDelay(pdMS_TO_TICKS(500));
|
||||||
uint32_t uptime = 0;
|
uint32_t uptime = 0;
|
||||||
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime);
|
gd32_version_t ping_ver = {};
|
||||||
|
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime, &ping_ver);
|
||||||
if (ping_err == ESP_OK) {
|
if (ping_err == ESP_OK) {
|
||||||
gd32AppAlive = true;
|
gd32AppAlive = true;
|
||||||
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec", (unsigned long)uptime);
|
gd32Version = ping_ver;
|
||||||
|
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
|
||||||
|
(unsigned long)uptime,
|
||||||
|
ping_ver.app_major, ping_ver.app_minor, ping_ver.app_patch, ping_ver.app_build,
|
||||||
|
ping_ver.boot_major, ping_ver.boot_minor, ping_ver.boot_patch, ping_ver.boot_build);
|
||||||
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1018,11 +1024,13 @@ void OtaRelay::taskEntry(void *pvParameters)
|
|||||||
|
|
||||||
if (result == OTA_RESULT_OK) {
|
if (result == OTA_RESULT_OK) {
|
||||||
DEBUG_INFO("OTA", "OTA 转发成功完成");
|
DEBUG_INFO("OTA", "OTA 转发成功完成");
|
||||||
instance().reset();
|
/* 保持 RELAY_DONE 状态 3 秒,让网页轮询有机会看到完成状态 */
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(3000));
|
||||||
} else {
|
} else {
|
||||||
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
|
||||||
instance().reset();
|
|
||||||
}
|
}
|
||||||
|
instance().reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include "transport/protocol.h"
|
#include "transport/protocol.h"
|
||||||
|
#include "transport/spi_master.h"
|
||||||
#include "esp_partition.h"
|
#include "esp_partition.h"
|
||||||
|
|
||||||
#define OTA_META_NVS_NAMESPACE "ota_meta"
|
#define OTA_META_NVS_NAMESPACE "ota_meta"
|
||||||
@@ -80,6 +81,7 @@ public:
|
|||||||
static uint32_t relaySeq;
|
static uint32_t relaySeq;
|
||||||
static uint32_t relayTotalPackets;
|
static uint32_t relayTotalPackets;
|
||||||
static volatile bool gd32AppAlive;
|
static volatile bool gd32AppAlive;
|
||||||
|
static gd32_version_t gd32Version;
|
||||||
static QueueHandle_t dispatchQueue;
|
static QueueHandle_t dispatchQueue;
|
||||||
static QueueHandle_t resultQueue;
|
static QueueHandle_t resultQueue;
|
||||||
|
|
||||||
@@ -140,3 +142,5 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // OTA_RELAY_H
|
#endif // OTA_RELAY_H
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+99
-108
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* wifi_ota.cpp - WiFi OTA 服务器实现
|
* wifi_ota.cpp - WiFi OTA 服务器实现
|
||||||
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
|
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
|
||||||
* T4.1 - T4.5
|
* T4.1 - T4.5
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
#include "monitor/debug_log.h"
|
#include "monitor/debug_log.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "utils/ota_compressor.h"
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -402,24 +402,6 @@ static esp_err_t esp32_ota_begin(size_t total_size)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 将接收到的数据块写入 ESP32 OTA 分区。
|
|
||||||
* @param data
|
|
||||||
* @param len
|
|
||||||
* @return ESP_OK 成功,其他失败。
|
|
||||||
*/
|
|
||||||
static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
|
|
||||||
{
|
|
||||||
if (!WifiOta::instance().m_esp32OtaActive || WifiOta::instance().m_esp32UpdatePartition == NULL) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
esp_err_t err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, data, len);
|
|
||||||
if (err == ESP_OK) {
|
|
||||||
WifiOta::instance().m_esp32Received += len;
|
|
||||||
}
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
|
* @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
|
||||||
* @param req
|
* @param req
|
||||||
@@ -539,23 +521,6 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
|
|||||||
* @param req
|
* @param req
|
||||||
* @return ESP_OK 成功,其他失败。
|
* @return ESP_OK 成功,其他失败。
|
||||||
*/
|
*/
|
||||||
// --- OTA decompress write callbacks ---
|
|
||||||
struct Esp32DecompArg { size_t written; };
|
|
||||||
static esp_err_t esp32_decomp_write_cb(const uint8_t *data, size_t len, void *arg) {
|
|
||||||
auto *a = (Esp32DecompArg *)arg;
|
|
||||||
esp_err_t err = esp32_ota_write(data, len);
|
|
||||||
if (err == ESP_OK) a->written += len;
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Gd32DecompArg { uint32_t offset; };
|
|
||||||
static esp_err_t gd32_decomp_write_cb(const uint8_t *data, size_t len, void *arg) {
|
|
||||||
auto *a = (Gd32DecompArg *)arg;
|
|
||||||
esp_err_t err = OtaRelay::flashWriteChunk(a->offset, data, len);
|
|
||||||
if (err == ESP_OK) a->offset += (uint32_t)len;
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t ota_upload_handler(httpd_req_t *req)
|
static esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||||
{
|
{
|
||||||
http_set_cors_headers(req);
|
http_set_cors_headers(req);
|
||||||
@@ -571,7 +536,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool is_esp32_ota = false;
|
bool is_esp32_ota = false;
|
||||||
OtaCompress compress_type = OtaCompress::NONE;
|
|
||||||
uint32_t total_size = 0;
|
uint32_t total_size = 0;
|
||||||
int json_header_len = 0;
|
int json_header_len = 0;
|
||||||
uint8_t pre_read_buf[256] = {0};
|
uint8_t pre_read_buf[256] = {0};
|
||||||
@@ -602,14 +566,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
if (size_item) {
|
if (size_item) {
|
||||||
total_size = (uint32_t)cJSON_GetNumberValue(size_item);
|
total_size = (uint32_t)cJSON_GetNumberValue(size_item);
|
||||||
}
|
}
|
||||||
cJSON *compress_item = cJSON_GetObjectItem(root, "compress");
|
|
||||||
if (compress_item && cJSON_IsString(compress_item)) {
|
|
||||||
const char *cstr = cJSON_GetStringValue(compress_item);
|
|
||||||
if (strcmp(cstr, "zlib") == 0) {
|
|
||||||
compress_type = OtaCompress::ZLIB;
|
|
||||||
DEBUG_INFO("WIFI", "detected zlib compressed upload");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,13 +584,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// init decompressor (NONE = passthrough, no extra RAM)
|
|
||||||
OtaDecompCtx decomp_ctx;
|
|
||||||
err = ota_decomp_init(&decomp_ctx, compress_type, OTA_DECOMP_OUT_BUF_SIZE);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "decompress init failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
|
// 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
|
||||||
uint8_t *fw_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
|
uint8_t *fw_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
|
||||||
@@ -654,15 +603,12 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||||
if (ret <= 0) break;
|
if (ret <= 0) break;
|
||||||
|
|
||||||
size_t decomp_out = 0;
|
err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, fw_buf, (size_t)ret);
|
||||||
Esp32DecompArg ea = { .written = 0 };
|
|
||||||
err = ota_decomp_process(&decomp_ctx, fw_buf, (size_t)ret,
|
|
||||||
esp32_decomp_write_cb, &ea, &decomp_out);
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "OTA decompress/write failed");
|
DEBUG_ERR("WIFI", "OTA write failed");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
total_firmware_written += ea.written;
|
total_firmware_written += ret;
|
||||||
total_received += ret;
|
total_received += ret;
|
||||||
|
|
||||||
if (total_size > 0) {
|
if (total_size > 0) {
|
||||||
@@ -676,13 +622,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
free(fw_buf);
|
free(fw_buf);
|
||||||
esp_err_t finish_err = ota_decomp_finish(&decomp_ctx);
|
|
||||||
ota_decomp_free(&decomp_ctx);
|
|
||||||
if (finish_err != ESP_OK) {
|
|
||||||
DEBUG_ERR("WIFI", "ESP32 OTA decompress verify failed");
|
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "decompress verify failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)WifiOta::instance().m_esp32Received, json_header_len);
|
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)WifiOta::instance().m_esp32Received, json_header_len);
|
||||||
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
|
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
@@ -692,11 +631,16 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
|
|
||||||
// --- GD32 OTA(原逻辑)---
|
// --- GD32 OTA(原逻辑)---
|
||||||
const uint32_t gd32_fw_size = (uint32_t)req->content_len;
|
const uint32_t gd32_fw_size = (uint32_t)req->content_len;
|
||||||
uint32_t gd32_decomp_size = (total_size > 0) ? total_size : gd32_fw_size;
|
// 网页上传协议:JSON 头 + raw binary。raw binary 大小优先取 JSON 中的 total_size,否则按 content_len - json_header_len 计算。
|
||||||
const uint32_t gd32_valid_size = gd32_decomp_size;
|
uint32_t gd32_valid_size;
|
||||||
|
if (json_header_len > 0 && (uint32_t)json_header_len < gd32_fw_size) {
|
||||||
|
gd32_valid_size = (total_size > 0 && total_size < gd32_fw_size) ? total_size : (gd32_fw_size - (uint32_t)json_header_len);
|
||||||
|
} else {
|
||||||
|
gd32_valid_size = gd32_fw_size;
|
||||||
|
}
|
||||||
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_valid_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_valid_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
||||||
if (gd32_valid_size == 0U || gd32_valid_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
if (gd32_valid_size == 0U || gd32_valid_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||||
DEBUG_ERR("WIFI", "firmware size invalid: decomp=%u rounded48=%u max=%u",
|
DEBUG_ERR("WIFI", "firmware size invalid: valid=%u rounded48=%u max=%u",
|
||||||
(unsigned)gd32_valid_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
(unsigned)gd32_valid_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
@@ -753,15 +697,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
.checksum = {0}
|
.checksum = {0}
|
||||||
};
|
};
|
||||||
OtaRelay::metaSave(&meta_raw);
|
OtaRelay::metaSave(&meta_raw);
|
||||||
// init GD32 decompressor
|
uint32_t gd32_offset = 0;
|
||||||
OtaDecompCtx gd32_decomp;
|
|
||||||
esp_err_t decomp_init_err = ota_decomp_init(&gd32_decomp, compress_type, OTA_DECOMP_OUT_BUF_SIZE);
|
|
||||||
if (decomp_init_err != ESP_OK) {
|
|
||||||
free(buf);
|
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "GD32 decompress init failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
Gd32DecompArg gd32_da = { .offset = 0 };
|
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw);
|
||||||
|
|
||||||
@@ -771,19 +707,18 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
int first_chunk_len = pre_read_len;
|
int first_chunk_len = pre_read_len;
|
||||||
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
||||||
if (first_chunk_len > 0) {
|
if (first_chunk_len > 0) {
|
||||||
size_t first_out = 0;
|
gd32_offset = 0;
|
||||||
gd32_da.offset = 0;
|
esp_err_t err = OtaRelay::flashWriteChunk(gd32_offset, pre_read_buf, (size_t)first_chunk_len);
|
||||||
esp_err_t err = ota_decomp_process(&gd32_decomp, pre_read_buf, (size_t)first_chunk_len,
|
|
||||||
gd32_decomp_write_cb, &gd32_da, &first_out);
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "GD32 first chunk decompress/write failed: len=%d err=%s",
|
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
|
||||||
first_chunk_len, esp_err_to_name(err));
|
first_chunk_len, esp_err_to_name(err));
|
||||||
free(buf);
|
free(buf);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
received = first_chunk_len;
|
received = first_chunk_len;
|
||||||
OtaRelay::metaUpdate(gd32_da.offset, (uint32_t)(gd32_da.offset / OTA_BLOCK_SIZE));
|
gd32_offset += (uint32_t)first_chunk_len;
|
||||||
|
OtaRelay::metaUpdate(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
received = pre_read_len; // 跳过已消费的 JSON 头字节
|
received = pre_read_len; // 跳过已消费的 JSON 头字节
|
||||||
@@ -802,35 +737,26 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t chunk_out = 0;
|
esp_err_t err = OtaRelay::flashWriteChunk(gd32_offset, buf, (size_t)ret);
|
||||||
esp_err_t err = ota_decomp_process(&gd32_decomp, buf, (size_t)ret,
|
|
||||||
gd32_decomp_write_cb, &gd32_da, &chunk_out);
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "GD32 decompress/write failed: offset=%u len=%d err=%s",
|
DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s",
|
||||||
(unsigned)gd32_da.offset, ret, esp_err_to_name(err));
|
(unsigned)gd32_offset, ret, esp_err_to_name(err));
|
||||||
free(buf);
|
free(buf);
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
received += ret;
|
received += ret;
|
||||||
|
gd32_offset += (uint32_t)ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
// finish GD32 decompression
|
|
||||||
esp_err_t gd32_finish_err = ota_decomp_finish(&gd32_decomp);
|
|
||||||
ota_decomp_free(&gd32_decomp);
|
|
||||||
if (gd32_finish_err != ESP_OK) {
|
|
||||||
DEBUG_ERR("WIFI", "GD32 decompress verify failed");
|
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "GD32 decompress verify failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update meta with actual decompressed size
|
// update meta with written size
|
||||||
OtaRelay::metaUpdate(gd32_da.offset, (uint32_t)(gd32_da.offset / OTA_BLOCK_SIZE));
|
OtaRelay::metaUpdate(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE));
|
||||||
|
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "GD32 upload done: %u / %u bytes (decompressed)", (unsigned)gd32_da.offset, expected_fw_size_raw);
|
DEBUG_INFO("WIFI", "GD32 upload done: %u / %u bytes", (unsigned)gd32_offset, expected_fw_size_raw);
|
||||||
|
|
||||||
ota_meta_t final_meta;
|
ota_meta_t final_meta;
|
||||||
OtaRelay::metaLoad(&final_meta);
|
OtaRelay::metaLoad(&final_meta);
|
||||||
@@ -869,7 +795,7 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
|
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
|
||||||
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_ERROR) {
|
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_APP_VERIFY && OtaRelay::relayState != RELAY_ERROR) {
|
||||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered");
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
@@ -921,7 +847,11 @@ static esp_err_t ota_status_handler(httpd_req_t *req)
|
|||||||
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s",
|
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s",
|
||||||
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
(unsigned long)meta.total_size, (unsigned long)meta.stored_size,
|
||||||
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
|
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
|
||||||
OtaRelay::gd32AppAlive ? "true" : "false");
|
OtaRelay::gd32AppAlive ? "true" : "false",
|
||||||
|
OtaRelay::gd32Version.app_major, OtaRelay::gd32Version.app_minor,
|
||||||
|
OtaRelay::gd32Version.app_patch, OtaRelay::gd32Version.app_build,
|
||||||
|
OtaRelay::gd32Version.boot_major, OtaRelay::gd32Version.boot_minor,
|
||||||
|
OtaRelay::gd32Version.boot_patch, OtaRelay::gd32Version.boot_build);
|
||||||
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
httpd_resp_send(req, resp, len);
|
httpd_resp_send(req, resp, len);
|
||||||
@@ -1208,6 +1138,25 @@ static esp_err_t wifi_status_handler(httpd_req_t *req)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief HTTP GET /api/version 处理器 - 返回固件版本、编译时间、Git hash
|
||||||
|
*/
|
||||||
|
static esp_err_t version_handler(httpd_req_t *req)
|
||||||
|
{
|
||||||
|
http_set_cors_headers(req);
|
||||||
|
gd32_version_t gv = OtaRelay::gd32Version;
|
||||||
|
char resp[512];
|
||||||
|
int len = snprintf(resp, sizeof(resp),
|
||||||
|
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", "
|
||||||
|
"\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\", \"gd32_alive\": %s}",
|
||||||
|
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH,
|
||||||
|
gv.app_major, gv.app_minor, gv.app_patch, gv.app_build,
|
||||||
|
gv.boot_major, gv.boot_minor, gv.boot_patch, gv.boot_build,
|
||||||
|
OtaRelay::gd32AppAlive ? "true" : "false");
|
||||||
|
httpd_resp_set_type(req, "application/json");
|
||||||
|
httpd_resp_send(req, resp, len);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @brief HTTP GET /system/status 处理器
|
* @brief HTTP GET /system/status 处理器
|
||||||
* @param req
|
* @param req
|
||||||
@@ -1234,14 +1183,33 @@ static esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
connected = true;
|
connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 若尚无 GD32 版本信息且 OTA 中继空闲,尝试一次 SPI PING 获取 */
|
||||||
|
if (OtaRelay::gd32Version.app_major == 0 &&
|
||||||
|
OtaRelay::gd32Version.boot_major == 0 &&
|
||||||
|
OtaRelay::relayState == RELAY_IDLE) {
|
||||||
|
static uint32_t last_ping_ms = 0;
|
||||||
|
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
|
||||||
|
if (now_ms - last_ping_ms > 5000) { /* 5 秒节流 */
|
||||||
|
last_ping_ms = now_ms;
|
||||||
|
uint32_t uptime = 0;
|
||||||
|
gd32_version_t ver = {};
|
||||||
|
if (SpiMaster::instance().appPing(500, &uptime, &ver) == ESP_OK) {
|
||||||
|
OtaRelay::gd32AppAlive = true;
|
||||||
|
OtaRelay::gd32Version = ver;
|
||||||
|
} else {
|
||||||
|
OtaRelay::gd32AppAlive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ota_meta_t meta;
|
ota_meta_t meta;
|
||||||
memset(&meta, 0, sizeof(meta));
|
memset(&meta, 0, sizeof(meta));
|
||||||
OtaRelay::metaLoad(&meta);
|
OtaRelay::metaLoad(&meta);
|
||||||
|
|
||||||
char resp[800];
|
char resp[1024];
|
||||||
int len = snprintf(resp, sizeof(resp),
|
int len = snprintf(resp, sizeof(resp),
|
||||||
"{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu}}",
|
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, \"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}}",
|
||||||
FW_VERSION,HealthMonitor::instance().getCurrent(),
|
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
|
||||||
(unsigned long)esp_get_free_heap_size(),
|
(unsigned long)esp_get_free_heap_size(),
|
||||||
(unsigned long)esp_get_minimum_free_heap_size(),
|
(unsigned long)esp_get_minimum_free_heap_size(),
|
||||||
(double)HealthMonitor::instance().getLastTemperature(),
|
(double)HealthMonitor::instance().getLastTemperature(),
|
||||||
@@ -1255,7 +1223,12 @@ static esp_err_t system_status_handler(httpd_req_t *req)
|
|||||||
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
|
WifiOta::instance().m_gd32StorageDone ? "true" : "false",
|
||||||
OtaRelay::relayState,
|
OtaRelay::relayState,
|
||||||
(unsigned long)OtaRelay::relaySeq,
|
(unsigned long)OtaRelay::relaySeq,
|
||||||
(unsigned long)OtaRelay::relayTotalPackets);
|
(unsigned long)OtaRelay::relayTotalPackets,
|
||||||
|
OtaRelay::gd32AppAlive ? "true" : "false",
|
||||||
|
OtaRelay::gd32Version.app_major, OtaRelay::gd32Version.app_minor,
|
||||||
|
OtaRelay::gd32Version.app_patch, OtaRelay::gd32Version.app_build,
|
||||||
|
OtaRelay::gd32Version.boot_major, OtaRelay::gd32Version.boot_minor,
|
||||||
|
OtaRelay::gd32Version.boot_patch, OtaRelay::gd32Version.boot_build);
|
||||||
|
|
||||||
httpd_resp_set_type(req, "application/json");
|
httpd_resp_set_type(req, "application/json");
|
||||||
httpd_resp_send(req, resp, len);
|
httpd_resp_send(req, resp, len);
|
||||||
@@ -1493,8 +1466,23 @@ esp_err_t WifiOta::httpServerStart()
|
|||||||
.user_ctx = NULL
|
.user_ctx = NULL
|
||||||
};
|
};
|
||||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri);
|
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri);
|
||||||
|
// 版本信息端点
|
||||||
|
httpd_uri_t version_uri = {
|
||||||
|
.uri = "/api/version",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = version_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &version_uri);
|
||||||
|
httpd_uri_t version_options_uri = {
|
||||||
|
.uri = "/api/version",
|
||||||
|
.method = HTTP_OPTIONS,
|
||||||
|
.handler = http_options_handler,
|
||||||
|
.user_ctx = NULL
|
||||||
|
};
|
||||||
|
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &version_options_uri);
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status, /spi/send");
|
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status, /spi/send, /api/version");
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -1515,7 +1503,7 @@ void WifiOta::signalRelay()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
|
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
|
||||||
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_ERROR) {
|
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_APP_VERIFY && OtaRelay::relayState != RELAY_ERROR) {
|
||||||
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", OtaRelay::relayState);
|
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", OtaRelay::relayState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1787,3 +1775,6 @@ void WifiOta::taskEntry(void *pvParameters)
|
|||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,11 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
|
|||||||
if (i + total > buf_len) continue;
|
if (i + total > buf_len) continue;
|
||||||
|
|
||||||
uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen);
|
uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen);
|
||||||
if (buf[i + 4 + plen] != expected_crc) continue;
|
if (buf[i + 4 + plen] != expected_crc) {
|
||||||
|
DEBUG_WARN("SPI", "CRC mismatch at offset %u: plen=%u expected=0x%02X actual=0x%02X",
|
||||||
|
(unsigned)i, (unsigned)plen, expected_crc, buf[i + 4 + plen]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (type) *type = buf[i + 1];
|
if (type) *type = buf[i + 1];
|
||||||
if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr;
|
if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr;
|
||||||
@@ -387,7 +391,7 @@ esp_err_t SpiMaster::appRequestBootloader()
|
|||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec)
|
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
|
||||||
{
|
{
|
||||||
constexpr uint8_t kAppPingType = 0xA1U;
|
constexpr uint8_t kAppPingType = 0xA1U;
|
||||||
constexpr size_t kAppPingPollBytes = 32U;
|
constexpr size_t kAppPingPollBytes = 32U;
|
||||||
@@ -412,7 +416,8 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_rom_delay_us(200);
|
/* 给 GD32 充足时间处理 CS 释放中断并准备响应帧 */
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(3));
|
||||||
|
|
||||||
uint8_t rx_raw[kAppPingPollBytes];
|
uint8_t rx_raw[kAppPingPollBytes];
|
||||||
uint8_t dummy[kAppPingPollBytes];
|
uint8_t dummy[kAppPingPollBytes];
|
||||||
@@ -427,36 +432,74 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t resp_type = 0U;
|
bool found = false;
|
||||||
const uint8_t *resp_payload = nullptr;
|
|
||||||
size_t resp_len = 0U;
|
|
||||||
size_t frame_offset = 0U;
|
|
||||||
if (otaFindFrame(rx_raw, sizeof(rx_raw),
|
|
||||||
&resp_type, &resp_payload, &resp_len, &frame_offset)) {
|
|
||||||
if (resp_type == SPI_OTA_RSP_ACK && resp_len >= 3U && resp_payload[0] == kAppPingType) {
|
|
||||||
if (uptime_sec) {
|
|
||||||
*uptime_sec = (uint32_t)resp_payload[1] | ((uint32_t)resp_payload[2] << 8);
|
|
||||||
}
|
|
||||||
DEBUG_INFO("SPI", "APP PING 成功: uptime=%lu sec, attempt=%lu, offset=%u",
|
|
||||||
uptime_sec ? (unsigned long)*uptime_sec : 0UL,
|
|
||||||
(unsigned long)attempt,
|
|
||||||
(unsigned)frame_offset);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
DEBUG_DBG("SPI", "APP PING 收到无效帧[%lu]: type=0x%02X len=%u offset=%u payload0=0x%02X",
|
/* GD32 SPI 从机字节偏移不可预测,直接在 raw buffer 中
|
||||||
(unsigned long)attempt,
|
* 搜索完整的标签化 PING 响应模式:
|
||||||
resp_type,
|
* 0xA1 [0x55...] app_vers [0xAA...] boot_vers
|
||||||
(unsigned)resp_len,
|
* 找到 0xA1 后跳过重复字节,验证 0x55 和 0xAA 标签。
|
||||||
(unsigned)frame_offset,
|
*/
|
||||||
resp_len > 0U ? resp_payload[0] : 0xFFU);
|
for (size_t i = 0; i + 14 < sizeof(rx_raw); ++i) {
|
||||||
} else {
|
if (rx_raw[i] != kAppPingType) continue; /* 找 0xA1 */
|
||||||
DEBUG_DBG("SPI", "APP PING 未找到有效帧[%lu]: first8=%02X%02X%02X%02X%02X%02X%02X%02X",
|
size_t p = i + 1;
|
||||||
(unsigned long)attempt,
|
/* 跳过最多 1 个重复的 A1 字节(SPI 偏移最多产生 1 个重复) */
|
||||||
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
|
if (p < sizeof(rx_raw) && rx_raw[p] == kAppPingType) ++p;
|
||||||
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7]);
|
if (p >= sizeof(rx_raw) || rx_raw[p] != 0x55U) continue;
|
||||||
|
/* 跳过最多 1 个重复的 0x55 */
|
||||||
|
if (p + 1 < sizeof(rx_raw) && rx_raw[p + 1] == 0x55U) ++p;
|
||||||
|
size_t app_start = p + 1; /* APP 版本起始 */
|
||||||
|
if (app_start + 4 > sizeof(rx_raw)) continue;
|
||||||
|
size_t bp = app_start + 4;
|
||||||
|
/* 跳过最多 1 个重复字节 */
|
||||||
|
if (bp < sizeof(rx_raw) && bp > 0 && rx_raw[bp] == rx_raw[bp - 1]) ++bp;
|
||||||
|
if (bp >= sizeof(rx_raw) || rx_raw[bp] != 0xAAU) continue;
|
||||||
|
/* 跳过最多 1 个重复的 0xAA */
|
||||||
|
if (bp + 1 < sizeof(rx_raw) && rx_raw[bp + 1] == 0xAAU) ++bp;
|
||||||
|
size_t boot_start = bp + 1;
|
||||||
|
if (boot_start + 4 > sizeof(rx_raw)) continue;
|
||||||
|
|
||||||
|
/* 模式匹配成功!提取版本数据 */
|
||||||
|
found = true;
|
||||||
|
|
||||||
|
if (version) {
|
||||||
|
memset(version, 0, sizeof(gd32_version_t));
|
||||||
|
version->app_major = rx_raw[app_start];
|
||||||
|
version->app_minor = rx_raw[app_start + 1];
|
||||||
|
version->app_patch = rx_raw[app_start + 2];
|
||||||
|
version->app_build = rx_raw[app_start + 3];
|
||||||
|
version->boot_major = rx_raw[boot_start];
|
||||||
|
version->boot_minor = rx_raw[boot_start + 1];
|
||||||
|
version->boot_patch = rx_raw[boot_start + 2];
|
||||||
|
version->boot_build = rx_raw[boot_start + 3];
|
||||||
|
}
|
||||||
|
if (uptime_sec) {
|
||||||
|
size_t ut = boot_start + 4;
|
||||||
|
if (ut + 3 < sizeof(rx_raw)) {
|
||||||
|
*uptime_sec = (uint32_t)rx_raw[ut]
|
||||||
|
| ((uint32_t)rx_raw[ut + 1] << 8)
|
||||||
|
| ((uint32_t)rx_raw[ut + 2] << 16)
|
||||||
|
| ((uint32_t)rx_raw[ut + 3] << 24);
|
||||||
|
} else {
|
||||||
|
*uptime_sec = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DEBUG_WARN("SPI", "APP PING 标签匹配: A1@%u APP@%u BOOT@%u",
|
||||||
|
(unsigned)i, (unsigned)app_start, (unsigned)boot_start);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
DEBUG_INFO("SPI", "APP PING 成功: uptime=%lu sec, attempt=%lu",
|
||||||
|
uptime_sec ? (unsigned long)*uptime_sec : 0UL,
|
||||||
|
(unsigned long)attempt);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_WARN("SPI", "APP PING 无匹配[%lu]: first8=%02X%02X%02X%02X%02X%02X%02X%02X",
|
||||||
|
(unsigned long)attempt,
|
||||||
|
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
|
||||||
|
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7]);
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(5));
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,3 +592,5 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ typedef struct {
|
|||||||
uint8_t dlc; // 数据长度 (1~64)
|
uint8_t dlc; // 数据长度 (1~64)
|
||||||
} can_frame_t;
|
} can_frame_t;
|
||||||
|
|
||||||
|
// GD32 版本信息(从 APP PING 响应解析)
|
||||||
|
typedef struct {
|
||||||
|
uint16_t app_major;
|
||||||
|
uint16_t app_minor;
|
||||||
|
uint16_t app_patch;
|
||||||
|
uint16_t app_build;
|
||||||
|
uint16_t boot_major;
|
||||||
|
uint16_t boot_minor;
|
||||||
|
uint16_t boot_patch;
|
||||||
|
uint16_t boot_build;
|
||||||
|
} gd32_version_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief SPI Master 驱动类(单例)
|
* @brief SPI Master 驱动类(单例)
|
||||||
*
|
*
|
||||||
@@ -97,7 +109,7 @@ public:
|
|||||||
esp_err_t appRequestBootloader();
|
esp_err_t appRequestBootloader();
|
||||||
|
|
||||||
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
||||||
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec);
|
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
|
||||||
|
|
||||||
/** @brief 执行带重试的 SPI 同步传输。 */
|
/** @brief 执行带重试的 SPI 同步传输。 */
|
||||||
esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||||
@@ -147,3 +159,5 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif // SPI_MASTER_H
|
#endif // SPI_MASTER_H
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
/*
|
|
||||||
* ota_compressor.cpp - OTA 流式解压实现
|
|
||||||
*
|
|
||||||
* 使用 miniz tinfl API 做分块 zlib/deflate 解压。
|
|
||||||
* 采用 TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF 模式,
|
|
||||||
* 输出缓冲尽可能大(最大 32KB),满了就 flush 到 Flash 然后重置。
|
|
||||||
* 这是 tinfl_decompress_mem_to_callback 的流式版本。
|
|
||||||
*
|
|
||||||
* 注意:如果 deflate 流中存在 > out_buf_size 的 back-reference,
|
|
||||||
* 解压会失败(理论上可能,实际浏览器 CompressionStream 生成的流几乎不会出现)。
|
|
||||||
*/
|
|
||||||
#include "ota_compressor.h"
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include "esp_log.h"
|
|
||||||
#include "esp_system.h"
|
|
||||||
// tinfl declarations are in ota_compressor.h
|
|
||||||
|
|
||||||
static const char *TAG = "OTA_DECOMP";
|
|
||||||
|
|
||||||
/* tinfl LZ 字典大小 = 32KB,输出缓冲的理想大小 */
|
|
||||||
static constexpr size_t TINFL_LZ_DICT = 32768; /* back-ref window, not alloc target */
|
|
||||||
/* 最小可用输出缓冲 */
|
|
||||||
static constexpr size_t TINFL_MIN_BUF = 512; /* 流式解压够用,每 flush 后重置 */
|
|
||||||
/* 为系统预留的堆空间(WiFi/TCP 栈需要一些余量) */
|
|
||||||
static constexpr size_t HEAP_MARGIN = 4096; /* WiFi+TCP 最低保障 */
|
|
||||||
|
|
||||||
esp_err_t ota_decomp_init(OtaDecompCtx *ctx, OtaCompress type, size_t out_buf_size)
|
|
||||||
{
|
|
||||||
if (!ctx) return ESP_ERR_INVALID_ARG;
|
|
||||||
memset(ctx, 0, sizeof(*ctx));
|
|
||||||
ctx->type = type;
|
|
||||||
|
|
||||||
if (type == OtaCompress::NONE) {
|
|
||||||
ESP_LOGI(TAG, "passthrough mode (no compression)");
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ① 先分配 tinfl 状态(~12KB),它不可压缩 */
|
|
||||||
size_t decomp_size = sizeof(tinfl_decompressor);
|
|
||||||
ctx->tinfl = (tinfl_decompressor *)calloc(1, decomp_size);
|
|
||||||
if (!ctx->tinfl) {
|
|
||||||
ESP_LOGE(TAG, "tinfl alloc failed: need %u B, free heap=%u",
|
|
||||||
(unsigned)decomp_size, (unsigned)esp_get_free_heap_size());
|
|
||||||
return ESP_ERR_NO_MEM;
|
|
||||||
}
|
|
||||||
tinfl_init((tinfl_decompressor *)ctx->tinfl);
|
|
||||||
|
|
||||||
/* ② 根据剩余堆自适应分配输出缓冲 */
|
|
||||||
size_t free_now = esp_get_free_heap_size();
|
|
||||||
size_t max_alloc = (free_now > HEAP_MARGIN) ? (free_now - HEAP_MARGIN) : 0;
|
|
||||||
size_t buf_size = out_buf_size;
|
|
||||||
if (buf_size > TINFL_LZ_DICT) buf_size = TINFL_LZ_DICT; /* 超过 32KB 没意义 */
|
|
||||||
if (buf_size > max_alloc) buf_size = max_alloc; /* 受限于可用堆 */
|
|
||||||
if (buf_size < TINFL_MIN_BUF) buf_size = TINFL_MIN_BUF; /* 最低保障 */
|
|
||||||
|
|
||||||
ctx->out_buf = (uint8_t *)malloc(buf_size);
|
|
||||||
if (!ctx->out_buf) {
|
|
||||||
ESP_LOGE(TAG, "out_buf alloc failed (%u B), free heap=%u",
|
|
||||||
(unsigned)buf_size, (unsigned)free_now);
|
|
||||||
free(ctx->tinfl);
|
|
||||||
ctx->tinfl = nullptr;
|
|
||||||
return ESP_ERR_NO_MEM;
|
|
||||||
}
|
|
||||||
ctx->out_buf_size = buf_size;
|
|
||||||
ctx->out_write_pos = 0;
|
|
||||||
ctx->finished = false;
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "zlib decompressor ready: tinfl=%u B, out_buf=%u B, free_heap=%u B",
|
|
||||||
(unsigned)decomp_size, (unsigned)buf_size,
|
|
||||||
(unsigned)esp_get_free_heap_size());
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t ota_decomp_process(OtaDecompCtx *ctx,
|
|
||||||
const uint8_t *chunk, size_t chunk_len,
|
|
||||||
ota_decomp_write_fn write_cb, void *user_arg,
|
|
||||||
size_t *out_bytes)
|
|
||||||
{
|
|
||||||
if (!ctx || !chunk || chunk_len == 0) return ESP_ERR_INVALID_ARG;
|
|
||||||
if (out_bytes) *out_bytes = 0;
|
|
||||||
|
|
||||||
/* ---- passthrough ---- */
|
|
||||||
if (ctx->type == OtaCompress::NONE) {
|
|
||||||
if (write_cb) {
|
|
||||||
esp_err_t err = write_cb(chunk, chunk_len, user_arg);
|
|
||||||
if (err != ESP_OK) return err;
|
|
||||||
}
|
|
||||||
ctx->total_in += chunk_len;
|
|
||||||
ctx->total_out += chunk_len;
|
|
||||||
if (out_bytes) *out_bytes = chunk_len;
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- ZLIB decompression ---- */
|
|
||||||
if (ctx->finished) {
|
|
||||||
ESP_LOGW(TAG, "decomp already finished, ignoring %u extra bytes", (unsigned)chunk_len);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t *in_ptr = chunk;
|
|
||||||
size_t in_left = chunk_len;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 非包装模式 (TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF):
|
|
||||||
* - pOut_buf_start = pOut_buf_next = out_buf + out_write_pos
|
|
||||||
* - tinfl 将缓冲视为线性空间,写满时返回 HAS_MORE_OUTPUT
|
|
||||||
* - 我们 flush 已写数据到 Flash,然后将写指针重置为 0 继续
|
|
||||||
* - 每次 flush 后旧数据从缓冲中消失;如果后续 deflate 流
|
|
||||||
* 引用了已 flush 的数据(back-reference > buf_size),会失败
|
|
||||||
* - buf_size 越大越安全,32KB 覆盖所有情况
|
|
||||||
*/
|
|
||||||
|
|
||||||
while (in_left > 0 && !ctx->finished) {
|
|
||||||
size_t in_bytes = in_left;
|
|
||||||
size_t out_space = ctx->out_buf_size - ctx->out_write_pos;
|
|
||||||
size_t out_written = out_space;
|
|
||||||
|
|
||||||
tinfl_status status = tinfl_decompress(
|
|
||||||
(tinfl_decompressor *)ctx->tinfl,
|
|
||||||
(const mz_uint8 *)in_ptr, &in_bytes,
|
|
||||||
ctx->out_buf + ctx->out_write_pos, /* pOut_buf_start */
|
|
||||||
ctx->out_buf + ctx->out_write_pos, /* pOut_buf_next */
|
|
||||||
&out_written,
|
|
||||||
TINFL_FLAG_HAS_MORE_INPUT |
|
|
||||||
TINFL_FLAG_PARSE_ZLIB_HEADER |
|
|
||||||
TINFL_FLAG_COMPUTE_ADLER32 |
|
|
||||||
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
|
|
||||||
|
|
||||||
/* advance input cursor */
|
|
||||||
in_ptr += in_bytes;
|
|
||||||
in_left -= in_bytes;
|
|
||||||
ctx->total_in += in_bytes;
|
|
||||||
|
|
||||||
/* flush newly decompressed data to flash */
|
|
||||||
if (out_written > 0 && write_cb) {
|
|
||||||
esp_err_t err = write_cb(ctx->out_buf + ctx->out_write_pos, out_written, user_arg);
|
|
||||||
if (err != ESP_OK) return err;
|
|
||||||
}
|
|
||||||
ctx->total_out += out_written;
|
|
||||||
if (out_bytes) *out_bytes += out_written;
|
|
||||||
|
|
||||||
/* advance write pointer; reset to 0 when segment is full */
|
|
||||||
ctx->out_write_pos += out_written;
|
|
||||||
if (ctx->out_write_pos >= ctx->out_buf_size) {
|
|
||||||
ctx->out_write_pos = 0; /* flush-complete: reset for next block */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* status dispatch */
|
|
||||||
if (status == TINFL_STATUS_DONE) {
|
|
||||||
ctx->finished = true;
|
|
||||||
ESP_LOGI(TAG, "decomp done: in=%u out=%u",
|
|
||||||
(unsigned)ctx->total_in, (unsigned)ctx->total_out);
|
|
||||||
} else if (status == TINFL_STATUS_HAS_MORE_OUTPUT) {
|
|
||||||
/* segment full → reset and continue */
|
|
||||||
ctx->out_write_pos = 0;
|
|
||||||
continue;
|
|
||||||
} else if (status == TINFL_STATUS_NEEDS_MORE_INPUT) {
|
|
||||||
/* consumed all input, wait for next chunk */
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "tinfl failed, status=%d, in_left=%u, total_in=%u",
|
|
||||||
(int)status, (unsigned)in_left, (unsigned)ctx->total_in);
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t ota_decomp_finish(OtaDecompCtx *ctx)
|
|
||||||
{
|
|
||||||
if (!ctx) return ESP_ERR_INVALID_ARG;
|
|
||||||
|
|
||||||
if (ctx->type == OtaCompress::NONE) {
|
|
||||||
ESP_LOGI(TAG, "passthrough done: %u bytes", (unsigned)ctx->total_out);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx->finished) {
|
|
||||||
ESP_LOGW(TAG, "zlib stream not finished, attempting final flush...");
|
|
||||||
|
|
||||||
size_t out_space = ctx->out_buf_size - ctx->out_write_pos;
|
|
||||||
size_t out_written = out_space;
|
|
||||||
tinfl_status status = tinfl_decompress(
|
|
||||||
(tinfl_decompressor *)ctx->tinfl,
|
|
||||||
nullptr, nullptr,
|
|
||||||
ctx->out_buf + ctx->out_write_pos,
|
|
||||||
ctx->out_buf + ctx->out_write_pos,
|
|
||||||
&out_written,
|
|
||||||
TINFL_FLAG_PARSE_ZLIB_HEADER |
|
|
||||||
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
|
|
||||||
|
|
||||||
if (out_written > 0) {
|
|
||||||
ctx->total_out += out_written;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status == TINFL_STATUS_DONE) {
|
|
||||||
ctx->finished = true;
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "zlib verify failed, status=%d", (int)status);
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "decomp verified: compressed=%u B -> decompressed=%u B (%.1f%%)",
|
|
||||||
(unsigned)ctx->total_in, (unsigned)ctx->total_out,
|
|
||||||
ctx->total_in > 0 ? (float)ctx->total_out * 100.0f / (float)ctx->total_in : 0.0f);
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ota_decomp_free(OtaDecompCtx *ctx)
|
|
||||||
{
|
|
||||||
if (!ctx) return;
|
|
||||||
if (ctx->tinfl) {
|
|
||||||
free(ctx->tinfl);
|
|
||||||
ctx->tinfl = nullptr;
|
|
||||||
}
|
|
||||||
if (ctx->out_buf) {
|
|
||||||
free(ctx->out_buf);
|
|
||||||
ctx->out_buf = nullptr;
|
|
||||||
}
|
|
||||||
ctx->type = OtaCompress::NONE;
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* ota_compressor.h - OTA 流式解压模块
|
|
||||||
*
|
|
||||||
* 支持 zlib/deflate 压缩固件的流式解压,边接收边解压边写 Flash。
|
|
||||||
* 采用 tinfl 非包装模式 + 自适应缓冲:满了就 flush 到 Flash 然后重置。
|
|
||||||
* tinfl 函数在 ESP32 ROM 中始终可用,此处仅声明原型,无需 esp_rom 组件依赖。
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include "esp_err.h"
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
* tinfl ROM API 声明(摘自 esp_rom/miniz.h,避免引入 esp_rom 依赖)
|
|
||||||
* ================================================================ */
|
|
||||||
|
|
||||||
typedef unsigned char mz_uint8;
|
|
||||||
typedef signed short mz_int16;
|
|
||||||
typedef unsigned int mz_uint32;
|
|
||||||
typedef unsigned int mz_uint;
|
|
||||||
|
|
||||||
#define MZ_MACRO_END while (0)
|
|
||||||
|
|
||||||
enum {
|
|
||||||
TINFL_FLAG_PARSE_ZLIB_HEADER = 1,
|
|
||||||
TINFL_FLAG_HAS_MORE_INPUT = 2,
|
|
||||||
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4,
|
|
||||||
TINFL_FLAG_COMPUTE_ADLER32 = 8
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TINFL_STATUS_BAD_PARAM = -3,
|
|
||||||
TINFL_STATUS_ADLER32_MISMATCH = -2,
|
|
||||||
TINFL_STATUS_FAILED = -1,
|
|
||||||
TINFL_STATUS_DONE = 0,
|
|
||||||
TINFL_STATUS_NEEDS_MORE_INPUT = 1,
|
|
||||||
TINFL_STATUS_HAS_MORE_OUTPUT = 2
|
|
||||||
} tinfl_status;
|
|
||||||
|
|
||||||
enum {
|
|
||||||
TINFL_MAX_HUFF_TABLES = 3,
|
|
||||||
TINFL_MAX_HUFF_SYMBOLS_0 = 288,
|
|
||||||
TINFL_MAX_HUFF_SYMBOLS_1 = 32,
|
|
||||||
TINFL_MAX_HUFF_SYMBOLS_2 = 19,
|
|
||||||
TINFL_FAST_LOOKUP_BITS = 10,
|
|
||||||
TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
mz_uint8 m_code_size[TINFL_MAX_HUFF_SYMBOLS_0];
|
|
||||||
mz_int16 m_look_up[TINFL_FAST_LOOKUP_SIZE];
|
|
||||||
mz_int16 m_tree[TINFL_MAX_HUFF_SYMBOLS_0 * 2];
|
|
||||||
} tinfl_huff_table;
|
|
||||||
|
|
||||||
/* ESP32-C3: 32-bit bitbuf */
|
|
||||||
typedef mz_uint32 tinfl_bit_buf_t;
|
|
||||||
|
|
||||||
struct tinfl_decompressor_tag {
|
|
||||||
mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32,
|
|
||||||
m_final, m_type, m_check_adler32, m_dist, m_counter,
|
|
||||||
m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES];
|
|
||||||
tinfl_bit_buf_t m_bit_buf;
|
|
||||||
size_t m_dist_from_out_buf_start;
|
|
||||||
tinfl_huff_table m_tables[TINFL_MAX_HUFF_TABLES];
|
|
||||||
mz_uint8 m_raw_header[4];
|
|
||||||
mz_uint8 m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137];
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct tinfl_decompressor_tag tinfl_decompressor;
|
|
||||||
|
|
||||||
#define tinfl_init(r) do { (r)->m_state = 0; } MZ_MACRO_END
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
tinfl_status tinfl_decompress(tinfl_decompressor *r,
|
|
||||||
const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size,
|
|
||||||
mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next,
|
|
||||||
size_t *pOut_buf_size, const mz_uint32 decomp_flags);
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
* OTA 解压上下文
|
|
||||||
* ================================================================ */
|
|
||||||
|
|
||||||
enum class OtaCompress : uint8_t {
|
|
||||||
NONE = 0,
|
|
||||||
ZLIB = 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct OtaDecompCtx {
|
|
||||||
OtaCompress type;
|
|
||||||
tinfl_decompressor *tinfl; // 仅 ZLIB 模式
|
|
||||||
uint8_t *out_buf;
|
|
||||||
size_t out_buf_size;
|
|
||||||
size_t out_write_pos;
|
|
||||||
size_t total_in;
|
|
||||||
size_t total_out;
|
|
||||||
bool finished;
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef esp_err_t (*ota_decomp_write_fn)(const uint8_t *data, size_t len, void *arg);
|
|
||||||
|
|
||||||
esp_err_t ota_decomp_init(OtaDecompCtx *ctx, OtaCompress type, size_t out_buf_size);
|
|
||||||
esp_err_t ota_decomp_process(OtaDecompCtx *ctx,
|
|
||||||
const uint8_t *chunk, size_t chunk_len,
|
|
||||||
ota_decomp_write_fn write_cb, void *user_arg,
|
|
||||||
size_t *out_bytes);
|
|
||||||
esp_err_t ota_decomp_finish(OtaDecompCtx *ctx);
|
|
||||||
void ota_decomp_free(OtaDecompCtx *ctx);
|
|
||||||
+47
-48
@@ -327,7 +327,7 @@ function updateStagesFromRelayState(rs) {
|
|||||||
if (rs >= 1 && rs <= 6) activeStage = 1;
|
if (rs >= 1 && rs <= 6) activeStage = 1;
|
||||||
else if (rs >= 7 && rs <= 8) activeStage = 2;
|
else if (rs >= 7 && rs <= 8) activeStage = 2;
|
||||||
else if (rs >= 9 && rs <= 15) activeStage = 3;
|
else if (rs >= 9 && rs <= 15) activeStage = 3;
|
||||||
else if (rs >= 16 && rs <= 17) activeStage = 4;
|
else if (rs >= 16 && rs <= 18) activeStage = 4;
|
||||||
|
|
||||||
for (var i = 0; i < activeStage - 1; i++) {
|
for (var i = 0; i < activeStage - 1; i++) {
|
||||||
all[i].className = 'stage-node done';
|
all[i].className = 'stage-node done';
|
||||||
@@ -350,8 +350,7 @@ function updateSystemStatusUi(data) {
|
|||||||
4: '收到握手响应', 5: '会话密钥已发', 6: '认证完成',
|
4: '收到握手响应', 5: '会话密钥已发', 6: '认证完成',
|
||||||
7: '大小已发', 8: '大小确认', 9: '接收中', 10: '转发中',
|
7: '大小已发', 8: '大小确认', 9: '接收中', 10: '转发中',
|
||||||
11: '数据包已发', 12: 'ACK正常', 13: '等待重发',
|
11: '数据包已发', 12: 'ACK正常', 13: '等待重发',
|
||||||
14: '接收完成', 15: '传输完成', 16: '结束已发', 17: 'OTA完成',
|
14: '接收完成', 15: '传输完成', 16: '结束已发', 17: 'OTA完成', 18: 'APP验证', 255: '错误'
|
||||||
255: '错误'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var connected = !!wifi.connected;
|
var connected = !!wifi.connected;
|
||||||
@@ -363,7 +362,30 @@ function updateSystemStatusUi(data) {
|
|||||||
// version
|
// version
|
||||||
if (data.version) {
|
if (data.version) {
|
||||||
document.getElementById('pillVer').textContent = 'v' + data.version;
|
document.getElementById('pillVer').textContent = 'v' + data.version;
|
||||||
document.getElementById('mVersion').textContent = 'v' + data.version;
|
document.getElementById('pillVer').title = data.build_time ? ('编译: ' + data.build_time) : '';
|
||||||
|
document.getElementById('mVersion').textContent = 'v' + data.version + (data.build_time ? ' (' + data.build_time + ')' : '');
|
||||||
|
|
||||||
|
// GD32 版本
|
||||||
|
var mGd32App = document.getElementById('mGd32App');
|
||||||
|
var mGd32Boot = document.getElementById('mGd32Boot');
|
||||||
|
if (mGd32App) {
|
||||||
|
if (ota.gd32_app && ota.gd32_app !== 'v0.0.0.0') {
|
||||||
|
mGd32App.textContent = ota.gd32_app;
|
||||||
|
mGd32App.className = 'v ok';
|
||||||
|
} else {
|
||||||
|
mGd32App.textContent = ota.gd32_app_alive ? '未知' : '离线';
|
||||||
|
mGd32App.className = 'v';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mGd32Boot) {
|
||||||
|
if (ota.gd32_boot && ota.gd32_boot !== 'v0.0.0.0') {
|
||||||
|
mGd32Boot.textContent = ota.gd32_boot;
|
||||||
|
mGd32Boot.className = 'v ok';
|
||||||
|
} else {
|
||||||
|
mGd32Boot.textContent = ota.gd32_app_alive ? '未知' : '离线';
|
||||||
|
mGd32Boot.className = 'v';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// temperature
|
// temperature
|
||||||
@@ -407,7 +429,7 @@ function updateSystemStatusUi(data) {
|
|||||||
var pr = document.getElementById('pillRelay');
|
var pr = document.getElementById('pillRelay');
|
||||||
if (rs === 255) pr.className = 'pill offline';
|
if (rs === 255) pr.className = 'pill offline';
|
||||||
else if (rs > 0 && rs < 17) pr.className = 'pill processing';
|
else if (rs > 0 && rs < 17) pr.className = 'pill processing';
|
||||||
else if (rs === 17) pr.className = 'pill online';
|
else if (rs === 17 || rs === 18) pr.className = 'pill online';
|
||||||
else pr.className = 'pill idle';
|
else pr.className = 'pill idle';
|
||||||
|
|
||||||
// OTA cache progress (GD32 upload to ESP32 flash)
|
// OTA cache progress (GD32 upload to ESP32 flash)
|
||||||
@@ -423,16 +445,30 @@ function updateSystemStatusUi(data) {
|
|||||||
var relayTotal = ota.relay_total_packets || 0;
|
var relayTotal = ota.relay_total_packets || 0;
|
||||||
var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0;
|
var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0;
|
||||||
|
|
||||||
|
var gd32AppAlive = ota.gd32_app_alive;
|
||||||
if (rs > 0 && rs < 17) {
|
if (rs > 0 && rs < 17) {
|
||||||
// Relay is active — show relay progress
|
// Relay is active — show relay progress
|
||||||
setActivePhase('gd32Relay');
|
setActivePhase('gd32Relay');
|
||||||
setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包');
|
setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包');
|
||||||
|
} else if (rs === 17) {
|
||||||
|
// Relay done, waiting for APP verify
|
||||||
|
setActivePhase('gd32Relay');
|
||||||
|
setProgress(100, '转发完成,等待 APP 启动...');
|
||||||
|
} else if (rs === 18) {
|
||||||
|
// APP verify phase
|
||||||
|
setActivePhase('gd32Relay');
|
||||||
|
if (gd32AppAlive) {
|
||||||
|
markPhaseDone('gd32Relay');
|
||||||
|
setProgress(100, 'GD32 刷写完成');
|
||||||
|
} else {
|
||||||
|
setProgress(100, '转发完成,验证 APP 存活...');
|
||||||
|
}
|
||||||
} else if (total > 0 && stored < total && !isUploading) {
|
} else if (total > 0 && stored < total && !isUploading) {
|
||||||
// GD32 upload to cache in progress (handled by upload progress if uploading)
|
// GD32 upload to cache in progress (handled by upload progress if uploading)
|
||||||
setActivePhase('gd32Upload');
|
setActivePhase('gd32Upload');
|
||||||
setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
|
setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
|
||||||
} else if (rs === 17) {
|
} else if (gd32AppAlive && rs === 0 && total > 0 && stored >= total) {
|
||||||
// Relay done
|
// Relay finished and reset, but APP is alive → truly done
|
||||||
markPhaseDone('gd32Relay');
|
markPhaseDone('gd32Relay');
|
||||||
setProgress(100, 'GD32 刷写完成');
|
setProgress(100, 'GD32 刷写完成');
|
||||||
}
|
}
|
||||||
@@ -836,29 +872,6 @@ function clearFile(chip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 上传固件 ====================
|
// ==================== 上传固件 ====================
|
||||||
// ==================== zlib compression ====================
|
|
||||||
async function compressZlib(arrayBuffer) {
|
|
||||||
var cs = new CompressionStream("deflate");
|
|
||||||
var writer = cs.writable.getWriter();
|
|
||||||
writer.write(new Uint8Array(arrayBuffer));
|
|
||||||
writer.close();
|
|
||||||
var chunks = [];
|
|
||||||
var reader = cs.readable.getReader();
|
|
||||||
for (;;) {
|
|
||||||
var r = await reader.read();
|
|
||||||
if (r.done) break;
|
|
||||||
chunks.push(r.value);
|
|
||||||
}
|
|
||||||
var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
|
|
||||||
var out = new Uint8Array(total);
|
|
||||||
var off = 0;
|
|
||||||
for (var j = 0; j < chunks.length; j++) {
|
|
||||||
out.set(chunks[j], off);
|
|
||||||
off += chunks[j].length;
|
|
||||||
}
|
|
||||||
return out.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startUpload(chip) {
|
async function startUpload(chip) {
|
||||||
var file = chip === 'esp32' ? esp32File : gd32File;
|
var file = chip === 'esp32' ? esp32File : gd32File;
|
||||||
if (!file || isUploading) return;
|
if (!file || isUploading) return;
|
||||||
@@ -913,20 +926,10 @@ async function startUpload(chip) {
|
|||||||
var fileSize = file.size;
|
var fileSize = file.size;
|
||||||
var body, url = getBaseUrl() + '/ota/upload';
|
var body, url = getBaseUrl() + '/ota/upload';
|
||||||
|
|
||||||
var useCompress = chip === 'esp32'
|
|
||||||
? document.getElementById('compressEsp32').checked
|
|
||||||
: document.getElementById('compressGd32').checked;
|
|
||||||
|
|
||||||
if (chip === 'esp32') {
|
if (chip === 'esp32') {
|
||||||
var headerObj = { type: 'esp32', total_size: fileSize };
|
var headerObj = { type: 'esp32', total_size: fileSize };
|
||||||
var payload = arrayBuffer;
|
var payload = arrayBuffer;
|
||||||
if (useCompress) {
|
addLog('I', '上传', '直传: ' + formatBytes(fileSize));
|
||||||
payload = await compressZlib(arrayBuffer);
|
|
||||||
headerObj.compress = 'zlib';
|
|
||||||
addLog('I', '压缩', 'zlib: ' + formatBytes(fileSize) + ' -> ' + formatBytes(payload.byteLength) + ' (' + Math.round(payload.byteLength * 100 / fileSize) + '%)');
|
|
||||||
} else {
|
|
||||||
addLog('I', '上传', '直传(无压缩): ' + formatBytes(fileSize));
|
|
||||||
}
|
|
||||||
var headerJson = JSON.stringify(headerObj);
|
var headerJson = JSON.stringify(headerObj);
|
||||||
var encoder = new TextEncoder();
|
var encoder = new TextEncoder();
|
||||||
var headerBytes = encoder.encode(headerJson);
|
var headerBytes = encoder.encode(headerJson);
|
||||||
@@ -937,13 +940,7 @@ async function startUpload(chip) {
|
|||||||
} else {
|
} else {
|
||||||
var headerObj = { total_size: fileSize };
|
var headerObj = { total_size: fileSize };
|
||||||
var payload = arrayBuffer;
|
var payload = arrayBuffer;
|
||||||
if (useCompress) {
|
addLog('I', '上传', '直传: ' + formatBytes(fileSize));
|
||||||
payload = await compressZlib(arrayBuffer);
|
|
||||||
headerObj.compress = 'zlib';
|
|
||||||
addLog('I', '压缩', 'zlib: ' + formatBytes(fileSize) + ' -> ' + formatBytes(payload.byteLength) + ' (' + Math.round(payload.byteLength * 100 / fileSize) + '%)');
|
|
||||||
} else {
|
|
||||||
addLog('I', '上传', '直传(无压缩): ' + formatBytes(fileSize));
|
|
||||||
}
|
|
||||||
var gd32Header = JSON.stringify(headerObj);
|
var gd32Header = JSON.stringify(headerObj);
|
||||||
var encoder = new TextEncoder();
|
var encoder = new TextEncoder();
|
||||||
var headerBytes = encoder.encode(gd32Header);
|
var headerBytes = encoder.encode(gd32Header);
|
||||||
@@ -1152,3 +1149,5 @@ setTimeout(function () {
|
|||||||
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
|
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
|
||||||
updateSpiHint();
|
updateSpiHint();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-6
@@ -69,8 +69,6 @@
|
|||||||
<div class="file-meta-item"><span>大小</span><b id="fileSizeEsp32">-</b></div>
|
<div class="file-meta-item"><span>大小</span><b id="fileSizeEsp32">-</b></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim);margin-top:8px;cursor:pointer;">
|
|
||||||
<input type="checkbox" id="compressEsp32"> zlib 压缩(弱网加速,CPU 开销大)
|
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-primary btn-block" id="uploadEsp32Btn" onclick="startUpload('esp32')" disabled style="margin-top:10px;">
|
<button class="btn btn-primary btn-block" id="uploadEsp32Btn" onclick="startUpload('esp32')" disabled style="margin-top:10px;">
|
||||||
上传并升级 ESP32
|
上传并升级 ESP32
|
||||||
@@ -98,8 +96,6 @@
|
|||||||
<div class="file-meta-item"><span>大小</span><b id="fileSizeGd32">-</b></div>
|
<div class="file-meta-item"><span>大小</span><b id="fileSizeGd32">-</b></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim);margin-bottom:4px;cursor:pointer;">
|
|
||||||
<input type="checkbox" id="compressGd32"> zlib 压缩(弱网加速,CPU 开销大)
|
|
||||||
</label>
|
</label>
|
||||||
<button class="btn btn-success btn-block" id="uploadGd32Btn" onclick="startUpload('gd32')" disabled style="margin-top:10px;margin-bottom:8px;">
|
<button class="btn btn-success btn-block" id="uploadGd32Btn" onclick="startUpload('gd32')" disabled style="margin-top:10px;margin-bottom:8px;">
|
||||||
上传固件到缓存
|
上传固件到缓存
|
||||||
@@ -169,7 +165,9 @@
|
|||||||
<div class="metric"><div class="k">信号强度</div><div class="v" id="mRSSI">--</div></div>
|
<div class="metric"><div class="k">信号强度</div><div class="v" id="mRSSI">--</div></div>
|
||||||
<div class="metric"><div class="k">空闲堆</div><div class="v" id="mHeap" style="font-size:16px;">--</div></div>
|
<div class="metric"><div class="k">空闲堆</div><div class="v" id="mHeap" style="font-size:16px;">--</div></div>
|
||||||
<div class="metric"><div class="k">最低空闲堆</div><div class="v" id="mMinHeap" style="font-size:16px;">--</div></div>
|
<div class="metric"><div class="k">最低空闲堆</div><div class="v" id="mMinHeap" style="font-size:16px;">--</div></div>
|
||||||
<div class="metric"><div class="k">固件版本</div><div class="v" id="mVersion" style="font-size:16px;">--</div></div>
|
<div class="metric"><div class="k">ESP32 版本</div><div class="v" id="mVersion" style="font-size:16px;">--</div></div>
|
||||||
|
<div class="metric"><div class="k">GD32 APP</div><div class="v" id="mGd32App" style="font-size:14px;">--</div></div>
|
||||||
|
<div class="metric"><div class="k">GD32 BOOT</div><div class="v" id="mGd32Boot" style="font-size:14px;">--</div></div>
|
||||||
<div class="metric"><div class="k">OTA 缓存</div><div class="v" id="mOtaCache" style="font-size:13px;">--</div></div>
|
<div class="metric"><div class="k">OTA 缓存</div><div class="v" id="mOtaCache" style="font-size:13px;">--</div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -273,6 +271,7 @@
|
|||||||
<span id="toastMsg">操作成功</span>
|
<span id="toastMsg">操作成功</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js?v=20260530f"></script>
|
<script src="app.js?v=20260603b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user