feat: 更新固件版本至 0.3.72 并实现构建时间自动生成
在 version.h 中将固件版本由 0.3.71 递增至 0.3.72。 移除 BUILD_TIME 的手动填写项,现由构建系统自动生成。 更新电机 OTA relay 逻辑,缩短状态检查的宽限期。 增强电机 OTA 上传与 relay 流程中的错误处理和日志记录。 为固件上传引入 SHA-256 校验,确保固件完整性。 调整前端命令值提交逻辑,改善用户体验。 更新 HTML 与 JavaScript 文件,以反映 OTA 流程及 UI 的变更。 新增 build_info 文件,用于管理构建时间的自动生成。
This commit is contained in:
@@ -3,6 +3,7 @@ idf_component_register(
|
||||
SRCS
|
||||
"core/main.cpp" # 程序入口 (app_main)
|
||||
"core/system_boot.cpp" # 启动模块位图与汇总
|
||||
"core/build_info.cpp" # 真实编译时间(每次构建自动刷新,见文末 gen_build_time)
|
||||
"ota/esp32_ota_core.cpp" # ESP32 本机 OTA 核心(分区写入/SHA验证)
|
||||
"ota/ota_coordinator.cpp" # OTA 多通道协调器(状态机/互斥)
|
||||
"relay/ota_relay.cpp" # GD32 SPI OTA 中继状态机
|
||||
@@ -49,3 +50,16 @@ idf_component_register(
|
||||
bt # NimBLE BLE 协议栈
|
||||
)
|
||||
|
||||
# ── 真实编译时间:每次构建重新生成 build_time_gen.h ──
|
||||
# 背景:version.h 的 BUILD_TIME 是手写死值(停在 2026-07-26 20:25),刷新固件后仍显示旧时间,
|
||||
# 被误判为 OTA 回退;__DATE__/__TIME__ 方案在增量编译下同样会过期。
|
||||
# 机制:gen_build_time 无产物依赖 → 每次构建必跑 → 重写头文件 → 仅 core/build_info.cpp 重编+重链。
|
||||
set(BUILD_TIME_HDR "${CMAKE_CURRENT_BINARY_DIR}/gen/build_time_gen.h")
|
||||
add_custom_target(gen_build_time
|
||||
COMMAND ${CMAKE_COMMAND} -D OUT=${BUILD_TIME_HDR} -P ${CMAKE_CURRENT_LIST_DIR}/gen_build_time.cmake
|
||||
BYPRODUCTS ${BUILD_TIME_HDR}
|
||||
COMMENT "Refreshing build_time_gen.h"
|
||||
)
|
||||
add_dependencies(${COMPONENT_LIB} gen_build_time)
|
||||
target_include_directories(${COMPONENT_LIB} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/gen")
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* build_info.cpp - 唯一包含自动生成时间头的编译单元。
|
||||
* build_time_gen.h 每次构建都被 gen_build_time 目标重写 → 仅本文件重编 + 重链接,
|
||||
* 其余增量编译不受影响;其他模块经 fwBuildTime() 取值。
|
||||
*/
|
||||
#include "build_info.h"
|
||||
#include "build_time_gen.h"
|
||||
|
||||
const char *fwBuildTime(void)
|
||||
{
|
||||
return FW_BUILD_TIME_AUTO;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# 每次构建执行:生成真实编译时间头文件(由 main/CMakeLists.txt 的 gen_build_time 目标调用)。
|
||||
# 手工维护的 version.h BUILD_TIME 曾停留在旧值,导致「刷了新固件仍显示旧时间」被误判为回退。
|
||||
string(TIMESTAMP _build_time "%Y-%m-%d %H:%M:%S")
|
||||
file(WRITE "${OUT}"
|
||||
"/* 自动生成(每次构建刷新),勿手改、勿提交。仅 core/build_info.cpp 包含本文件。 */
|
||||
#pragma once
|
||||
#define FW_BUILD_TIME_AUTO \"${_build_time}\"
|
||||
")
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstdint>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "version.h"
|
||||
#include "build_info.h"
|
||||
|
||||
// ============================================================
|
||||
// 功能开关(ESP32 本机;GD32 侧开关随 HELLO/PING 身份块上报,无需镜像)
|
||||
@@ -20,7 +21,8 @@ inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
|
||||
// 固件版本(在 version.h 中手动维护,此处提供 C++ constexpr 访问)
|
||||
// ============================================================
|
||||
inline constexpr const char *FW_VERSION = FW_VERSION_STRING; ///< 固件版本字符串(手动维护)
|
||||
inline constexpr const char *FW_BUILD_TIME = BUILD_TIME; ///< 编译时间字符串(手动维护)
|
||||
/* FW_BUILD_TIME 已删:手写死值停在旧时间,刷新固件仍显示 20:25 被误判回退。
|
||||
* 改用 fwBuildTime()(build_info.h,构建系统每次自动生成真实编译时间)。 */
|
||||
inline constexpr const char *FW_GIT_HASH = GIT_HASH; ///< Git 提交哈希(手动维护)
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* build_info.h - 真实编译时间(构建系统每次自动生成,替代 version.h 手工 BUILD_TIME)
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** 返回本固件的真实编译时间字符串 "YYYY-MM-DD HH:MM:SS"(每次构建自动刷新)。 */
|
||||
const char *fwBuildTime(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,22 +1,21 @@
|
||||
/*
|
||||
* version.h - 固件版本信息(手动维护)
|
||||
*
|
||||
* 注意:本文件不再由构建脚本自动生成。如需修改版本号、编译时间或
|
||||
* Git Hash,请直接编辑本文件。
|
||||
* 注意:本文件不再由构建脚本自动生成。如需修改版本号或 Git Hash,
|
||||
* 请直接编辑本文件。
|
||||
* - FW_VERSION_MAJOR / MINOR / PATCH:发版时手动递增
|
||||
* - BUILD_TIME:每次发版编译时手动填写
|
||||
* - GIT_HASH:手动填写对应提交的 git short hash
|
||||
* - 编译时间:已改为构建系统自动生成(build_info.h 的 fwBuildTime(),
|
||||
* 每次构建刷新),手写 BUILD_TIME 已删——旧值停在 2026-07-26 20:25
|
||||
* 曾被误判为 OTA 回退。
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 71
|
||||
#define FW_VERSION_STRING "0.3.71"
|
||||
|
||||
// 编译时间(手动填写)
|
||||
#define BUILD_TIME "2026-07-26 20:25:00"
|
||||
#define FW_VERSION_PATCH 72
|
||||
#define FW_VERSION_STRING "0.3.72"
|
||||
|
||||
// Git 信息(手动填写)
|
||||
#define GIT_HASH "770ae14"
|
||||
@@ -51,12 +51,12 @@ static void feedTask(void *arg)
|
||||
* 导致 motorOtaIsActive() 一直为 true、阻塞其他 OTA。 */
|
||||
const int64_t deadline_ms = nowMs() + 120000LL;
|
||||
|
||||
/* 启动宽限期:GD32 需要时间完成 kPreCancel(800ms 残帧窗口) + 握手(~1s)。
|
||||
* 在这个窗口内不检查终态,避免上一次 OTA 残留的 ERROR 状态推送
|
||||
* (0xD2 ring) 抢先覆写 s_status 导致 feedTask 误判退出。
|
||||
* 原 3s 过保守:GD32 进握手后不再发 ERROR push,1500ms 足够覆盖
|
||||
* kPreCancel + 开始握手(kBootSettleMs 已降到 800ms)。 */
|
||||
const int64_t grace_end_ms = nowMs() + 1500LL;
|
||||
/* 启动宽限期:只需覆盖“suppress 生效前在途的陈旧 0xD2 push”这一极小窗口——
|
||||
* s_status 刚被 motorOtaStart 置为 HANDSHAKE_REQ,下方 suppress 后 ring 不再读,
|
||||
* 残留 ERROR 只可能在这几十 ms 内覆写。原 1500ms 基于 kBootSettleMs=800ms 时代,
|
||||
* GD32 v1.5.1 已移除残帧窗口(收 0x103 即进握手,握手后不再发 ERROR push),
|
||||
* 100ms 足够(用户指定)。 */
|
||||
const int64_t grace_end_ms = nowMs() + 100LL;
|
||||
|
||||
/* 重新启用 suppressRingService:ring service 抢占 SPI 带宽导致吐量从 ~5 chunks/s 降到 ~1.5 chunks/s。
|
||||
* 进度显示由 feedTask 自己基于 s_fw_offset 更新(近似值),不依赖 GD32 push。 */
|
||||
@@ -77,9 +77,8 @@ static void feedTask(void *arg)
|
||||
}
|
||||
|
||||
/* GD32 已进终态:停止喂,避免盲重试空转到 120s 超时。
|
||||
* 启动宽限期(3s)内跳过终态检查:避免上一次 OTA 残留的 ERROR 状态推送
|
||||
* (0xD2 ring 延迟到达) 覆写 s_status 导致 feedTask 误判退出发 CANCEL。
|
||||
* GD32 需 kPreCancel(500ms 沉淀)+握手(~1s) 才能进入传输态。 */
|
||||
* 启动宽限期(100ms)内跳过终态检查:避免上一次 OTA 残留的 ERROR 状态推送
|
||||
* (0xD2 ring 延迟到达) 覆写 s_status 导致 feedTask 误判退出发 CANCEL。 */
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (nowMs() >= grace_end_ms) {
|
||||
if (st.state == MOTOR_OTA_IDLE || st.state == MOTOR_OTA_FINISHED ||
|
||||
|
||||
@@ -36,7 +36,8 @@ typedef enum {
|
||||
MOTOR_OTA_FINISH_OTA, /* 收到 0xF0F0F0F0,已发 FINISH_OTA,等跳转 ACK */
|
||||
MOTOR_OTA_FINISHED,
|
||||
MOTOR_OTA_ERROR,
|
||||
MOTOR_OTA_PRE_CANCEL = 9 /* 握手前预取消:发 CANCEL 清理电机残留会话(GD32 侧过渡态) */
|
||||
MOTOR_OTA_PRE_CANCEL = 9, /* 握手前预取消:发 CANCEL 清理电机残留会话(GD32 侧过渡态) */
|
||||
MOTOR_OTA_WAIT_APP_RUN = 10 /* 尾包 ACK 后等新 APP 自主发 [0A AC] 运行确认,收到才 FINISHED */
|
||||
} motor_ota_state_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -287,7 +287,7 @@ int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
const char *version = FW_VERSION;
|
||||
// 版本字符串: "vX.Y.Z YYYY-MM-DD HH:MM:SS"
|
||||
char ver_buf[64];
|
||||
int ver_len = snprintf(ver_buf, sizeof(ver_buf), "%s %s", FW_VERSION, FW_BUILD_TIME);
|
||||
int ver_len = snprintf(ver_buf, sizeof(ver_buf), "%s %s", FW_VERSION, fwBuildTime());
|
||||
if (ver_len < 0 || ver_len >= (int)sizeof(ver_buf)) {
|
||||
ver_len = strlen(version);
|
||||
int rc = os_mbuf_append(ctxt->om, version, ver_len);
|
||||
|
||||
@@ -1566,7 +1566,7 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
"\"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"channel\": %d, \"channel_name\": \"%s\", "
|
||||
"\"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, "
|
||||
"\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}, ",
|
||||
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
|
||||
FW_VERSION, fwBuildTime(), FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
|
||||
(unsigned long)free_heap,
|
||||
(unsigned long)min_free_heap,
|
||||
(unsigned long)largest_free_block,
|
||||
@@ -2157,12 +2157,23 @@ esp_err_t motor_ota_upload_handler(httpd_req_t *req)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* ★ 把设备端 flash SHA 直接回给前端:前端在发进 BOOT 帧之前就能比对,
|
||||
* flash 写坏时电机还在 APP 正常运行即可中止,不用等到 relay 阶段
|
||||
* (那时电机已被踢进 BOOT 白等)。relay 的三方比对仍保留作兜底。 */
|
||||
ota_meta_t sha_meta = {};
|
||||
char sha_hex[65] = {0};
|
||||
if (Gd32FlashStore::instance().metaLoad(&sha_meta) == ESP_OK) {
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&sha_hex[i * 2], 3, "%02x", sha_meta.checksum[i]);
|
||||
}
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
char resp_buf[160];
|
||||
char resp_buf[256];
|
||||
snprintf(resp_buf, sizeof(resp_buf),
|
||||
"{\"status\":\"stored\",\"message\":\"call POST /motor/ota/relay?ch=%u to trigger\","
|
||||
"\"channel\":%u,\"size\":%d}",
|
||||
(unsigned)channel, (unsigned)channel, received);
|
||||
"\"channel\":%u,\"size\":%d,\"sha256\":\"%s\"}",
|
||||
(unsigned)channel, (unsigned)channel, received, sha_hex);
|
||||
httpd_resp_sendstr(req, resp_buf);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -2182,70 +2193,29 @@ esp_err_t motor_ota_relay_handler(httpd_req_t *req)
|
||||
}
|
||||
}
|
||||
|
||||
/* 确认固件已完整存储 */
|
||||
/* 读 meta 拿 fw_size(motorOtaStart 参数的唯一来源)。
|
||||
* total_size==0 = 无固件可刷(未上传过,或上次 relay 成功后 metaClear 已消费)。
|
||||
* 数量完整性由上传流程保证:上传开头 clearGd32Meta,收满 content_len 才 metaSave,
|
||||
* 中断则 meta 保持空——因此这里无需再查 stored_size。 */
|
||||
ota_meta_t meta = {};
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK ||
|
||||
meta.total_size == 0 || meta.stored_size < meta.total_size) {
|
||||
if (Gd32FlashStore::instance().metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"firmware_not_stored", "firmware not fully stored");
|
||||
"firmware_not_stored", "no firmware stored (upload first)");
|
||||
}
|
||||
|
||||
/* ★ 接收前端发送的固件 SHA-256(JSON body: {"sha256":"<hex64>"})。
|
||||
* 前端在 relay 触发前用 Web Crypto 算固件 bytes 的 SHA-256,
|
||||
* ESP32 与自己算的 flash SHA 比对,从源头确认"前端发的 = ESP32 flash 存的"。
|
||||
* 之前 motor OTA 没有这一层比对,flash 写入位错误无法被发现,
|
||||
* 直到刷进电机 APP 跑飞才暴露。 */
|
||||
uint8_t frontend_sha[32] = {0};
|
||||
bool has_frontend_sha = false;
|
||||
if (req->content_len > 0 && req->content_len < 512) {
|
||||
char body_buf[512] = {0};
|
||||
int rret = httpd_req_recv(req, body_buf, req->content_len);
|
||||
if (rret > 0) {
|
||||
body_buf[rret] = '\0';
|
||||
cJSON *root = cJSON_Parse(body_buf);
|
||||
if (root) {
|
||||
cJSON *sha_item = cJSON_GetObjectItem(root, "sha256");
|
||||
if (sha_item && cJSON_IsString(sha_item) &&
|
||||
strlen(cJSON_GetStringValue(sha_item)) == 64) {
|
||||
const char *hex = cJSON_GetStringValue(sha_item);
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
char hb[3] = { hex[i*2], hex[i*2+1], 0 };
|
||||
frontend_sha[i] = (uint8_t)strtoul(hb, nullptr, 16);
|
||||
}
|
||||
has_frontend_sha = true;
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
/* relay 不做 SHA 校验(2026-07-27):完整性校验已在上传阶段闭环
|
||||
*(ESP32 从 flash 读回算 SHA 随响应返回,前端比对 + 失败重传)。
|
||||
* relay 只是启动扳机;body 一律忽略,仅排空以维持 keep-alive 解析。 */
|
||||
if (req->content_len > 0) {
|
||||
char drain[128];
|
||||
int remaining = req->content_len;
|
||||
while (remaining > 0) {
|
||||
const int chunk = remaining > (int)sizeof(drain) ? (int)sizeof(drain) : remaining;
|
||||
const int r = httpd_req_recv(req, drain, (size_t)chunk);
|
||||
if (r <= 0) break;
|
||||
remaining -= r;
|
||||
}
|
||||
}
|
||||
if (!has_frontend_sha) {
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"missing_sha256", "missing sha256 in body (frontend SHA-256 required)");
|
||||
}
|
||||
|
||||
/* 三方比对:前端 SHA vs flash 实际 SHA vs meta.checksum。
|
||||
* verifyFrontendSha 内部重算 flash SHA,与 frontend_sha 比对,
|
||||
* 同时也验证与 meta.checksum(上传收尾存的)一致。
|
||||
* 任何一项不符都拒绝刷写,从源头堵住 flash 存储环节位错误。 */
|
||||
uint8_t device_sha[32] = {0};
|
||||
esp_err_t verify_err = Gd32FlashStore::instance().verifyFrontendSha(frontend_sha, device_sha);
|
||||
if (verify_err != ESP_OK) {
|
||||
char dev_hex[65] = {0}, front_hex[65] = {0};
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
snprintf(&dev_hex[i*2], 3, "%02X", device_sha[i]);
|
||||
snprintf(&front_hex[i*2], 3, "%02X", frontend_sha[i]);
|
||||
}
|
||||
DEBUG_ERR("MOTOR_OTA", "relay 前 SHA 比对失败: front=%s device=%s err=%s",
|
||||
front_hex, dev_hex, esp_err_to_name(verify_err));
|
||||
(void)Gd32FlashStore::instance().metaClear();
|
||||
char err_msg[224]; /* 固定文字 ~55 + 2×64 hex = 需 >183,给 224 余量 */
|
||||
snprintf(err_msg, sizeof(err_msg),
|
||||
"frontend SHA != flash SHA (front=%s device=%s), re-upload required",
|
||||
front_hex, dev_hex);
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"firmware_checksum_mismatch", err_msg);
|
||||
}
|
||||
DEBUG_INFO("MOTOR_OTA", "relay 前三方 SHA 比对通过 (%lu B)", (unsigned long)meta.total_size);
|
||||
|
||||
if (motorOtaIsActive()) {
|
||||
httpd_resp_set_status(req, "409 Conflict");
|
||||
@@ -2282,9 +2252,9 @@ esp_err_t motor_ota_status_handler(httpd_req_t *req)
|
||||
static const char *state_names[] = {
|
||||
"idle", "handshake_req", "handshake_nonce", "handshake_confirm",
|
||||
"erase", "transfer", "finish_ota", "finished", "error",
|
||||
"pre_cancel"
|
||||
"pre_cancel", "wait_app_run"
|
||||
};
|
||||
const int si = (st.state <= MOTOR_OTA_PRE_CANCEL) ? (int)st.state : 0;
|
||||
const int si = (st.state <= MOTOR_OTA_WAIT_APP_RUN) ? (int)st.state : 0;
|
||||
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf),
|
||||
|
||||
Reference in New Issue
Block a user