diff --git a/CMakeLists.txt b/CMakeLists.txt index afd7768..e43450c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22) +cmake_minimum_required(VERSION 3.22) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(ESP32_COMM) diff --git a/cmake/gen_version.cmake b/cmake/gen_version.cmake new file mode 100644 index 0000000..3068ff0 --- /dev/null +++ b/cmake/gen_version.cmake @@ -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() diff --git a/cmake/gen_version_script.cmake b/cmake/gen_version_script.cmake new file mode 100644 index 0000000..e27ecfd --- /dev/null +++ b/cmake/gen_version_script.cmake @@ -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() diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 76b36f4..bfcc950 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -6,7 +6,6 @@ "transport/protocol.cpp" "server/wifi_ota.cpp" "server/ble_ota.cpp" - "utils/ota_compressor.cpp" "monitor/health_monitor.cpp" "monitor/debug_log.cpp" INCLUDE_DIRS @@ -34,4 +33,14 @@ freertos mbedtls 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 ) \ No newline at end of file diff --git a/main/include/app_config.h b/main/include/app_config.h index 333d8d6..7d6a054 100644 --- a/main/include/app_config.h +++ b/main/include/app_config.h @@ -3,14 +3,18 @@ * ESP32-C3 OTA 中继项目 * * 使用 constexpr 代替 #define,提供类型安全 + * 版本号和编译时间由 version.h 自动生成 */ #pragma once #include +#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 工作模式选择 @@ -54,21 +58,20 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; // OTA 配置 // ============================================================ 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 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_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间 // ============================================================ // 健康监控阈值 // ============================================================ inline constexpr int32_t WIFI_RSSI_CRITICAL = -85; inline constexpr int32_t WIFI_RSSI_WARNING = -75; -inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; // OTA 期间更宽松,避免瞬时波动误判 -inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; // OTA 最多接受 15 秒内的 RSSI 样本 -inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; // 每 32 个 48B 包做一次 OTA 健康检查 +inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; +inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; +inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; inline constexpr int32_t TEMP_WARNING = 80; inline constexpr int32_t TEMP_CRITICAL = 100; inline constexpr size_t HEAP_WARNING_MIN = 10240; @@ -76,8 +79,8 @@ inline constexpr size_t HEAP_WARNING_MIN = 10240; // ============================================================ // 调试配置(UDP 日志) // ============================================================ -inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志 -inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP +inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; +inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; 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_CMD_GD32_DATA = 0x00; 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_PACKET_OVERHEAD = 11; // cmd(1)+seq(2)+size(4)+crc(4) +inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; +inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; // ============================================================ // 安全常量(需与 GD32 固件团队确认) @@ -120,3 +123,4 @@ inline constexpr uint8_t GD32_CHIP_UID[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B }; + diff --git a/main/include/version.h b/main/include/version.h new file mode 100644 index 0000000..13fecce --- /dev/null +++ b/main/include/version.h @@ -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" diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 0a60060..abdc892 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -28,6 +28,7 @@ relay_state_t OtaRelay::relayState = RELAY_IDLE; uint32_t OtaRelay::relaySeq = 0; uint32_t OtaRelay::relayTotalPackets = 0; volatile bool OtaRelay::gd32AppAlive = false; +gd32_version_t OtaRelay::gd32Version = {}; QueueHandle_t OtaRelay::dispatchQueue = NULL; QueueHandle_t OtaRelay::resultQueue = NULL; @@ -870,10 +871,15 @@ ota_result_t OtaRelay::finishOta() for (uint32_t attempt = 0; attempt < 30; ++attempt) { vTaskDelay(pdMS_TO_TICKS(500)); 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) { 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 已启动) ==="); break; } @@ -1018,11 +1024,13 @@ void OtaRelay::taskEntry(void *pvParameters) if (result == OTA_RESULT_OK) { DEBUG_INFO("OTA", "OTA 转发成功完成"); - instance().reset(); + /* 保持 RELAY_DONE 状态 3 秒,让网页轮询有机会看到完成状态 */ + vTaskDelay(pdMS_TO_TICKS(3000)); } else { DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result); - instance().reset(); } + instance().reset(); } } + diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index 944f385..0689605 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -11,6 +11,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "transport/protocol.h" +#include "transport/spi_master.h" #include "esp_partition.h" #define OTA_META_NVS_NAMESPACE "ota_meta" @@ -80,6 +81,7 @@ public: static uint32_t relaySeq; static uint32_t relayTotalPackets; static volatile bool gd32AppAlive; + static gd32_version_t gd32Version; static QueueHandle_t dispatchQueue; static QueueHandle_t resultQueue; @@ -140,3 +142,5 @@ private: }; #endif // OTA_RELAY_H + + diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 9fd81ce..bb296d1 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -1,4 +1,4 @@ -/* +/* * wifi_ota.cpp - WiFi OTA 服务器实现 * WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。 * T4.1 - T4.5 @@ -28,7 +28,7 @@ #include "monitor/debug_log.h" #include "spi_master.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; } -/** - * @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 并重启 * @param req @@ -539,23 +521,6 @@ static int recv_json_header(httpd_req_t *req, char *buf, int buf_size) * @param req * @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) { 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; - OtaCompress compress_type = OtaCompress::NONE; uint32_t total_size = 0; int json_header_len = 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) { 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); } } @@ -628,13 +584,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) 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) 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); if (ret <= 0) break; - size_t decomp_out = 0; - Esp32DecompArg ea = { .written = 0 }; - err = ota_decomp_process(&decomp_ctx, fw_buf, (size_t)ret, - esp32_decomp_write_cb, &ea, &decomp_out); + err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, fw_buf, (size_t)ret); if (err != ESP_OK) { - DEBUG_ERR("WIFI", "OTA decompress/write failed"); + DEBUG_ERR("WIFI", "OTA write failed"); break; } - total_firmware_written += ea.written; + total_firmware_written += ret; total_received += ret; if (total_size > 0) { @@ -676,13 +622,6 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) } 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); const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}"; httpd_resp_set_type(req, "application/json"); @@ -692,11 +631,16 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) // --- GD32 OTA(原逻辑)--- const uint32_t gd32_fw_size = (uint32_t)req->content_len; - uint32_t gd32_decomp_size = (total_size > 0) ? total_size : gd32_fw_size; - const uint32_t gd32_valid_size = gd32_decomp_size; + // 网页上传协议:JSON 头 + raw binary。raw binary 大小优先取 JSON 中的 total_size,否则按 content_len - json_header_len 计算。 + 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); 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); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "firmware too large or alignment overflow"); return ESP_FAIL; @@ -753,15 +697,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) .checksum = {0} }; OtaRelay::metaSave(&meta_raw); - // init GD32 decompressor - 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 }; + uint32_t gd32_offset = 0; 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; if (first_chunk_len > req->content_len) first_chunk_len = req->content_len; if (first_chunk_len > 0) { - size_t first_out = 0; - gd32_da.offset = 0; - 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); + gd32_offset = 0; + esp_err_t err = OtaRelay::flashWriteChunk(gd32_offset, pre_read_buf, (size_t)first_chunk_len); 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)); free(buf); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed"); return ESP_FAIL; } 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 { received = pre_read_len; // 跳过已消费的 JSON 头字节 @@ -802,35 +737,26 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) return ESP_FAIL; } - size_t chunk_out = 0; - esp_err_t err = ota_decomp_process(&gd32_decomp, buf, (size_t)ret, - gd32_decomp_write_cb, &gd32_da, &chunk_out); + esp_err_t err = OtaRelay::flashWriteChunk(gd32_offset, buf, (size_t)ret); if (err != ESP_OK) { - DEBUG_ERR("WIFI", "GD32 decompress/write failed: offset=%u len=%d err=%s", - (unsigned)gd32_da.offset, ret, esp_err_to_name(err)); + DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s", + (unsigned)gd32_offset, ret, esp_err_to_name(err)); free(buf); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed"); return ESP_FAIL; } received += ret; + gd32_offset += (uint32_t)ret; } 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 - OtaRelay::metaUpdate(gd32_da.offset, (uint32_t)(gd32_da.offset / OTA_BLOCK_SIZE)); + // update meta with written 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; 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 && - 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"); 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", (unsigned long)meta.total_size, (unsigned long)meta.stored_size, 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_send(req, resp, len); @@ -1208,6 +1138,25 @@ static esp_err_t wifi_status_handler(httpd_req_t *req) 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 处理器 * @param req @@ -1234,14 +1183,33 @@ static esp_err_t system_status_handler(httpd_req_t *req) 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; memset(&meta, 0, sizeof(meta)); OtaRelay::metaLoad(&meta); - char resp[800]; + char resp[1024]; 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}}", - FW_VERSION,HealthMonitor::instance().getCurrent(), + "{\"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, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(), (unsigned long)esp_get_free_heap_size(), (unsigned long)esp_get_minimum_free_heap_size(), (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", OtaRelay::relayState, (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_send(req, resp, len); @@ -1493,8 +1466,23 @@ esp_err_t WifiOta::httpServerStart() .user_ctx = NULL }; 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; } @@ -1515,7 +1503,7 @@ void WifiOta::signalRelay() } 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); return; } @@ -1787,3 +1775,6 @@ void WifiOta::taskEntry(void *pvParameters) vTaskDelay(pdMS_TO_TICKS(1000)); } } + + + diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index b003116..ccbb022 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -189,7 +189,11 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len, if (i + total > buf_len) continue; 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 (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr; @@ -387,7 +391,7 @@ esp_err_t SpiMaster::appRequestBootloader() 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 size_t kAppPingPollBytes = 32U; @@ -412,7 +416,8 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec) continue; } - esp_rom_delay_us(200); + /* 给 GD32 充足时间处理 CS 释放中断并准备响应帧 */ + vTaskDelay(pdMS_TO_TICKS(3)); uint8_t rx_raw[kAppPingPollBytes]; uint8_t dummy[kAppPingPollBytes]; @@ -427,36 +432,74 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec) continue; } - uint8_t resp_type = 0U; - 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; - } + bool found = false; - DEBUG_DBG("SPI", "APP PING 收到无效帧[%lu]: type=0x%02X len=%u offset=%u payload0=0x%02X", - (unsigned long)attempt, - resp_type, - (unsigned)resp_len, - (unsigned)frame_offset, - resp_len > 0U ? resp_payload[0] : 0xFFU); - } else { - DEBUG_DBG("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]); + /* GD32 SPI 从机字节偏移不可预测,直接在 raw buffer 中 + * 搜索完整的标签化 PING 响应模式: + * 0xA1 [0x55...] app_vers [0xAA...] boot_vers + * 找到 0xA1 后跳过重复字节,验证 0x55 和 0xAA 标签。 + */ + for (size_t i = 0; i + 14 < sizeof(rx_raw); ++i) { + if (rx_raw[i] != kAppPingType) continue; /* 找 0xA1 */ + size_t p = i + 1; + /* 跳过最多 1 个重复的 A1 字节(SPI 偏移最多产生 1 个重复) */ + if (p < sizeof(rx_raw) && rx_raw[p] == kAppPingType) ++p; + 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)); } @@ -549,3 +592,5 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id, return 0; } + + diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index 7d62934..a84d6e5 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -53,6 +53,18 @@ typedef struct { uint8_t dlc; // 数据长度 (1~64) } 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 驱动类(单例) * @@ -97,7 +109,7 @@ public: esp_err_t appRequestBootloader(); /** @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 同步传输。 */ esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload, @@ -147,3 +159,5 @@ private: }; #endif // SPI_MASTER_H + + diff --git a/main/utils/ota_compressor.cpp b/main/utils/ota_compressor.cpp deleted file mode 100644 index 48bfc0a..0000000 --- a/main/utils/ota_compressor.cpp +++ /dev/null @@ -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 -#include -#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; -} \ No newline at end of file diff --git a/main/utils/ota_compressor.h b/main/utils/ota_compressor.h deleted file mode 100644 index dc01249..0000000 --- a/main/utils/ota_compressor.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * ota_compressor.h - OTA 流式解压模块 - * - * 支持 zlib/deflate 压缩固件的流式解压,边接收边解压边写 Flash。 - * 采用 tinfl 非包装模式 + 自适应缓冲:满了就 flush 到 Flash 然后重置。 - * tinfl 函数在 ESP32 ROM 中始终可用,此处仅声明原型,无需 esp_rom 组件依赖。 - */ -#pragma once - -#include -#include -#include -#include -#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); \ No newline at end of file diff --git a/tools/app.js b/tools/app.js index bbec8c9..8a3a857 100644 --- a/tools/app.js +++ b/tools/app.js @@ -327,7 +327,7 @@ function updateStagesFromRelayState(rs) { if (rs >= 1 && rs <= 6) activeStage = 1; else if (rs >= 7 && rs <= 8) activeStage = 2; 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++) { all[i].className = 'stage-node done'; @@ -350,8 +350,7 @@ function updateSystemStatusUi(data) { 4: '收到握手响应', 5: '会话密钥已发', 6: '认证完成', 7: '大小已发', 8: '大小确认', 9: '接收中', 10: '转发中', 11: '数据包已发', 12: 'ACK正常', 13: '等待重发', - 14: '接收完成', 15: '传输完成', 16: '结束已发', 17: 'OTA完成', - 255: '错误' + 14: '接收完成', 15: '传输完成', 16: '结束已发', 17: 'OTA完成', 18: 'APP验证', 255: '错误' }; var connected = !!wifi.connected; @@ -363,7 +362,30 @@ function updateSystemStatusUi(data) { // version if (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 @@ -407,7 +429,7 @@ function updateSystemStatusUi(data) { var pr = document.getElementById('pillRelay'); if (rs === 255) pr.className = 'pill offline'; 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'; // OTA cache progress (GD32 upload to ESP32 flash) @@ -423,16 +445,30 @@ function updateSystemStatusUi(data) { var relayTotal = ota.relay_total_packets || 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) { // Relay is active — show relay progress setActivePhase('gd32Relay'); 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) { // GD32 upload to cache in progress (handled by upload progress if uploading) setActivePhase('gd32Upload'); setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total)); - } else if (rs === 17) { - // Relay done + } else if (gd32AppAlive && rs === 0 && total > 0 && stored >= total) { + // Relay finished and reset, but APP is alive → truly done markPhaseDone('gd32Relay'); 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) { var file = chip === 'esp32' ? esp32File : gd32File; if (!file || isUploading) return; @@ -913,20 +926,10 @@ async function startUpload(chip) { var fileSize = file.size; var body, url = getBaseUrl() + '/ota/upload'; - var useCompress = chip === 'esp32' - ? document.getElementById('compressEsp32').checked - : document.getElementById('compressGd32').checked; - if (chip === 'esp32') { var headerObj = { type: 'esp32', total_size: fileSize }; var payload = arrayBuffer; - if (useCompress) { - 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)); - } + addLog('I', '上传', '直传: ' + formatBytes(fileSize)); var headerJson = JSON.stringify(headerObj); var encoder = new TextEncoder(); var headerBytes = encoder.encode(headerJson); @@ -937,13 +940,7 @@ async function startUpload(chip) { } else { var headerObj = { total_size: fileSize }; var payload = arrayBuffer; - if (useCompress) { - 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)); - } + addLog('I', '上传', '直传: ' + formatBytes(fileSize)); var gd32Header = JSON.stringify(headerObj); var encoder = new TextEncoder(); var headerBytes = encoder.encode(gd32Header); @@ -1152,3 +1149,5 @@ setTimeout(function () { addLog('I', '系统', '控制台初始化完成,等待固件文件...'); updateSpiHint(); }, 500); + + diff --git a/tools/index.html b/tools/index.html index ac38475..5bbe92b 100644 --- a/tools/index.html +++ b/tools/index.html @@ -69,8 +69,6 @@
大小-
-