Compare commits

...
3 Commits
Author SHA1 Message Date
小龙虾 d8e4222fec feat(ota): 添加 GD32 版本信息获取和 API 端点
- 实现了通过 SPI PING 命令获取 GD32 应用程序和引导加载程序版本信息
- 添加 /api/version HTTP 端点返回固件版本、编译时间和 Git 哈希
- 在系统状态 API 中包含 GD32 版本信息和存活状态
- 优化 SPI PING 协议解析以支持版本数据提取

refactor(ota): 移除压缩功能并简化 OTA 上传流程

- 移除 OTA 数据压缩和解压缩功能,简化数据处理流程
- 删除 utils/ota_compressor.cpp 文件及相关依赖
- 直接写入原始数据块而不是解压缩后写入
- 更新 OTA 上传处理器以处理原始二进制数据

build(cmakelists): 添加版本头文件自动生成机制

- 配置 CMake 构建时自动生成 version.h 头文件
- 集成 git 哈希和编译时间信息到固件版本
- 更新主 CMakeLists.txt 以包含版本生成目标
- 修改 app_config.h 以使用自动生成的版本字符串

fix(wifi_ota): 修复 OTA 中继状态管理和 SPI 通信问题

- 修复 OTA 中继完成后立即重置的问题,增加 3 秒延迟让前端轮询完成
- 修正 SPI CRC 错误检测和调试信息输出
- 优化 SPI PING 通信超时和重试机制
- 修复 OTA 上传大小计算和数据写入偏移跟踪
2026-06-03 03:14:01 +08:00
小龙虾 a59364e239 feat(ota): 完善OTA传输协议和错误处理机制
- 修改finish ACK验证逻辑,不再接受structured ACK 0xBAC0作为FINISH成功信号
- 移除appRequestBootloader函数的timeout参数,改为内部实现重试机制
- 新增probeBootHandshake函数用于检测GD32是否已处于bootloader模式
- 调整数据包传输中expected_ack值从0xFFFFFFFFU改为0xF0F0F0F0U
- 增加重试次数限制:poll轮询从5次增加到10次,重发次数从3次增加到10次
- 实现FINISH_OTA阶段的重试机制,支持WAIT帧处理和错误恢复
- 在startHandshake和forwardFirmware中添加bootloader探测和状态检查
- 优化APP存活确认失败时的错误处理,添加设备复位操作
- 统一各阶段错误处理逻辑,确保失败时正确清理状态并返回错误码
2026-06-02 03:25:02 +08:00
小龙虾 c4d78bdfbc 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
- 添加更多调试日志和错误检查
2026-05-31 19:09:11 +08:00
15 changed files with 631 additions and 199 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.22) cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ESP32_COMM) project(ESP32_COMM)
+54
View File
@@ -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()
+58
View File
@@ -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()
+11 -1
View File
@@ -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"
@@ -34,3 +34,13 @@ idf_component_register(
mbedtls mbedtls
bt bt
) )
# 每次构建时自动刷新 version.h(BUILD_TIME / GIT_HASH)
add_custom_target(version_header ALL
COMMAND ${CMAKE_COMMAND}
-DSOURCE_DIR=${CMAKE_SOURCE_DIR}
-DOUTPUT_FILE=${CMAKE_CURRENT_SOURCE_DIR}/include/version.h
-P ${CMAKE_SOURCE_DIR}/cmake/gen_version_script.cmake
COMMENT "Refreshing version.h"
VERBATIM
)
+2 -2
View File
@@ -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,
+17 -9
View File
@@ -3,14 +3,18 @@
* ESP32-C3 OTA 中继项目 * ESP32-C3 OTA 中继项目
* *
* 使用 constexpr 代替 #define,提供类型安全 * 使用 constexpr 代替 #define,提供类型安全
* 版本号和编译时间由 version.h 自动生成
*/ */
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include "version.h"
// ============================================================ // ============================================================
// 固件版本 // 固件版本(由 version.h 自动生成,此处提供 C++ constexpr 访问)
// ============================================================ // ============================================================
inline constexpr const char *FW_VERSION = "0.3.0"; inline constexpr const char *FW_VERSION = FW_VERSION_STRING;
inline constexpr const char *FW_BUILD_TIME = BUILD_TIME;
inline constexpr const char *FW_GIT_HASH = GIT_HASH;
// ============================================================ // ============================================================
// WiFi 工作模式选择 // WiFi 工作模式选择
@@ -56,15 +60,18 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
inline constexpr size_t OTA_BLOCK_SIZE = 8192; inline constexpr size_t OTA_BLOCK_SIZE = 8192;
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 处理时间
// ============================================================ // ============================================================
// 健康监控阈值 // 健康监控阈值
// ============================================================ // ============================================================
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85; inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
inline constexpr int32_t WIFI_RSSI_WARNING = -75; inline constexpr int32_t WIFI_RSSI_WARNING = -75;
inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; // OTA 期间更宽松,避免瞬时波动误判 inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88;
inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; // OTA 最多接受 15 秒内的 RSSI 样本 inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000;
inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; // 每 32 个 48B 包做一次 OTA 健康检查 inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32;
inline constexpr int32_t TEMP_WARNING = 80; inline constexpr int32_t TEMP_WARNING = 80;
inline constexpr int32_t TEMP_CRITICAL = 100; inline constexpr int32_t TEMP_CRITICAL = 100;
inline constexpr size_t HEAP_WARNING_MIN = 10240; inline constexpr size_t HEAP_WARNING_MIN = 10240;
@@ -72,8 +79,8 @@ inline constexpr size_t HEAP_WARNING_MIN = 10240;
// ============================================================ // ============================================================
// 调试配置(UDP 日志) // 调试配置(UDP 日志)
// ============================================================ // ============================================================
inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志 inline constexpr uint8_t DEBUG_LOG_ENABLED = 1;
inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3";
inline constexpr uint16_t DEBUG_LOG_PORT = 6666; inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
// ============================================================ // ============================================================
@@ -90,8 +97,8 @@ inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7;
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8; inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8;
inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00;
inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01;
inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; // 默认目标 payload;ATT Write Command 可写值≈(MTU-3),当前 512 MTU 下协议 payload 建议 ≤ 498B inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480;
inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; // cmd(1)+seq(2)+size(4)+crc(4) inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11;
// ============================================================ // ============================================================
// 安全常量(需与 GD32 固件团队确认) // 安全常量(需与 GD32 固件团队确认)
@@ -116,3 +123,4 @@ inline constexpr uint8_t GD32_CHIP_UID[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
}; };
+14
View File
@@ -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"
+123 -56
View File
@@ -28,6 +28,7 @@ relay_state_t OtaRelay::relayState = RELAY_IDLE;
uint32_t OtaRelay::relaySeq = 0; uint32_t OtaRelay::relaySeq = 0;
uint32_t OtaRelay::relayTotalPackets = 0; uint32_t OtaRelay::relayTotalPackets = 0;
volatile bool OtaRelay::gd32AppAlive = false; volatile bool OtaRelay::gd32AppAlive = false;
gd32_version_t OtaRelay::gd32Version = {};
QueueHandle_t OtaRelay::dispatchQueue = NULL; QueueHandle_t OtaRelay::dispatchQueue = NULL;
QueueHandle_t OtaRelay::resultQueue = NULL; QueueHandle_t OtaRelay::resultQueue = NULL;
@@ -123,8 +124,8 @@ bool OtaRelay::isSizeSeq0Ack(const uint8_t *payload, size_t len)
bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len) bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len)
{ {
if (!payload) return false; if (!payload) return false;
if (len >= 2U && payload[0] == 0x0AU && payload[1] == 0xACU) return true; if (len == 2U && payload[0] == 0x0AU && payload[1] == 0xACU) return true;
if (len >= 7U && payload[0] == 0xBAU && payload[1] == 0xC0U) return true; /* structured ACK 0xBAC0 no longer accepted as FINISH success */
return false; return false;
} }
@@ -343,10 +344,10 @@ 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();
if (err == ESP_OK) { if (err == ESP_OK) {
/* 收到确认:可能来自 APP(即将跳转 BOOT)或 BOOT 本身(已在 BOOT)。 /* 收到确认:可能来自 APP(即将跳转 BOOT)或 BOOT 本身(已在 BOOT)。
* 如果来自 APP,GD32 需要复位并重新初始化 SPI 外设,需等 ~1-2s; * 如果来自 APP,GD32 需要复位并重新初始化 SPI 外设,需等 ~1-2s;
@@ -354,9 +355,32 @@ 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::probeBootHandshake()
{
/* 0xB0 timeout: probe if GD32 is already in BOOT.
* Send one lightweight HANDSHAKE_REQ, accept only echo ACK 0x00 0xAC
* as "already in BOOT" signal; otherwise fail immediately. */
DEBUG_INFO("OTA", "0xB0 timeout, probing if GD32 already in BOOT...");
uint8_t payload[] = {0x00, 0xAC};
uint8_t resp_type = 0U;
uint8_t resp_payload[64] = {0};
size_t resp_len = 0U;
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_REQ, payload, sizeof(payload),
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 500);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_ACK && isHandshakeEchoAck(resp_payload, resp_len)) {
DEBUG_INFO("OTA", "BOOT probe success: GD32 already in BOOT, continue OTA");
return OTA_RESULT_OK;
}
DEBUG_ERR("OTA", "BOOT probe failed: GD32 not in BOOT, abort (err=%d resp_type=0x%02X)", err, resp_type);
return OTA_RESULT_ERR_TIMEOUT;
} }
ota_result_t OtaRelay::sendHandshakeReq() ota_result_t OtaRelay::sendHandshakeReq()
@@ -641,7 +665,7 @@ ota_result_t OtaRelay::relayPacketsFromFlash()
break; break;
} }
uint32_t expected_ack = (seq == total_packets - 1) ? 0xFFFFFFFFU : (seq + 1U); uint32_t expected_ack = (seq == total_packets - 1) ? 0xF0F0F0F0U : (seq + 1U);
/* 跳过上一阶段残留的、seq 不匹配的结构化 ACK。 /* 跳过上一阶段残留的、seq 不匹配的结构化 ACK。
* *
@@ -668,8 +692,8 @@ ota_result_t OtaRelay::relayPacketsFromFlash()
* 超时/非ACK 退出时需要重发当前包(GD32 Flash 缓冲满时会发 WAIT 后排空, * 超时/非ACK 退出时需要重发当前包(GD32 Flash 缓冲满时会发 WAIT 后排空,
* ESP32 必须重发才能让 GD32 重新处理)。 */ * ESP32 必须重发才能让 GD32 重新处理)。 */
bool got_expected = false; bool got_expected = false;
for (uint32_t retry = 0; retry < 5; ++retry) { for (uint32_t retry = 0; retry < 10; ++retry) {
DEBUG_WARN("OTA", "包 %u 收到非目标ACK(seq不匹配),继续轮询(%u/5)...", seq, retry + 1); DEBUG_DBG("OTA", "包 %u 收到非目标ACK(seq不匹配),继续轮询(%u/5)...", seq, retry + 1);
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000); err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) { if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5); err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
@@ -706,7 +730,7 @@ ota_result_t OtaRelay::relayPacketsFromFlash()
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) { if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code); DEBUG_WARN("OTA", "包 %u 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
relayState = RELAY_PACKET_ACK_RETRY; relayState = RELAY_PACKET_ACK_RETRY;
if (++retry_count >= 3U) { if (++retry_count >=10U) {
DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); DEBUG_ERR("OTA", "包 %u 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32; result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break; break;
@@ -717,7 +741,7 @@ ota_result_t OtaRelay::relayPacketsFromFlash()
/* 轮询耗尽仍未拿到期望 ACK -> 重发当前包 */ /* 轮询耗尽仍未拿到期望 ACK -> 重发当前包 */
DEBUG_WARN("OTA", "包 %u 轮询耗尽未匹配,重发 (retry=%u/3)", seq, retry_count + 1); DEBUG_WARN("OTA", "包 %u 轮询耗尽未匹配,重发 (retry=%u/3)", seq, retry_count + 1);
relayState = RELAY_PACKET_ACK_RETRY; relayState = RELAY_PACKET_ACK_RETRY;
if (++retry_count >= 3U) { if (++retry_count >= 10U) {
DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); DEBUG_ERR("OTA", "包 %u 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32; result = OTA_RESULT_ERR_GD32;
break; break;
@@ -755,45 +779,71 @@ ota_result_t OtaRelay::finishOta()
DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ==="); DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ===");
DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP"); DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP");
uint8_t finish_payload[2] = {0x0A, 0xAC};
uint8_t resp_type = 0; uint8_t resp_type = 0;
uint8_t resp_payload[32] = {0}; uint8_t resp_payload[32] = {0};
size_t resp_len = 0; size_t resp_len = 0;
uint8_t finish_payload[2] = {0x0A, 0xAC};
relayState = RELAY_FINISH_SENT; /* FINISH_OTA 重试机制:GD32 Flash 缓冲未排空时会回 WAIT,
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload), * 需要重试直到收到正确的 ACK(0x0A 0xAC) 或超时。 */
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); for (uint32_t retry = 0; retry < 10; ++retry) {
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len); resp_type = 0;
err = pollUntilExpectedAckShape("FINISH_OTA", &resp_type, resp_payload, resp_len = 0;
sizeof(resp_payload), &resp_len, memset(resp_payload, 0, sizeof(resp_payload));
1000, 10, 8, OtaRelay::isFinishPlainAck);
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || relayState = RELAY_FINISH_SENT;
!isFinishPlainAck(resp_payload, resp_len)) { esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, finish_payload, sizeof(finish_payload),
DEBUG_ERR("OTA", "FINISH_OTA 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("FINISH_OTA", resp_type, resp_payload, resp_len);
if (err != ESP_OK) {
DEBUG_WARN("OTA", "FINISH_OTA 传输失败, err=%d, retry=%u/10", err, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
err = pollUntilExpectedAckShape("FINISH_OTA", &resp_type, resp_payload,
sizeof(resp_payload), &resp_len,
1000, 10, 8, OtaRelay::isFinishPlainAck);
if (err != ESP_OK) {
DEBUG_WARN("OTA", "FINISH_OTA poll 失败, err=%d, retry=%u/10", err, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
if (resp_type == SPI_OTA_RSP_ACK && isFinishPlainAck(resp_payload, resp_len)) {
/* 成功收到 FINISH_OTA ACK */
break;
}
/* 检查是否为 WAIT 帧(GD32 Flash 缓冲未排空) */
if (resp_type == SPI_OTA_RSP_WAIT) {
DEBUG_INFO("OTA", "FINISH_OTA 收到 WAIT, GD32 正在排空 Flash 缓冲, retry=%u/10", retry + 1);
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
/* 检查是否为错误帧 */
uint32_t err_seq = 0;
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
DEBUG_ERR("OTA", "FINISH_OTA 收到错误帧 err=0x%02X, retry=%u/10", err_code, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
DEBUG_WARN("OTA", "FINISH_OTA 响应异常, resp_type=0x%02X, resp_len=%zu, retry=%u/10",
resp_type, resp_len, retry + 1);
vTaskDelay(pdMS_TO_TICKS(100));
}
if (resp_type != SPI_OTA_RSP_ACK || !isFinishPlainAck(resp_payload, resp_len)) {
DEBUG_ERR("OTA", "FINISH_OTA 失败, err=0, resp_type=0x%02X, resp_len=%zu", resp_type, resp_len);
relayState = RELAY_ERROR; relayState = RELAY_ERROR;
return OTA_RESULT_ERR_GD32; return OTA_RESULT_ERR_GD32;
} }
/* 检查是否为结构化错误帧(BOOT validateApplication 失败等) */ /* isFinishPlainAck already verified: only 0x0A 0xAC (len==2) passes.
uint32_t err_seq = 0; * Structured error frames (0xBA 0xC0 ...) are rejected above. */
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
DEBUG_ERR("OTA", "FINISH_OTA BOOT 返回错误: seq=0x%08lX err=0x%02X (%s)",
(unsigned long)err_seq, err_code,
err_code == 0x02U ? "FLASH_ERROR/validateApp失败" :
err_code == 0x04U ? "INVALID_PACKET" :
err_code == 0x03U ? "INVALID_KEY" :
"未知错误");
relayState = RELAY_ERROR;
return OTA_RESULT_ERR_GD32;
}
/* 正常明文 ACK: 0x0A 0xAC */
if (!isFinishPlainAck(resp_payload, resp_len)) {
DEBUG_ERR("OTA", "FINISH_OTA 响应格式异常: len=%zu payload[0]=0x%02X", resp_len, resp_payload[0]);
relayState = RELAY_ERROR;
return OTA_RESULT_ERR_GD32;
}
relayState = RELAY_DONE; relayState = RELAY_DONE;
Protocol::sessionKeyClear(&m_sessionCtx); Protocol::sessionKeyClear(&m_sessionCtx);
@@ -821,10 +871,15 @@ ota_result_t OtaRelay::finishOta()
for (uint32_t attempt = 0; attempt < 30; ++attempt) { for (uint32_t attempt = 0; attempt < 30; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(500)); vTaskDelay(pdMS_TO_TICKS(500));
uint32_t uptime = 0; uint32_t uptime = 0;
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime); gd32_version_t ping_ver = {};
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime, &ping_ver);
if (ping_err == ESP_OK) { if (ping_err == ESP_OK) {
gd32AppAlive = true; gd32AppAlive = true;
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec", (unsigned long)uptime); gd32Version = ping_ver;
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%lu sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
(unsigned long)uptime,
ping_ver.app_major, ping_ver.app_minor, ping_ver.app_patch, ping_ver.app_build,
ping_ver.boot_major, ping_ver.boot_minor, ping_ver.boot_patch, ping_ver.boot_build);
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ==="); DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
break; break;
} }
@@ -832,7 +887,9 @@ ota_result_t OtaRelay::finishOta()
} }
if (!gd32AppAlive) { if (!gd32AppAlive) {
DEBUG_WARN("OTA", "GD32 APP 存活确认超时,OTA 传输已完成但 APP 未启动"); DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
reset();
return OTA_RESULT_ERR_GD32;
} }
return OTA_RESULT_OK; return OTA_RESULT_OK;
@@ -846,21 +903,28 @@ ota_result_t OtaRelay::startHandshake()
{ {
ota_result_t res; ota_result_t res;
requestGd32AppEnterBootloader(); esp_err_t boot_err = requestGd32AppEnterBootloader();
if (boot_err != ESP_OK) {
res = probeBootHandshake();
if (res != OTA_RESULT_OK) {
reset();
return res;
}
}
res = sendHandshakeReq(); res = sendHandshakeReq();
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = sendNonceAndAuthenticate(); res = sendNonceAndAuthenticate();
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
ota_meta_t meta; ota_meta_t meta;
OtaRelay::metaLoad(&meta); OtaRelay::metaLoad(&meta);
res = sendFirmwareSize(meta.total_size); res = sendFirmwareSize(meta.total_size);
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = relayPacketsFromFlash(); res = relayPacketsFromFlash();
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = finishOta(); res = finishOta();
@@ -884,16 +948,16 @@ ota_result_t OtaRelay::forwardFirmware()
requestGd32AppEnterBootloader(); requestGd32AppEnterBootloader();
ota_result_t res = sendHandshakeReq(); ota_result_t res = sendHandshakeReq();
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = sendNonceAndAuthenticate(); res = sendNonceAndAuthenticate();
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = sendFirmwareSize(meta.total_size); res = sendFirmwareSize(meta.total_size);
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = relayPacketsFromFlash(); res = relayPacketsFromFlash();
if (res != OTA_RESULT_OK) return res; if (res != OTA_RESULT_OK) { reset(); return res; }
res = finishOta(); res = finishOta();
@@ -960,10 +1024,13 @@ void OtaRelay::taskEntry(void *pvParameters)
if (result == OTA_RESULT_OK) { if (result == OTA_RESULT_OK) {
DEBUG_INFO("OTA", "OTA 转发成功完成"); DEBUG_INFO("OTA", "OTA 转发成功完成");
instance().reset(); /* 保持 RELAY_DONE 状态 3 秒,让网页轮询有机会看到完成状态 */
vTaskDelay(pdMS_TO_TICKS(3000));
} else { } else {
DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result); DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
instance().reset();
} }
instance().reset();
} }
} }
+6 -2
View File
@@ -11,6 +11,7 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/queue.h" #include "freertos/queue.h"
#include "transport/protocol.h" #include "transport/protocol.h"
#include "transport/spi_master.h"
#include "esp_partition.h" #include "esp_partition.h"
#define OTA_META_NVS_NAMESPACE "ota_meta" #define OTA_META_NVS_NAMESPACE "ota_meta"
@@ -80,6 +81,7 @@ public:
static uint32_t relaySeq; static uint32_t relaySeq;
static uint32_t relayTotalPackets; static uint32_t relayTotalPackets;
static volatile bool gd32AppAlive; static volatile bool gd32AppAlive;
static gd32_version_t gd32Version;
static QueueHandle_t dispatchQueue; static QueueHandle_t dispatchQueue;
static QueueHandle_t resultQueue; static QueueHandle_t resultQueue;
@@ -93,6 +95,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 +134,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;
@@ -140,3 +142,5 @@ private:
}; };
#endif // OTA_RELAY_H #endif // OTA_RELAY_H
+125 -56
View File
@@ -30,6 +30,7 @@
#include "cJSON.h" #include "cJSON.h"
// ============================================================ // ============================================================
WifiOta& WifiOta::instance() WifiOta& WifiOta::instance()
{ {
@@ -401,24 +402,6 @@ static esp_err_t esp32_ota_begin(size_t total_size)
return ESP_OK; return ESP_OK;
} }
/**
* @brief 将接收到的数据块写入 ESP32 OTA 分区。
* @param data
* @param len
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
{
if (!WifiOta::instance().m_esp32OtaActive || WifiOta::instance().m_esp32UpdatePartition == NULL) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, data, len);
if (err == ESP_OK) {
WifiOta::instance().m_esp32Received += len;
}
return err;
}
/** /**
* @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启 * @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
* @param req * @param req
@@ -601,6 +584,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_FAIL; 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 +603,13 @@ 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); err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, fw_buf, (size_t)ret);
if (err != ESP_OK) { if (err != ESP_OK) {
DEBUG_ERR("WIFI", "OTA write 失败"); DEBUG_ERR("WIFI", "OTA write failed");
break; break;
} }
total_received += ret;
total_firmware_written += ret; total_firmware_written += ret;
total_received += 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);
@@ -647,14 +631,20 @@ 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); // 网页上传协议:JSON 头 + raw binary。raw binary 大小优先取 JSON 中的 total_size,否则按 content_len - json_header_len 计算。
if (gd32_fw_size == 0U || gd32_fw_size > OTA_MAX_FIRMWARE_SIZE || gd32_rounded_size > OTA_MAX_FIRMWARE_SIZE) { uint32_t gd32_valid_size;
DEBUG_ERR("WIFI", "固件大小非法: raw=%u rounded48=%u max=%u", if (json_header_len > 0 && (uint32_t)json_header_len < gd32_fw_size) {
(unsigned)gd32_fw_size, (unsigned)gd32_rounded_size, (unsigned)OTA_MAX_FIRMWARE_SIZE); gd32_valid_size = (total_size > 0 && total_size < gd32_fw_size) ? total_size : (gd32_fw_size - (uint32_t)json_header_len);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大或48B对齐后越界"); } 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: 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; 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 +659,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 +668,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 +688,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 +697,31 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
.checksum = {0} .checksum = {0}
}; };
OtaRelay::metaSave(&meta_raw); OtaRelay::metaSave(&meta_raw);
uint32_t gd32_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)); gd32_offset = 0;
free(buf); esp_err_t err = OtaRelay::flashWriteChunk(gd32_offset, pre_read_buf, (size_t)first_chunk_len);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed"); if (err != ESP_OK) {
return ESP_FAIL; 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;
gd32_offset += (uint32_t)first_chunk_len;
OtaRelay::metaUpdate(gd32_offset, (uint32_t)(gd32_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 +737,26 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_FAIL; return ESP_FAIL;
} }
uint32_t flash_offset = (uint32_t)received; esp_err_t err = OtaRelay::flashWriteChunk(gd32_offset, buf, (size_t)ret);
esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, buf, ret);
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 write failed: offset=%u len=%d err=%s",
flash_offset, ret, esp_err_to_name(err)); (unsigned)gd32_offset, ret, esp_err_to_name(err));
free(buf); free(buf);
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "write gd32_ota failed");
return ESP_FAIL; return ESP_FAIL;
} }
received += ret; received += ret;
OtaRelay::metaUpdate((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); gd32_offset += (uint32_t)ret;
} }
free(buf); free(buf);
DEBUG_INFO("WIFI", "GD32 raw upload done: %d / %u bytes", received, expected_fw_size_raw);
// 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", (unsigned)gd32_offset, expected_fw_size_raw);
ota_meta_t final_meta; ota_meta_t final_meta;
OtaRelay::metaLoad(&final_meta); OtaRelay::metaLoad(&final_meta);
@@ -791,7 +795,7 @@ static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
} }
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE && if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_ERROR) { OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_APP_VERIFY && OtaRelay::relayState != RELAY_ERROR) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "relay already triggered");
return ESP_FAIL; return ESP_FAIL;
} }
@@ -843,7 +847,11 @@ static esp_err_t ota_status_handler(httpd_req_t *req)
DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s", DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s, app_alive=%s",
(unsigned long)meta.total_size, (unsigned long)meta.stored_size, (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
WifiOta::instance().m_gd32StorageDone ? "true" : "false", WifiOta::instance().m_gd32StorageDone ? "true" : "false",
OtaRelay::gd32AppAlive ? "true" : "false"); OtaRelay::gd32AppAlive ? "true" : "false",
OtaRelay::gd32Version.app_major, OtaRelay::gd32Version.app_minor,
OtaRelay::gd32Version.app_patch, OtaRelay::gd32Version.app_build,
OtaRelay::gd32Version.boot_major, OtaRelay::gd32Version.boot_minor,
OtaRelay::gd32Version.boot_patch, OtaRelay::gd32Version.boot_build);
httpd_resp_set_type(req, "application/json"); httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len); httpd_resp_send(req, resp, len);
@@ -1130,6 +1138,25 @@ static esp_err_t wifi_status_handler(httpd_req_t *req)
return ESP_OK; return ESP_OK;
} }
/**
* @brief HTTP GET /api/version 处理器 - 返回固件版本、编译时间、Git hash
*/
static esp_err_t version_handler(httpd_req_t *req)
{
http_set_cors_headers(req);
gd32_version_t gv = OtaRelay::gd32Version;
char resp[512];
int len = snprintf(resp, sizeof(resp),
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", "
"\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\", \"gd32_alive\": %s}",
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH,
gv.app_major, gv.app_minor, gv.app_patch, gv.app_build,
gv.boot_major, gv.boot_minor, gv.boot_patch, gv.boot_build,
OtaRelay::gd32AppAlive ? "true" : "false");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
return ESP_OK;
}
/** /**
* @brief HTTP GET /system/status 处理器 * @brief HTTP GET /system/status 处理器
* @param req * @param req
@@ -1156,14 +1183,33 @@ static esp_err_t system_status_handler(httpd_req_t *req)
connected = true; connected = true;
} }
/* 若尚无 GD32 版本信息且 OTA 中继空闲,尝试一次 SPI PING 获取 */
if (OtaRelay::gd32Version.app_major == 0 &&
OtaRelay::gd32Version.boot_major == 0 &&
OtaRelay::relayState == RELAY_IDLE) {
static uint32_t last_ping_ms = 0;
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
if (now_ms - last_ping_ms > 5000) { /* 5 秒节流 */
last_ping_ms = now_ms;
uint32_t uptime = 0;
gd32_version_t ver = {};
if (SpiMaster::instance().appPing(500, &uptime, &ver) == ESP_OK) {
OtaRelay::gd32AppAlive = true;
OtaRelay::gd32Version = ver;
} else {
OtaRelay::gd32AppAlive = false;
}
}
}
ota_meta_t meta; ota_meta_t meta;
memset(&meta, 0, sizeof(meta)); memset(&meta, 0, sizeof(meta));
OtaRelay::metaLoad(&meta); OtaRelay::metaLoad(&meta);
char resp[800]; char resp[1024];
int len = snprintf(resp, sizeof(resp), int len = snprintf(resp, sizeof(resp),
"{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu}}", "{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, \"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}}",
FW_VERSION,HealthMonitor::instance().getCurrent(), FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
(unsigned long)esp_get_free_heap_size(), (unsigned long)esp_get_free_heap_size(),
(unsigned long)esp_get_minimum_free_heap_size(), (unsigned long)esp_get_minimum_free_heap_size(),
(double)HealthMonitor::instance().getLastTemperature(), (double)HealthMonitor::instance().getLastTemperature(),
@@ -1177,7 +1223,12 @@ static esp_err_t system_status_handler(httpd_req_t *req)
WifiOta::instance().m_gd32StorageDone ? "true" : "false", WifiOta::instance().m_gd32StorageDone ? "true" : "false",
OtaRelay::relayState, OtaRelay::relayState,
(unsigned long)OtaRelay::relaySeq, (unsigned long)OtaRelay::relaySeq,
(unsigned long)OtaRelay::relayTotalPackets); (unsigned long)OtaRelay::relayTotalPackets,
OtaRelay::gd32AppAlive ? "true" : "false",
OtaRelay::gd32Version.app_major, OtaRelay::gd32Version.app_minor,
OtaRelay::gd32Version.app_patch, OtaRelay::gd32Version.app_build,
OtaRelay::gd32Version.boot_major, OtaRelay::gd32Version.boot_minor,
OtaRelay::gd32Version.boot_patch, OtaRelay::gd32Version.boot_build);
httpd_resp_set_type(req, "application/json"); httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len); httpd_resp_send(req, resp, len);
@@ -1415,8 +1466,23 @@ esp_err_t WifiOta::httpServerStart()
.user_ctx = NULL .user_ctx = NULL
}; };
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri); httpd_register_uri_handler(WifiOta::instance().m_httpServer, &spi_send_options_uri);
// 版本信息端点
httpd_uri_t version_uri = {
.uri = "/api/version",
.method = HTTP_GET,
.handler = version_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &version_uri);
httpd_uri_t version_options_uri = {
.uri = "/api/version",
.method = HTTP_OPTIONS,
.handler = http_options_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &version_options_uri);
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status, /spi/send"); DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status, /spi/send, /api/version");
return ESP_OK; return ESP_OK;
} }
@@ -1437,7 +1503,7 @@ void WifiOta::signalRelay()
} }
if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE && if (WifiOta::instance().m_relayTriggered && OtaRelay::relayState != RELAY_IDLE &&
OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_ERROR) { OtaRelay::relayState != RELAY_DONE && OtaRelay::relayState != RELAY_APP_VERIFY && OtaRelay::relayState != RELAY_ERROR) {
DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", OtaRelay::relayState); DEBUG_WARN("WIFI", "转发正在执行中,状态: %d", OtaRelay::relayState);
return; return;
} }
@@ -1709,3 +1775,6 @@ void WifiOta::taskEntry(void *pvParameters)
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
} }
} }
+127 -51
View File
@@ -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);
@@ -183,7 +189,11 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
if (i + total > buf_len) continue; if (i + total > buf_len) continue;
uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen); uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen);
if (buf[i + 4 + plen] != expected_crc) continue; if (buf[i + 4 + plen] != expected_crc) {
DEBUG_WARN("SPI", "CRC mismatch at offset %u: plen=%u expected=0x%02X actual=0x%02X",
(unsigned)i, (unsigned)plen, expected_crc, buf[i + 4 + plen]);
continue;
}
if (type) *type = buf[i + 1]; if (type) *type = buf[i + 1];
if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr; if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr;
@@ -202,8 +212,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 +230,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 +242,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 +283,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 +318,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;
@@ -328,35 +353,45 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
esp_err_t SpiMaster::appRequestBootloader(uint32_t timeout_ms) esp_err_t SpiMaster::appRequestBootloader()
{ {
DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader..."); DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader...");
uint8_t cmd = SPI_APP_ENTER_BOOT_CMD; constexpr uint32_t kMaxRetries = 3U;
esp_err_t ret = transferClocked(&cmd, 1, NULL, 0); constexpr uint32_t kPerRetryAckMs = 200U;
if (ret != ESP_OK) { constexpr uint32_t kRetryGapMs = 50U;
DEBUG_WARN("SPI", "发送 Bootloader 请求失败: %s", esp_err_to_name(ret));
return ret;
}
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); for (uint32_t retry = 0U; retry < kMaxRetries; ++retry) {
if (retry > 0U) {
DEBUG_WARN("SPI", "Bootloader 请求重发 (%lu/%lu)", (unsigned long)retry, (unsigned long)kMaxRetries);
vTaskDelay(pdMS_TO_TICKS(kRetryGapMs));
}
while (xTaskGetTickCount() <= deadline) { uint8_t cmd = SPI_APP_ENTER_BOOT_CMD;
uint8_t rx = 0; esp_err_t ret = transferClocked(&cmd, 1, NULL, 0);
uint8_t dummy = 0xFF; if (ret != ESP_OK) {
ret = transferClocked(&dummy, 1, &rx, 1); DEBUG_WARN("SPI", "发送 Bootloader 请求失败: %s", esp_err_to_name(ret));
if (ret == ESP_OK && rx == SPI_APP_ENTER_BOOT_ACK_OK) { continue;
DEBUG_INFO("SPI", "GD32 APP 已确认进入 Bootloader"); }
return ESP_OK;
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(kPerRetryAckMs);
while (xTaskGetTickCount() <= deadline) {
uint8_t rx = 0;
uint8_t dummy = 0xFF;
ret = transferClocked(&dummy, 1, &rx, 1);
if (ret == ESP_OK && rx == SPI_APP_ENTER_BOOT_ACK_OK) {
DEBUG_INFO("SPI", "GD32 APP 已确认进入 Bootloader (retry=%lu)", (unsigned long)retry);
return ESP_OK;
}
vTaskDelay(pdMS_TO_TICKS(10));
} }
vTaskDelay(pdMS_TO_TICKS(10));
} }
DEBUG_WARN("SPI", "GD32 APP 进入 Bootloader 确认超时"); DEBUG_WARN("SPI", "GD32 APP 进入 Bootloader 确认超时 (retries exhausted)");
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec) esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
{ {
constexpr uint8_t kAppPingType = 0xA1U; constexpr uint8_t kAppPingType = 0xA1U;
constexpr size_t kAppPingPollBytes = 32U; constexpr size_t kAppPingPollBytes = 32U;
@@ -381,7 +416,8 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec)
continue; continue;
} }
esp_rom_delay_us(200); /* 给 GD32 充足时间处理 CS 释放中断并准备响应帧 */
vTaskDelay(pdMS_TO_TICKS(3));
uint8_t rx_raw[kAppPingPollBytes]; uint8_t rx_raw[kAppPingPollBytes];
uint8_t dummy[kAppPingPollBytes]; uint8_t dummy[kAppPingPollBytes];
@@ -396,36 +432,74 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec)
continue; continue;
} }
uint8_t resp_type = 0U; bool found = false;
const uint8_t *resp_payload = nullptr;
size_t resp_len = 0U;
size_t frame_offset = 0U;
if (otaFindFrame(rx_raw, sizeof(rx_raw),
&resp_type, &resp_payload, &resp_len, &frame_offset)) {
if (resp_type == SPI_OTA_RSP_ACK && resp_len >= 3U && resp_payload[0] == kAppPingType) {
if (uptime_sec) {
*uptime_sec = (uint32_t)resp_payload[1] | ((uint32_t)resp_payload[2] << 8);
}
DEBUG_INFO("SPI", "APP PING 成功: uptime=%lu sec, attempt=%lu, offset=%u",
uptime_sec ? (unsigned long)*uptime_sec : 0UL,
(unsigned long)attempt,
(unsigned)frame_offset);
return ESP_OK;
}
DEBUG_DBG("SPI", "APP PING 收到无效帧[%lu]: type=0x%02X len=%u offset=%u payload0=0x%02X", /* GD32 SPI 从机字节偏移不可预测,直接在 raw buffer 中
(unsigned long)attempt, * 搜索完整的标签化 PING 响应模式:
resp_type, * 0xA1 [0x55...] app_vers [0xAA...] boot_vers
(unsigned)resp_len, * 找到 0xA1 后跳过重复字节,验证 0x55 和 0xAA 标签。
(unsigned)frame_offset, */
resp_len > 0U ? resp_payload[0] : 0xFFU); for (size_t i = 0; i + 14 < sizeof(rx_raw); ++i) {
} else { if (rx_raw[i] != kAppPingType) continue; /* 找 0xA1 */
DEBUG_DBG("SPI", "APP PING 未找到有效帧[%lu]: first8=%02X%02X%02X%02X%02X%02X%02X%02X", size_t p = i + 1;
(unsigned long)attempt, /* 跳过最多 1 个重复的 A1 字节(SPI 偏移最多产生 1 个重复) */
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3], if (p < sizeof(rx_raw) && rx_raw[p] == kAppPingType) ++p;
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7]); if (p >= sizeof(rx_raw) || rx_raw[p] != 0x55U) continue;
/* 跳过最多 1 个重复的 0x55 */
if (p + 1 < sizeof(rx_raw) && rx_raw[p + 1] == 0x55U) ++p;
size_t app_start = p + 1; /* APP 版本起始 */
if (app_start + 4 > sizeof(rx_raw)) continue;
size_t bp = app_start + 4;
/* 跳过最多 1 个重复字节 */
if (bp < sizeof(rx_raw) && bp > 0 && rx_raw[bp] == rx_raw[bp - 1]) ++bp;
if (bp >= sizeof(rx_raw) || rx_raw[bp] != 0xAAU) continue;
/* 跳过最多 1 个重复的 0xAA */
if (bp + 1 < sizeof(rx_raw) && rx_raw[bp + 1] == 0xAAU) ++bp;
size_t boot_start = bp + 1;
if (boot_start + 4 > sizeof(rx_raw)) continue;
/* 模式匹配成功!提取版本数据 */
found = true;
if (version) {
memset(version, 0, sizeof(gd32_version_t));
version->app_major = rx_raw[app_start];
version->app_minor = rx_raw[app_start + 1];
version->app_patch = rx_raw[app_start + 2];
version->app_build = rx_raw[app_start + 3];
version->boot_major = rx_raw[boot_start];
version->boot_minor = rx_raw[boot_start + 1];
version->boot_patch = rx_raw[boot_start + 2];
version->boot_build = rx_raw[boot_start + 3];
}
if (uptime_sec) {
size_t ut = boot_start + 4;
if (ut + 3 < sizeof(rx_raw)) {
*uptime_sec = (uint32_t)rx_raw[ut]
| ((uint32_t)rx_raw[ut + 1] << 8)
| ((uint32_t)rx_raw[ut + 2] << 16)
| ((uint32_t)rx_raw[ut + 3] << 24);
} else {
*uptime_sec = 0;
}
}
DEBUG_WARN("SPI", "APP PING 标签匹配: A1@%u APP@%u BOOT@%u",
(unsigned)i, (unsigned)app_start, (unsigned)boot_start);
break;
} }
if (found) {
DEBUG_INFO("SPI", "APP PING 成功: uptime=%lu sec, attempt=%lu",
uptime_sec ? (unsigned long)*uptime_sec : 0UL,
(unsigned long)attempt);
return ESP_OK;
}
DEBUG_WARN("SPI", "APP PING 无匹配[%lu]: first8=%02X%02X%02X%02X%02X%02X%02X%02X",
(unsigned long)attempt,
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7]);
vTaskDelay(pdMS_TO_TICKS(5)); vTaskDelay(pdMS_TO_TICKS(5));
} }
@@ -518,3 +592,5 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
return 0; return 0;
} }
+16 -2
View File
@@ -53,6 +53,18 @@ typedef struct {
uint8_t dlc; // 数据长度 (1~64) uint8_t dlc; // 数据长度 (1~64)
} can_frame_t; } can_frame_t;
// GD32 版本信息(从 APP PING 响应解析)
typedef struct {
uint16_t app_major;
uint16_t app_minor;
uint16_t app_patch;
uint16_t app_build;
uint16_t boot_major;
uint16_t boot_minor;
uint16_t boot_patch;
uint16_t boot_build;
} gd32_version_t;
/** /**
* @brief SPI Master 驱动类(单例) * @brief SPI Master 驱动类(单例)
* *
@@ -94,10 +106,10 @@ public:
size_t resp_max, size_t *resp_len, uint32_t timeout_ms); size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
/** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */ /** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */
esp_err_t appRequestBootloader(uint32_t timeout_ms); esp_err_t appRequestBootloader();
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */ /** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec); esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version = nullptr);
/** @brief 执行带重试的 SPI 同步传输。 */ /** @brief 执行带重试的 SPI 同步传输。 */
esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload, esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload,
@@ -147,3 +159,5 @@ private:
}; };
#endif // SPI_MASTER_H #endif // SPI_MASTER_H
+5
View File
@@ -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
+61 -13
View File
@@ -327,7 +327,7 @@ function updateStagesFromRelayState(rs) {
if (rs >= 1 && rs <= 6) activeStage = 1; if (rs >= 1 && rs <= 6) activeStage = 1;
else if (rs >= 7 && rs <= 8) activeStage = 2; else if (rs >= 7 && rs <= 8) activeStage = 2;
else if (rs >= 9 && rs <= 15) activeStage = 3; else if (rs >= 9 && rs <= 15) activeStage = 3;
else if (rs >= 16 && rs <= 17) activeStage = 4; else if (rs >= 16 && rs <= 18) activeStage = 4;
for (var i = 0; i < activeStage - 1; i++) { for (var i = 0; i < activeStage - 1; i++) {
all[i].className = 'stage-node done'; all[i].className = 'stage-node done';
@@ -350,8 +350,7 @@ function updateSystemStatusUi(data) {
4: '收到握手响应', 5: '会话密钥已发', 6: '认证完成', 4: '收到握手响应', 5: '会话密钥已发', 6: '认证完成',
7: '大小已发', 8: '大小确认', 9: '接收中', 10: '转发中', 7: '大小已发', 8: '大小确认', 9: '接收中', 10: '转发中',
11: '数据包已发', 12: 'ACK正常', 13: '等待重发', 11: '数据包已发', 12: 'ACK正常', 13: '等待重发',
14: '接收完成', 15: '传输完成', 16: '结束已发', 17: 'OTA完成', 14: '接收完成', 15: '传输完成', 16: '结束已发', 17: 'OTA完成', 18: 'APP验证', 255: '错误'
255: '错误'
}; };
var connected = !!wifi.connected; var connected = !!wifi.connected;
@@ -363,7 +362,30 @@ function updateSystemStatusUi(data) {
// version // version
if (data.version) { if (data.version) {
document.getElementById('pillVer').textContent = 'v' + data.version; document.getElementById('pillVer').textContent = 'v' + data.version;
document.getElementById('mVersion').textContent = 'v' + data.version; document.getElementById('pillVer').title = data.build_time ? ('编译: ' + data.build_time) : '';
document.getElementById('mVersion').textContent = 'v' + data.version + (data.build_time ? ' (' + data.build_time + ')' : '');
// GD32 版本
var mGd32App = document.getElementById('mGd32App');
var mGd32Boot = document.getElementById('mGd32Boot');
if (mGd32App) {
if (ota.gd32_app && ota.gd32_app !== 'v0.0.0.0') {
mGd32App.textContent = ota.gd32_app;
mGd32App.className = 'v ok';
} else {
mGd32App.textContent = ota.gd32_app_alive ? '未知' : '离线';
mGd32App.className = 'v';
}
}
if (mGd32Boot) {
if (ota.gd32_boot && ota.gd32_boot !== 'v0.0.0.0') {
mGd32Boot.textContent = ota.gd32_boot;
mGd32Boot.className = 'v ok';
} else {
mGd32Boot.textContent = ota.gd32_app_alive ? '未知' : '离线';
mGd32Boot.className = 'v';
}
}
} }
// temperature // temperature
@@ -407,7 +429,7 @@ function updateSystemStatusUi(data) {
var pr = document.getElementById('pillRelay'); var pr = document.getElementById('pillRelay');
if (rs === 255) pr.className = 'pill offline'; if (rs === 255) pr.className = 'pill offline';
else if (rs > 0 && rs < 17) pr.className = 'pill processing'; else if (rs > 0 && rs < 17) pr.className = 'pill processing';
else if (rs === 17) pr.className = 'pill online'; else if (rs === 17 || rs === 18) pr.className = 'pill online';
else pr.className = 'pill idle'; else pr.className = 'pill idle';
// OTA cache progress (GD32 upload to ESP32 flash) // OTA cache progress (GD32 upload to ESP32 flash)
@@ -423,16 +445,30 @@ function updateSystemStatusUi(data) {
var relayTotal = ota.relay_total_packets || 0; var relayTotal = ota.relay_total_packets || 0;
var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0; var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0;
var gd32AppAlive = ota.gd32_app_alive;
if (rs > 0 && rs < 17) { if (rs > 0 && rs < 17) {
// Relay is active — show relay progress // Relay is active — show relay progress
setActivePhase('gd32Relay'); setActivePhase('gd32Relay');
setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包'); setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包');
} else if (rs === 17) {
// Relay done, waiting for APP verify
setActivePhase('gd32Relay');
setProgress(100, '转发完成,等待 APP 启动...');
} else if (rs === 18) {
// APP verify phase
setActivePhase('gd32Relay');
if (gd32AppAlive) {
markPhaseDone('gd32Relay');
setProgress(100, 'GD32 刷写完成');
} else {
setProgress(100, '转发完成,验证 APP 存活...');
}
} else if (total > 0 && stored < total && !isUploading) { } else if (total > 0 && stored < total && !isUploading) {
// GD32 upload to cache in progress (handled by upload progress if uploading) // GD32 upload to cache in progress (handled by upload progress if uploading)
setActivePhase('gd32Upload'); setActivePhase('gd32Upload');
setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total)); setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total));
} else if (rs === 17) { } else if (gd32AppAlive && rs === 0 && total > 0 && stored >= total) {
// Relay done // Relay finished and reset, but APP is alive → truly done
markPhaseDone('gd32Relay'); markPhaseDone('gd32Relay');
setProgress(100, 'GD32 刷写完成'); setProgress(100, 'GD32 刷写完成');
} }
@@ -891,18 +927,28 @@ async function startUpload(chip) {
var body, url = getBaseUrl() + '/ota/upload'; var body, url = getBaseUrl() + '/ota/upload';
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;
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;
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) {
@@ -1103,3 +1149,5 @@ setTimeout(function () {
addLog('I', '系统', '控制台初始化完成,等待固件文件...'); addLog('I', '系统', '控制台初始化完成,等待固件文件...');
updateSpiHint(); updateSpiHint();
}, 500); }, 500);
+7 -2
View File
@@ -69,6 +69,7 @@
<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>
<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 +96,7 @@
<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>
<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>
@@ -163,7 +165,9 @@
<div class="metric"><div class="k">信号强度</div><div class="v" id="mRSSI">--</div></div> <div class="metric"><div class="k">信号强度</div><div class="v" id="mRSSI">--</div></div>
<div class="metric"><div class="k">空闲堆</div><div class="v" id="mHeap" style="font-size:16px;">--</div></div> <div class="metric"><div class="k">空闲堆</div><div class="v" id="mHeap" style="font-size:16px;">--</div></div>
<div class="metric"><div class="k">最低空闲堆</div><div class="v" id="mMinHeap" style="font-size:16px;">--</div></div> <div class="metric"><div class="k">最低空闲堆</div><div class="v" id="mMinHeap" style="font-size:16px;">--</div></div>
<div class="metric"><div class="k">固件版本</div><div class="v" id="mVersion" style="font-size:16px;">--</div></div> <div class="metric"><div class="k">ESP32 版本</div><div class="v" id="mVersion" style="font-size:16px;">--</div></div>
<div class="metric"><div class="k">GD32 APP</div><div class="v" id="mGd32App" style="font-size:14px;">--</div></div>
<div class="metric"><div class="k">GD32 BOOT</div><div class="v" id="mGd32Boot" style="font-size:14px;">--</div></div>
<div class="metric"><div class="k">OTA 缓存</div><div class="v" id="mOtaCache" style="font-size:13px;">--</div></div> <div class="metric"><div class="k">OTA 缓存</div><div class="v" id="mOtaCache" style="font-size:13px;">--</div></div>
</div> </div>
</div> </div>
@@ -267,6 +271,7 @@
<span id="toastMsg">操作成功</span> <span id="toastMsg">操作成功</span>
</div> </div>
<script src="app.js?v=20260530f"></script> <script src="app.js?v=20260603b"></script>
</body> </body>
</html> </html>