diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 77cc109..76b36f4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,4 @@ -idf_component_register( +idf_component_register( SRCS "core/main.cpp" "relay/ota_relay.cpp" @@ -6,6 +6,7 @@ idf_component_register( "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 @@ -33,4 +34,4 @@ idf_component_register( freertos mbedtls bt -) +) \ No newline at end of file diff --git a/main/core/main.cpp b/main/core/main.cpp index d3c03f7..2c38bdf 100644 --- a/main/core/main.cpp +++ b/main/core/main.cpp @@ -1,4 +1,4 @@ -/* +/* * ESP32-C3FH4 OTA 中继 - 程序入口 * 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT * @@ -79,7 +79,7 @@ extern "C" void app_main(void) xRet = xTaskCreatePinnedToCore( OtaRelay::taskEntry, "ota_relay", - 8192, + 12288, NULL, configMAX_PRIORITIES - 2, NULL, @@ -128,4 +128,4 @@ extern "C" void app_main(void) DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)", DEBUG_LOG_IP, DEBUG_LOG_PORT); DEBUG_INFO("MAIN", "========================================"); -} \ No newline at end of file +} diff --git a/main/include/app_config.h b/main/include/app_config.h index b33aa5b..333d8d6 100644 --- a/main/include/app_config.h +++ b/main/include/app_config.h @@ -10,7 +10,7 @@ // ============================================================ // 固件版本 // ============================================================ -inline constexpr const char *FW_VERSION = "0.3.0"; +inline constexpr const char *FW_VERSION = "0.3.1"; // ============================================================ // WiFi 工作模式选择 @@ -54,8 +54,12 @@ 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 处理时间 // 每包重发次数上限 // ============================================================ // 健康监控阈值 diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 5e7d726..522f49b 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -343,7 +343,7 @@ esp_err_t OtaRelay::flashReadChunk(uint32_t offset, uint8_t *data, size_t len) * @brief 阶段1 - 发起与 GD32 的握手 * @return 操作结果码。 */ -void OtaRelay::requestGd32AppEnterBootloader() +esp_err_t OtaRelay::requestGd32AppEnterBootloader() { DEBUG_INFO("OTA", "尝试通知 GD32 APP 进入 Bootloader OTA 窗口"); esp_err_t err = SpiMaster::instance().appRequestBootloader(500); @@ -354,9 +354,10 @@ void OtaRelay::requestGd32AppEnterBootloader() * 等待 1000ms 给 GD32 足够时间完成系统复位和 Bootloader 初始化。 */ DEBUG_INFO("OTA", "GD32 已确认进入 Bootloader,等待就绪"); vTaskDelay(pdMS_TO_TICKS(1000)); - } else { - DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader,继续尝试直接握手: %s", esp_err_to_name(err)); + return ESP_OK; } + DEBUG_WARN("OTA", "GD32 APP 未确认进入 Bootloader: %s", esp_err_to_name(err)); + return err; } ota_result_t OtaRelay::sendHandshakeReq() diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index f99e4b2..944f385 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -93,6 +93,8 @@ public: static constexpr size_t GD32_UID_LEN = 12; static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36; + esp_err_t requestGd32AppEnterBootloader(); + ota_result_t probeBootHandshake(); ota_result_t sendHandshakeReq(); ota_result_t sendNonceAndAuthenticate(); 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 void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output); - void requestGd32AppEnterBootloader(); - private: OtaRelay() = default; ~OtaRelay() = default; @@ -139,4 +139,4 @@ private: OtaRelay& operator=(const OtaRelay&) = delete; }; -#endif // OTA_RELAY_H \ No newline at end of file +#endif // OTA_RELAY_H diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index d2913ee..9fd81ce 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,6 +28,7 @@ #include "monitor/debug_log.h" #include "spi_master.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 * @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); @@ -553,6 +571,7 @@ 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}; @@ -583,6 +602,14 @@ 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); } } @@ -601,6 +628,14 @@ 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); 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); 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) { - DEBUG_ERR("WIFI", "OTA write 失败"); + DEBUG_ERR("WIFI", "OTA decompress/write failed"); break; } + total_firmware_written += ea.written; total_received += ret; - total_firmware_written += ret; if (total_size > 0) { 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); + 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"); @@ -647,14 +692,15 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) // --- GD32 OTA(原逻辑)--- 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); - if (gd32_fw_size == 0U || gd32_fw_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) { - DEBUG_ERR("WIFI", "固件大小非法: raw=%u rounded48=%u max=%u", - (unsigned)gd32_fw_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE); - httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大或48B对齐后越界"); + uint32_t gd32_decomp_size = (total_size > 0) ? total_size : gd32_fw_size; + const uint32_t gd32_valid_size = gd32_decomp_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", + (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; } - if (OtaRelay::relayState != RELAY_IDLE) { DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::relayState); 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 分区"); 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) { DEBUG_ERR("WIFI", "gd32_ota 分区空间不足: erase=%u partition=%u", (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)", - (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); if (erase_err != ESP_OK) { 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; // 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 = { .total_size = expected_fw_size_raw, .stored_size = 0, @@ -707,21 +753,40 @@ 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 }; + DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw); - 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) { - esp_err_t err = OtaRelay::flashWriteChunk(0, pre_read_buf, first_chunk_len); - if (err != ESP_OK) { - 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; + // JSON 头已解析完毕,pre_read_buf 中的 JSON 字节不是固件数据,不能写入分区。 + // 仅在纯二进制模式(无 JSON 头)时写入 peek 的首字节。 + if (json_header_len == 0 && pre_read_len > 0) { + 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); + 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; - OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); + } else { + received = pre_read_len; // 跳过已消费的 JSON 头字节 } while (received < req->content_len) { @@ -737,22 +802,35 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) return ESP_FAIL; } - uint32_t flash_offset = (uint32_t)received; - esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, buf, ret); + 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); if (err != ESP_OK) { - DEBUG_ERR("WIFI", "GD32 flash write failed: offset=%u len=%d err=%s", - flash_offset, ret, esp_err_to_name(err)); + DEBUG_ERR("WIFI", "GD32 decompress/write failed: offset=%u len=%d err=%s", + (unsigned)gd32_da.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; - OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); } 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; OtaRelay::metaLoad(&final_meta); diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 8380eeb..b92bd2d 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -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)) { + 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; } @@ -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; 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) { memcpy(rx_buf, rx_ptr, rx_len); @@ -202,8 +208,8 @@ esp_err_t SpiMaster::init() DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ==="); spi_bus_config_t bus_cfg = {}; - bus_cfg.miso_io_num = 2; - bus_cfg.mosi_io_num = 7; + bus_cfg.miso_io_num = 7; + bus_cfg.mosi_io_num = 5; bus_cfg.sclk_io_num = 6; bus_cfg.quadwp_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.spics_io_num = SPI_MASTER_CS_GPIO; 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); if (ret != ESP_OK) { @@ -228,7 +238,7 @@ esp_err_t SpiMaster::init() 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); 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; } + 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); if (ret != ESP_OK) { 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; 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_payload && resp_max > 0) { size_t copy_len = (found_len < resp_max) ? found_len : resp_max; @@ -517,4 +538,4 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id, } return 0; -} \ No newline at end of file +} diff --git a/main/utils/ota_compressor.cpp b/main/utils/ota_compressor.cpp new file mode 100644 index 0000000..48bfc0a --- /dev/null +++ b/main/utils/ota_compressor.cpp @@ -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 +#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 new file mode 100644 index 0000000..dc01249 --- /dev/null +++ b/main/utils/ota_compressor.h @@ -0,0 +1,115 @@ +/* + * 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/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..33c03da --- /dev/null +++ b/sdkconfig.defaults @@ -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 diff --git a/tools/app.js b/tools/app.js index a424d43..bbec8c9 100644 --- a/tools/app.js +++ b/tools/app.js @@ -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) { var file = chip === 'esp32' ? esp32File : gd32File; if (!file || isUploading) return; @@ -890,19 +913,45 @@ 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 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 headerBytes = encoder.encode(headerJson); - var fileBytes = new Uint8Array(arrayBuffer); - var combined = new Uint8Array(headerBytes.length + fileSize); + var combined = new Uint8Array(headerBytes.length + payload.byteLength); combined.set(headerBytes, 0); - combined.set(fileBytes, headerBytes.length); + combined.set(new Uint8Array(payload), headerBytes.length); body = combined.buffer; } 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(); xhr.upload.addEventListener('progress', function (e) { if (e.lengthComputable) { diff --git a/tools/index.html b/tools/index.html index 711332c..ac38475 100644 --- a/tools/index.html +++ b/tools/index.html @@ -69,6 +69,9 @@
大小-
+ @@ -95,6 +98,9 @@
大小-
+