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:
@@ -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);
|
||||
Reference in New Issue
Block a user