feat(ota): 临时版本,添加zlib压缩支持和SPI通信优化
添加了zlib压缩功能以减少固件传输时间,特别是在弱网环境下 优化了SPI通信参数和错误处理机制 BREAKING CHANGE: 固件版本从0.3.0升级到0.3.1,增加了压缩传输协议 - 在WiFi OTA中集成zlib压缩/解压缩功能 - 添加压缩选项UI界面和前端压缩逻辑 - 修复SPI引脚配置错误(MOSI/MISO接反) - 优化SPI通信参数设置增加CS时序控制 - 改进OTA重试机制和超时处理 - 扩展ota_relay组件栈大小从8K到12K - 添加解压缩回调函数和上下文管理 - 更新固件版本号到0.3.1 - 添加更多调试日志和错误检查
This commit is contained in:
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS
|
SRCS
|
||||||
"core/main.cpp"
|
"core/main.cpp"
|
||||||
"relay/ota_relay.cpp"
|
"relay/ota_relay.cpp"
|
||||||
@@ -6,6 +6,7 @@ idf_component_register(
|
|||||||
"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
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
* ESP32-C3FH4 OTA 中继 - 程序入口
|
* ESP32-C3FH4 OTA 中继 - 程序入口
|
||||||
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
|
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
|
||||||
*
|
*
|
||||||
@@ -79,7 +79,7 @@ extern "C" void app_main(void)
|
|||||||
xRet = xTaskCreatePinnedToCore(
|
xRet = xTaskCreatePinnedToCore(
|
||||||
OtaRelay::taskEntry,
|
OtaRelay::taskEntry,
|
||||||
"ota_relay",
|
"ota_relay",
|
||||||
8192,
|
12288,
|
||||||
NULL,
|
NULL,
|
||||||
configMAX_PRIORITIES - 2,
|
configMAX_PRIORITIES - 2,
|
||||||
NULL,
|
NULL,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// 固件版本
|
// 固件版本
|
||||||
// ============================================================
|
// ============================================================
|
||||||
inline constexpr const char *FW_VERSION = "0.3.0";
|
inline constexpr const char *FW_VERSION = "0.3.1";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// WiFi 工作模式选择
|
// WiFi 工作模式选择
|
||||||
@@ -54,8 +54,12 @@ 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_RESEND_RETRIES = 10; // 每包重发次数上限
|
||||||
|
inline constexpr uint32_t OTA_PACKET_DELAY_MS = 5; // 每包发送后延时(ms),给 GD32 处理时间 // 每包重发次数上限
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 健康监控阈值
|
// 健康监控阈值
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ esp_err_t OtaRelay::flashReadChunk(uint32_t offset, uint8_t *data, size_t len)
|
|||||||
* @brief 阶段1 - 发起与 GD32 的握手
|
* @brief 阶段1 - 发起与 GD32 的握手
|
||||||
* @return 操作结果码。
|
* @return 操作结果码。
|
||||||
*/
|
*/
|
||||||
void OtaRelay::requestGd32AppEnterBootloader()
|
esp_err_t OtaRelay::requestGd32AppEnterBootloader()
|
||||||
{
|
{
|
||||||
DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口");
|
DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口");
|
||||||
esp_err_t err = SpiMaster::instance().appRequestBootloader(500);
|
esp_err_t err = SpiMaster::instance().appRequestBootloader(500);
|
||||||
@@ -354,9 +354,10 @@ void OtaRelay::requestGd32AppEnterBootloader()
|
|||||||
* 等待 1000ms 给 GD32 足够时间完成系统复位和 Bootloader 初始化。 */
|
* 等待 1000ms 给 GD32 足够时间完成系统复位和 Bootloader 初始化。 */
|
||||||
DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪");
|
DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪");
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
} else {
|
return ESP_OK;
|
||||||
DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader,继续尝试直接握手: %s", esp_err_to_name(err));
|
|
||||||
}
|
}
|
||||||
|
DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
ota_result_t OtaRelay::sendHandshakeReq()
|
ota_result_t OtaRelay::sendHandshakeReq()
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ public:
|
|||||||
static constexpr size_t GD32_UID_LEN = 12;
|
static constexpr size_t GD32_UID_LEN = 12;
|
||||||
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
||||||
|
|
||||||
|
esp_err_t requestGd32AppEnterBootloader();
|
||||||
|
ota_result_t probeBootHandshake();
|
||||||
ota_result_t sendHandshakeReq();
|
ota_result_t sendHandshakeReq();
|
||||||
ota_result_t sendNonceAndAuthenticate();
|
ota_result_t sendNonceAndAuthenticate();
|
||||||
ota_result_t sendFirmwareSize(uint32_t size);
|
ota_result_t sendFirmwareSize(uint32_t size);
|
||||||
@@ -130,8 +132,6 @@ public:
|
|||||||
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||||
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
|
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
|
||||||
|
|
||||||
void requestGd32AppEnterBootloader();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OtaRelay() = default;
|
OtaRelay() = default;
|
||||||
~OtaRelay() = default;
|
~OtaRelay() = default;
|
||||||
|
|||||||
+109
-31
@@ -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,6 +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"
|
||||||
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -538,6 +539,23 @@ 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);
|
||||||
@@ -553,6 +571,7 @@ 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};
|
||||||
@@ -583,6 +602,14 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,6 +628,14 @@ 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);
|
||||||
if (!fw_buf) {
|
if (!fw_buf) {
|
||||||
@@ -619,13 +654,16 @@ 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;
|
||||||
|
|
||||||
err = esp32_ota_write(fw_buf, ret);
|
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);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "OTA write 失败");
|
DEBUG_ERR("WIFI", "OTA decompress/write failed");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
total_firmware_written += ea.written;
|
||||||
total_received += ret;
|
total_received += ret;
|
||||||
total_firmware_written += ret;
|
|
||||||
|
|
||||||
if (total_size > 0) {
|
if (total_size > 0) {
|
||||||
int percent = (int)((total_firmware_written * 100) / total_size);
|
int percent = (int)((total_firmware_written * 100) / total_size);
|
||||||
@@ -638,6 +676,13 @@ 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");
|
||||||
@@ -647,14 +692,15 @@ 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;
|
||||||
const uint32_t gd32_rounded_size = (uint32_t)(((gd32_fw_size + (uint32_t)GD32_OTA_PACKET_SIZE - 1U) / (uint32_t)GD32_OTA_PACKET_SIZE) * (uint32_t)GD32_OTA_PACKET_SIZE);
|
uint32_t gd32_decomp_size = (total_size > 0) ? total_size : gd32_fw_size;
|
||||||
if (gd32_fw_size == 0U || gd32_fw_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
const uint32_t gd32_valid_size = gd32_decomp_size;
|
||||||
DEBUG_ERR("WIFI", "固件大小非法: raw=%u rounded48=%u max=%u",
|
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);
|
||||||
(unsigned)gd32_fw_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE);
|
if (gd32_valid_size == 0U || gd32_valid_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) {
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大或48B对齐后越界");
|
DEBUG_ERR("WIFI", "firmware size invalid: decomp=%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;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OtaRelay::relayState != RELAY_IDLE) {
|
if (OtaRelay::relayState != RELAY_IDLE) {
|
||||||
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::relayState);
|
DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::relayState);
|
||||||
OtaRelay::instance().reset();
|
OtaRelay::instance().reset();
|
||||||
@@ -669,7 +715,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "找不到 gd32_ota 分区");
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "找不到 gd32_ota 分区");
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
const uint32_t gd32_erase_size = align_up_u32(gd32_fw_size, GD32_OTA_FLASH_SECTOR_SIZE);
|
const uint32_t gd32_erase_size = align_up_u32(gd32_valid_size, GD32_OTA_FLASH_SECTOR_SIZE);
|
||||||
if (gd32_erase_size > gd32_partition->size) {
|
if (gd32_erase_size > gd32_partition->size) {
|
||||||
DEBUG_ERR("WIFI", "gd32_ota 分区空间不足: erase=%u partition=%u",
|
DEBUG_ERR("WIFI", "gd32_ota 分区空间不足: erase=%u partition=%u",
|
||||||
(unsigned)gd32_erase_size, (unsigned)gd32_partition->size);
|
(unsigned)gd32_erase_size, (unsigned)gd32_partition->size);
|
||||||
@@ -678,7 +724,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("WIFI", "擦除 gd32_ota 分区: %u bytes (fw=%u, partition=%u)",
|
DEBUG_INFO("WIFI", "擦除 gd32_ota 分区: %u bytes (fw=%u, partition=%u)",
|
||||||
(unsigned)gd32_erase_size, (unsigned)gd32_fw_size, (unsigned)gd32_partition->size);
|
(unsigned)gd32_erase_size, (unsigned)gd32_valid_size, (unsigned)gd32_partition->size);
|
||||||
esp_err_t erase_err = esp_partition_erase_range(gd32_partition, 0, gd32_erase_size);
|
esp_err_t erase_err = esp_partition_erase_range(gd32_partition, 0, gd32_erase_size);
|
||||||
if (erase_err != ESP_OK) {
|
if (erase_err != ESP_OK) {
|
||||||
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err));
|
||||||
@@ -698,7 +744,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
int received = 0;
|
int received = 0;
|
||||||
|
|
||||||
// GD32 路径与网页上传协议对齐:网页上传的是 raw binary(不带 6B 头)。
|
// GD32 路径与网页上传协议对齐:网页上传的是 raw binary(不带 6B 头)。
|
||||||
uint32_t expected_fw_size_raw = gd32_fw_size;
|
uint32_t expected_fw_size_raw = gd32_valid_size;
|
||||||
ota_meta_t meta_raw = {
|
ota_meta_t meta_raw = {
|
||||||
.total_size = expected_fw_size_raw,
|
.total_size = expected_fw_size_raw,
|
||||||
.stored_size = 0,
|
.stored_size = 0,
|
||||||
@@ -707,21 +753,40 @@ 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
|
||||||
|
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);
|
||||||
|
|
||||||
int first_chunk_len = pre_read_len;
|
// JSON 头已解析完毕,pre_read_buf 中的 JSON 字节不是固件数据,不能写入分区。
|
||||||
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
// 仅在纯二进制模式(无 JSON 头)时写入 peek 的首字节。
|
||||||
if (first_chunk_len > 0) {
|
if (json_header_len == 0 && pre_read_len > 0) {
|
||||||
esp_err_t err = OtaRelay::flashWriteChunk(0, pre_read_buf, first_chunk_len);
|
int first_chunk_len = pre_read_len;
|
||||||
if (err != ESP_OK) {
|
if (first_chunk_len > req->content_len) first_chunk_len = req->content_len;
|
||||||
DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s",
|
if (first_chunk_len > 0) {
|
||||||
first_chunk_len, esp_err_to_name(err));
|
size_t first_out = 0;
|
||||||
free(buf);
|
gd32_da.offset = 0;
|
||||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
|
esp_err_t err = ota_decomp_process(&gd32_decomp, pre_read_buf, (size_t)first_chunk_len,
|
||||||
return ESP_FAIL;
|
gd32_decomp_write_cb, &gd32_da, &first_out);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
DEBUG_ERR("WIFI", "GD32 first chunk decompress/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));
|
||||||
}
|
}
|
||||||
received = first_chunk_len;
|
} else {
|
||||||
OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE));
|
received = pre_read_len; // 跳过已消费的 JSON 头字节
|
||||||
}
|
}
|
||||||
|
|
||||||
while (received < req->content_len) {
|
while (received < req->content_len) {
|
||||||
@@ -737,22 +802,35 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t flash_offset = (uint32_t)received;
|
size_t chunk_out = 0;
|
||||||
esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, buf, 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 flash write failed: offset=%u len=%d err=%s",
|
DEBUG_ERR("WIFI", "GD32 decompress/write failed: offset=%u len=%d err=%s",
|
||||||
flash_offset, ret, esp_err_to_name(err));
|
(unsigned)gd32_da.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;
|
||||||
OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
free(buf);
|
free(buf);
|
||||||
DEBUG_INFO("WIFI", "GD32 raw upload done: %d / %u bytes", received, expected_fw_size_raw);
|
// 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));
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG_INFO("WIFI", "GD32 upload done: %u / %u bytes (decompressed)", (unsigned)gd32_da.offset, expected_fw_size_raw);
|
||||||
|
|
||||||
ota_meta_t final_meta;
|
ota_meta_t final_meta;
|
||||||
OtaRelay::metaLoad(&final_meta);
|
OtaRelay::metaLoad(&final_meta);
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((tx_len > 0U && tx_buf == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
if ((tx_len > 0U && tx_buf == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
||||||
|
DEBUG_WARN("SPI", "transferClocked INVALID_ARG: tx_len=%u tx_buf=%p rx_len=%u rx_buf=%p dev=%p",
|
||||||
|
(unsigned)tx_len, tx_buf, (unsigned)rx_len, rx_buf, m_spiDevice);
|
||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +76,10 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
|||||||
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
|
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
|
||||||
|
|
||||||
esp_err_t ret = spi_device_transmit(m_spiDevice, &t);
|
esp_err_t ret = spi_device_transmit(m_spiDevice, &t);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
DEBUG_WARN("SPI", "spi_device_transmit fail: %s len=%u tx=%p rx=%p",
|
||||||
|
esp_err_to_name(ret), (unsigned)(total_len * 8U), t.tx_buffer, t.rx_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
if (ret == ESP_OK && rx_len > 0U && rx_ptr != rx_buf) {
|
if (ret == ESP_OK && rx_len > 0U && rx_ptr != rx_buf) {
|
||||||
memcpy(rx_buf, rx_ptr, rx_len);
|
memcpy(rx_buf, rx_ptr, rx_len);
|
||||||
@@ -202,8 +208,8 @@ esp_err_t SpiMaster::init()
|
|||||||
DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ===");
|
DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ===");
|
||||||
|
|
||||||
spi_bus_config_t bus_cfg = {};
|
spi_bus_config_t bus_cfg = {};
|
||||||
bus_cfg.miso_io_num = 2;
|
bus_cfg.miso_io_num = 7;
|
||||||
bus_cfg.mosi_io_num = 7;
|
bus_cfg.mosi_io_num = 5;
|
||||||
bus_cfg.sclk_io_num = 6;
|
bus_cfg.sclk_io_num = 6;
|
||||||
bus_cfg.quadwp_io_num = -1;
|
bus_cfg.quadwp_io_num = -1;
|
||||||
bus_cfg.quadhd_io_num = -1;
|
bus_cfg.quadhd_io_num = -1;
|
||||||
@@ -220,7 +226,11 @@ esp_err_t SpiMaster::init()
|
|||||||
dev_cfg.mode = SPI_MODE;
|
dev_cfg.mode = SPI_MODE;
|
||||||
dev_cfg.spics_io_num = SPI_MASTER_CS_GPIO;
|
dev_cfg.spics_io_num = SPI_MASTER_CS_GPIO;
|
||||||
dev_cfg.queue_size = SPI_QUEUE_SIZE;
|
dev_cfg.queue_size = SPI_QUEUE_SIZE;
|
||||||
dev_cfg.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
|
dev_cfg.command_bits = 0;
|
||||||
|
dev_cfg.address_bits = 0;
|
||||||
|
dev_cfg.dummy_bits = 0;
|
||||||
|
dev_cfg.cs_ena_posttrans = 10;
|
||||||
|
dev_cfg.cs_ena_pretrans = 20;
|
||||||
|
|
||||||
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &m_spiDevice);
|
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &m_spiDevice);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
@@ -228,7 +238,7 @@ esp_err_t SpiMaster::init()
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEBUG_INFO("SPI", "SPI2 总线初始化完成: CS=GPIO%d, CLK=%d Hz",
|
DEBUG_INFO("SPI", "SPI2 总线初始化完成: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO%d, CLK=%d Hz",
|
||||||
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
|
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
@@ -269,6 +279,8 @@ esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t pa
|
|||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u first4=%02X%02X%02X%02X",
|
||||||
|
type, (unsigned)tx_len, tx_frame[0], tx_frame[1], tx_frame[2], tx_frame[3]);
|
||||||
esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0);
|
esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0);
|
||||||
if (ret != ESP_OK) {
|
if (ret != ESP_OK) {
|
||||||
DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret));
|
DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret));
|
||||||
@@ -302,6 +314,15 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
|
|||||||
size_t frame_offset = 0;
|
size_t frame_offset = 0;
|
||||||
|
|
||||||
if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) {
|
if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) {
|
||||||
|
/* 有效性检查:ACK/ERROR 帧必须有 payload,空 payload 的 ACK 是帧截断/错位,
|
||||||
|
* 继续轮询而非当作有效响应返回。 */
|
||||||
|
if ((found_type == SPI_OTA_RSP_ACK || found_type == SPI_OTA_RSP_ERROR) && found_len == 0) {
|
||||||
|
DEBUG_DBG("SPI", "OTA 响应帧截断: type=0x%02X len=0 offset=%u, 继续轮询",
|
||||||
|
found_type, (unsigned)frame_offset);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (resp_type) *resp_type = found_type;
|
if (resp_type) *resp_type = found_type;
|
||||||
if (resp_payload && resp_max > 0) {
|
if (resp_payload && resp_max > 0) {
|
||||||
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
|
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# WiFi perf -- OTA acceleration
|
||||||
|
CONFIG_ESP_WIFI_RX_BA_WIN=6
|
||||||
|
CONFIG_ESP_WIFI_TX_BA_WIN=6
|
||||||
|
CONFIG_LWIP_TCP_WND_DEFAULT=11480
|
||||||
|
CONFIG_LWIP_TCP_SND_BUF_DEFAULT=11520
|
||||||
+55
-6
@@ -836,6 +836,29 @@ 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;
|
||||||
@@ -890,19 +913,45 @@ 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 headerJson = JSON.stringify({ type: 'esp32', total_size: fileSize });
|
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));
|
||||||
|
}
|
||||||
|
var headerJson = JSON.stringify(headerObj);
|
||||||
var encoder = new TextEncoder();
|
var encoder = new TextEncoder();
|
||||||
var headerBytes = encoder.encode(headerJson);
|
var headerBytes = encoder.encode(headerJson);
|
||||||
var fileBytes = new Uint8Array(arrayBuffer);
|
var combined = new Uint8Array(headerBytes.length + payload.byteLength);
|
||||||
var combined = new Uint8Array(headerBytes.length + fileSize);
|
|
||||||
combined.set(headerBytes, 0);
|
combined.set(headerBytes, 0);
|
||||||
combined.set(fileBytes, headerBytes.length);
|
combined.set(new Uint8Array(payload), headerBytes.length);
|
||||||
body = combined.buffer;
|
body = combined.buffer;
|
||||||
} else {
|
} else {
|
||||||
body = arrayBuffer;
|
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));
|
||||||
|
}
|
||||||
|
var gd32Header = JSON.stringify(headerObj);
|
||||||
|
var encoder = new TextEncoder();
|
||||||
|
var headerBytes = encoder.encode(gd32Header);
|
||||||
|
var combined = new Uint8Array(headerBytes.length + payload.byteLength);
|
||||||
|
combined.set(headerBytes, 0);
|
||||||
|
combined.set(new Uint8Array(payload), headerBytes.length);
|
||||||
|
body = combined.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.upload.addEventListener('progress', function (e) {
|
xhr.upload.addEventListener('progress', function (e) {
|
||||||
if (e.lengthComputable) {
|
if (e.lengthComputable) {
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
<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>
|
||||||
<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
|
||||||
</button>
|
</button>
|
||||||
@@ -95,6 +98,9 @@
|
|||||||
<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>
|
||||||
<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;">
|
||||||
上传固件到缓存
|
上传固件到缓存
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user