feat(ota): 解耦ESP32 OTA核心逻辑到独立模块并增强BLE控制功能

- 新增ota目录和esp32_ota_core.cpp实现ESP32 OTA核心功能,支持begin/write/finalize操作
- 修改CMakeLists.txt添加ota源文件和包含目录
- 重构ble_ota.cpp将OTA写入/收尾逻辑委托给Esp32OtaCore,移除mbedtls直接引用
- 新增BLE_OTA_CTRL_RESTART_ESP32(0xFA)命令用于重启ESP32设备
- 更新BLE_OTA_CTRL_TRIGGER_ESP32(0xF8)逻辑,要求前端提供SHA-256校验并比对
- 添加SHA-256校验不匹配错误码BLE_OTA_FINISH_ERR_SHA_MISMATCH(0x85)
- 优化OTA收尾流程,设备端读取分区计算SHA-256并与前端提供的哈希比对
- 添加BLE_OTA_NOTIFY_SHA256通知类型用于上报设备端计算的SHA-256值
- 更新app_config.h中的BLE OTA命令描述文档
- 移除BleOta类中的OTA状态变量,仅保留BLE传输层相关状态
- 更新版本构建时间和Git哈希信息
This commit is contained in:
小龙虾
2026-06-14 23:03:28 +08:00
parent 2caf10b9de
commit 9ca53ae66e
19 changed files with 1338 additions and 572 deletions
+2
View File
@@ -1,6 +1,7 @@
idf_component_register( idf_component_register(
SRCS SRCS
"core/main.cpp" "core/main.cpp"
"ota/esp32_ota_core.cpp"
"relay/ota_relay.cpp" "relay/ota_relay.cpp"
"transport/spi_master.cpp" "transport/spi_master.cpp"
"transport/protocol.cpp" "transport/protocol.cpp"
@@ -11,6 +12,7 @@
INCLUDE_DIRS INCLUDE_DIRS
"." "."
"core" "core"
"ota"
"relay" "relay"
"transport" "transport"
"server" "server"
+2 -1
View File
@@ -96,7 +96,8 @@ inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004; inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7; 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_CTRL_VERIFY_SHA256 = 0xF9; // SHA-256哈希验证命令 inline constexpr uint8_t BLE_OTA_CTRL_VERIFY_SHA256 = 0xF9; // SHA-256哈希验证命令(仅从分区读回算 SHA-256,前端比对后再决定是否触发)
inline constexpr uint8_t BLE_OTA_CTRL_RESTART_ESP32 = 0xFA; // 重启 ESP32 命令(配套前端 expectedReboot 自动重连)
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; inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480;
+2 -2
View File
@@ -8,7 +8,7 @@
#define FW_VERSION_STRING "0.3.12" #define FW_VERSION_STRING "0.3.12"
// 编译时间 // 编译时间
#define BUILD_TIME "2026-06-14 17:05:57" #define BUILD_TIME "2026-06-14 22:46:58"
// Git 信息 // Git 信息
#define GIT_HASH "6a6bc9d" #define GIT_HASH "2caf10b"
+301
View File
@@ -0,0 +1,301 @@
/*
* esp32_ota_core.cpp - ESP32 本机 OTA 核心流程实现(WiFi / BLE 通道共用)
*
* 设计依据 WiFi 原流程(wifi_ota.cpp 旧实现),修复已知 bug:
* - 旧 wifi_ota.cpp:625 在 esp_ota_end 成功后又调 esp_ota_abort → 用 m_ended 标志避免
* - SHA 统一从分区读回计算,与 BLE 增量计算方式统一为本风格
*/
#include <string.h>
#include <ctype.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
#include "mbedtls/private/sha256.h"
#include "esp32_ota_core.h"
#include "ota_mutex.h"
#include "app_config.h"
#include "monitor/debug_log.h"
#define TAG "OTA_CORE"
Esp32OtaCore& Esp32OtaCore::instance()
{
static Esp32OtaCore inst;
return inst;
}
void Esp32OtaCore::resetState()
{
m_active = false;
m_handle = 0;
m_partition = nullptr;
m_received = 0;
m_shaComputed = false;
memset(m_sha256, 0, sizeof(m_sha256));
m_ended = false;
}
esp_err_t Esp32OtaCore::begin(size_t total_size)
{
// 若本核心已有未完成会话,先中止并释放全局互斥
if (m_active) {
DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA 会话,先中止旧会话");
if (!m_ended) {
esp_ota_abort(m_handle);
}
esp32_ota_mutex_release();
resetState();
}
// ★ 全局互斥:检查 ESP32 OTA 是否已被其他通道占用
if (!esp32_ota_mutex_acquire(1000)) {
DEBUG_ERR(TAG, "ESP32 OTA 被其他通道占用,拒绝 begin");
return ESP_ERR_INVALID_STATE;
}
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
boot_partition->label, boot_partition->size, boot_partition->address);
// 选择非当前启动的 OTA 分区(OTA_0 → OTA_1 fallback)
m_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (m_partition == NULL ||
m_partition->address == boot_partition->address) {
m_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (m_partition == NULL) {
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
esp32_ota_mutex_release();
return ESP_FAIL;
}
DEBUG_INFO(TAG, "OTA 目标分区: %s, size=%u, address=0x%lx",
m_partition->label, m_partition->size, m_partition->address);
if (total_size > m_partition->size) {
DEBUG_ERR(TAG, "固件太大: %u > 分区大小: %u",
(unsigned)total_size, (unsigned)m_partition->size);
m_partition = nullptr;
esp32_ota_mutex_release();
return ESP_FAIL;
}
// 统一传 total_size 给 esp_ota_begin(WiFi 风格)
esp_err_t err = esp_ota_begin(m_partition, total_size, &m_handle);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
m_partition = nullptr;
m_handle = 0;
esp32_ota_mutex_release();
return err;
}
m_received = 0;
m_active = true;
m_shaComputed = false;
m_ended = false;
memset(m_sha256, 0, sizeof(m_sha256));
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s, 声明大小: %u 字节",
m_partition->label, (unsigned)total_size);
return ESP_OK;
}
esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
{
if (!m_active || m_partition == nullptr) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = esp_ota_write(m_handle, data, len);
if (err == ESP_OK) {
m_received += len;
} else {
DEBUG_ERR(TAG, "esp_ota_write 失败: %s (len=%u)", esp_err_to_name(err), (unsigned)len);
}
return err;
}
esp_err_t Esp32OtaCore::computeShaFromPartition()
{
if (m_partition == NULL || m_received == 0) {
return ESP_ERR_INVALID_STATE;
}
DEBUG_INFO(TAG, "从 OTA 分区读回计算 SHA-256 (%u 字节)...", (unsigned)m_received);
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
uint8_t *read_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!read_buf) {
DEBUG_ERR(TAG, "SHA 计算: malloc 失败");
mbedtls_sha256_free(&ctx);
return ESP_ERR_NO_MEM;
}
size_t offset = 0;
while (offset < m_received) {
size_t to_read = m_received - offset;
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
esp_err_t rd_err = esp_partition_read(m_partition, offset, read_buf, to_read);
if (rd_err != ESP_OK) {
DEBUG_ERR(TAG, "SHA 计算: 分区读失败 @offset %u: %s",
(unsigned)offset, esp_err_to_name(rd_err));
free(read_buf);
mbedtls_sha256_free(&ctx);
return rd_err;
}
mbedtls_sha256_update(&ctx, read_buf, to_read);
offset += to_read;
}
free(read_buf);
mbedtls_sha256_finish(&ctx, m_sha256);
mbedtls_sha256_free(&ctx);
m_shaComputed = true;
char hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02x", m_sha256[i]);
}
DEBUG_INFO(TAG, "设备端 SHA-256: %s", hex);
return ESP_OK;
}
esp_err_t Esp32OtaCore::computeDeviceSha(char *out_device_sha_hex)
{
if (!m_active || m_partition == NULL) {
return ESP_ERR_INVALID_STATE;
}
// 若已计算过,直接复用
if (!m_shaComputed) {
esp_err_t err = computeShaFromPartition();
if (err != ESP_OK) return err;
}
if (out_device_sha_hex) {
for (int i = 0; i < 32; ++i) {
snprintf(&out_device_sha_hex[i * 2], 65 - i * 2, "%02x", m_sha256[i]);
}
out_device_sha_hex[64] = '\0';
}
return ESP_OK;
}
esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
char *out_device_sha_hex)
{
if (!m_active) {
DEBUG_ERR(TAG, "finalize: OTA 未激活");
return ESP_ERR_INVALID_STATE;
}
// ── esp_ota_end ──
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_end...");
esp_err_t err = esp_ota_end(m_handle);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_end 失败: %s", esp_err_to_name(err));
// end 失败时仍可 abort(未真正 end)
esp_ota_abort(m_handle);
m_ended = false;
esp32_ota_mutex_release();
resetState();
return err;
}
m_ended = true; // ★ end 成功,后续不得再 abort
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_end 完成");
// ── SHA-256 校验(若前端提供)──
if (frontend_sha_hex != nullptr && strlen(frontend_sha_hex) == 64) {
esp_err_t sha_err = computeShaFromPartition();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
esp32_ota_mutex_release();
resetState();
return sha_err;
}
char device_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&device_hex[i * 2], sizeof(device_hex) - i * 2, "%02x", m_sha256[i]);
}
bool match = true;
for (int i = 0; i < 64; i++) {
if (tolower((unsigned char)frontend_sha_hex[i]) !=
tolower((unsigned char)device_hex[i])) {
match = false;
break;
}
}
if (out_device_sha_hex) {
strncpy(out_device_sha_hex, device_hex, 64);
out_device_sha_hex[64] = '\0';
}
if (!match) {
DEBUG_ERR(TAG, "SHA-256 不匹配! frontend=%s device=%s,中止",
frontend_sha_hex, device_hex);
esp32_ota_mutex_release();
resetState();
return ESP_ERR_INVALID_VERSION; // 调用方据此返回错误给前端
}
DEBUG_INFO(TAG, "SHA-256 校验通过");
} else if (out_device_sha_hex) {
// 未提供前端 SHA,但调用方仍想拿到设备端 SHA
if (computeShaFromPartition() == ESP_OK) {
for (int i = 0; i < 32; ++i) {
snprintf(&out_device_sha_hex[i * 2], 65 - i * 2, "%02x", m_sha256[i]);
}
out_device_sha_hex[64] = '\0';
}
}
// ── 设置启动分区 ──
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_set_boot_partition...");
err = esp_ota_set_boot_partition(m_partition);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
esp32_ota_mutex_release();
resetState();
return err;
}
DEBUG_INFO(TAG, "[FINALIZE] esp_ota_set_boot_partition 完成");
esp32_ota_mutex_release();
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,共 %u 字节", (unsigned)m_received);
// 保持 m_active 直到重启或下次 begin;resetState 由 begin/abort 处理
// (此处不 reset,以便 BLE finish task 在重启前可查询状态)
if (reboot) {
DEBUG_INFO(TAG, "[FINALIZE] 500ms 后重启...");
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
// 不会返回
}
return ESP_OK;
}
void Esp32OtaCore::abort()
{
if (!m_active) {
return;
}
if (!m_ended) {
esp_ota_abort(m_handle);
}
esp32_ota_mutex_release();
resetState();
DEBUG_INFO(TAG, "ESP32 OTA 已中止并清理");
}
+82
View File
@@ -0,0 +1,82 @@
/*
* esp32_ota_core.h - ESP32 本机 OTA 核心流程(WiFi / BLE 通道共用)
*
* 将 ESP32 自身固件升级的完整生命周期封装为单例:
* begin → write*(N) → finalize / abort
*
* 统一以 WiFi 流程为基准:
* - esp_ota_begin 第二参数传 total_size
* - SHA-256 在 finalize 时从 OTA 分区读回计算(不在上传期间增量计算)
* - finalize 顺序: esp_ota_end → 比对前端 SHA → esp_ota_set_boot_partition → 可选重启
* - m_ended 标志位防止 esp_ota_end 成功后再次 esp_ota_abort 同一 handle
*
* WiFi 与 BLE 仅传输方式不同(HTTP / GATT),OTA 写入与收尾逻辑完全复用本核心。
*/
#ifndef ESP32_OTA_CORE_H
#define ESP32_OTA_CORE_H
#include <stddef.h>
#include "esp_err.h"
#include "esp_ota_ops.h"
#include "esp_partition.h"
class Esp32OtaCore {
public:
static Esp32OtaCore& instance();
// ── 阶段 1:开启 OTA 会话 ──
// total_size: 固件总大小(字节),用于分区大小校验和传给 esp_ota_begin
// 返回 ESP_OK / ESP_ERR_INVALID_STATE(被其它通道占用)/ ESP_FAIL
esp_err_t begin(size_t total_size);
// ── 阶段 2:写入固件数据(可在 begin 后多次调用)──
// 纯 esp_ota_write,不做增量 SHA(SHA 在 finalize 时统一从分区读回计算)
esp_err_t write(const uint8_t *data, size_t len);
// ── 阶段 3:收尾(end → 校验 SHA → set_boot → 可选重启)──
// frontend_sha_hex: 前端提供的 64 字符 hex SHA-256,nullptr 表示跳过校验直接应用
// reboot: true 则 set_boot 成功后延时 500ms 调 esp_restart()
// out_device_sha_hex: 输出设备端计算出的 SHA hex(缓冲区至少 65 字节),可为 nullptr
// 返回:
// ESP_OK 成功(若 reboot=true,本调用不会返回)
// ESP_ERR_INVALID_VERSION SHA 不匹配(已 abort 并清理)
// 其他 end/set_boot 失败(已清理)
esp_err_t finalize(const char *frontend_sha_hex, bool reboot,
char *out_device_sha_hex);
// ── 仅计算设备端 SHA(不 end、不 set_boot、不重启)──
// 供前端只读校验场景(如 WiFi verify_only 模式)
// out_device_sha_hex: 输出缓冲区,至少 65 字节
// 返回 ESP_OK / 失败
esp_err_t computeDeviceSha(char *out_device_sha_hex);
// ── 中止并清理(abort + 释放 mutex + 状态复位)──
// 若 esp_ota_end 已成功(m_ended=true),则跳过 esp_ota_abort 仅复位状态
void abort();
// ── 状态查询 ──
bool isActive() const { return m_active; }
size_t getReceived() const { return m_received; }
bool isRebootPending() const { return m_rebootPending; }
void setRebootPending(bool v) { m_rebootPending = v; }
private:
Esp32OtaCore() = default;
// 从 OTA 分区读回计算 SHA-256(WiFi 风格),结果写入 m_sha256,置 m_shaComputed=true
esp_err_t computeShaFromPartition();
// 状态全复位(不调 esp_ota_abort,由调用方根据 m_ended 决定)
void resetState();
bool m_active = false;
esp_ota_handle_t m_handle = 0;
const esp_partition_t *m_partition = nullptr;
size_t m_received = 0;
uint8_t m_sha256[32] = {0};
bool m_shaComputed = false;
bool m_ended = false; // ★ esp_ota_end 成功后置 true,防 abort 误用
bool m_rebootPending = false; // BLE 收尾任务去重用
};
#endif // ESP32_OTA_CORE_H
+132 -289
View File
@@ -9,7 +9,7 @@
* *
* GATT 服务: * GATT 服务:
* Service UUID: 0xFFFF * Service UUID: 0xFFFF
* - CTRL (0x0001, Write): 0xF7=触发GD32中继, 0xF8=触发ESP32 OTA升级 * - CTRL (0x0001, Write): 0xF7=触发GD32中继, 0xF8=触发ESP32 OTA升级(含 32B SHA-256 校验并 reboot),0xFA=重启ESP32
* - DATA_IN (0x0002, Write): cmd=0x00=GD32数据, cmd=0x01=ESP32数据 * - DATA_IN (0x0002, Write): cmd=0x00=GD32数据, cmd=0x01=ESP32数据
* - DATA_OUT (0x0003, Notify): 推送响应 * - DATA_OUT (0x0003, Notify): 推送响应
* - VERSION (0x0004, Read): 固件版本 * - VERSION (0x0004, Read): 固件版本
@@ -34,13 +34,12 @@
#include "services/gatt/ble_svc_gatt.h" #include "services/gatt/ble_svc_gatt.h"
#include "host/ble_att.h" #include "host/ble_att.h"
// SHA-256 哈希计算 // SHA-256 计算已抽离到 Esp32OtaCore,本文件不再直接引用 mbedtls
#define MBEDTLS_DECLARE_PRIVATE_IDENTIFIERS
#include "mbedtls/private/sha256.h"
#include "app_config.h" #include "app_config.h"
#include "ota_mutex.h" #include "ota_mutex.h"
#include "ble_ota.h" #include "ble_ota.h"
#include "esp32_ota_core.h"
#include "ota_relay.h" #include "ota_relay.h"
#include "health_monitor.h" #include "health_monitor.h"
#include "monitor/debug_log.h" #include "monitor/debug_log.h"
@@ -48,6 +47,7 @@
#define TAG "BLE_OTA" #define TAG "BLE_OTA"
#define BLE_OTA_NOTIFY_ACK 0xA1 #define BLE_OTA_NOTIFY_ACK 0xA1
#define BLE_OTA_NOTIFY_FINISH 0xB1 #define BLE_OTA_NOTIFY_FINISH 0xB1
#define BLE_OTA_NOTIFY_SHA256 0xC1 // 设备端 SHA-256 上报([0xC1] + [32字节])
#define BLE_OTA_FINISH_BEGIN_OTA_END 0x01 #define BLE_OTA_FINISH_BEGIN_OTA_END 0x01
#define BLE_OTA_FINISH_DONE_OTA_END 0x02 #define BLE_OTA_FINISH_DONE_OTA_END 0x02
#define BLE_OTA_FINISH_BEGIN_SET_BOOT 0x03 #define BLE_OTA_FINISH_BEGIN_SET_BOOT 0x03
@@ -61,6 +61,7 @@
#define BLE_ATT_ERR_OTA_BUSY 0x80 // ESP32 OTA 被其他通道占用 #define BLE_ATT_ERR_OTA_BUSY 0x80 // ESP32 OTA 被其他通道占用
#define BLE_OTA_FINISH_ERR_MUTEX 0x83 // 互斥拒绝: OTA 被其他通道占用 #define BLE_OTA_FINISH_ERR_MUTEX 0x83 // 互斥拒绝: OTA 被其他通道占用
#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小 #define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小
#define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85 // ESP32 OTA SHA-256 不匹配
// ============================================================ // ============================================================
// 前向声明 // 前向声明
@@ -79,110 +80,91 @@ BleOta& BleOta::instance()
// File-scope val_handle for GATT service definition (struct literal needs compile-time address) // File-scope val_handle for GATT service definition (struct literal needs compile-time address)
static uint16_t s_data_out_val_handle = 0; static uint16_t s_data_out_val_handle = 0;
// ESP32 自身 OTA 状态 // ESP32 自身 OTA 收尾任务(OTA 写入/校验/重启逻辑委托给 Esp32OtaCore)
/** /**
* @brief 异步完成 ESP32 OTA 收尾并触发重启。 * @brief 异步完成 ESP32 OTA 收尾并触发重启。
*
* 实际的 esp_ota_end / SHA 校验 / esp_ota_set_boot_partition / esp_restart 全部
* 由 Esp32OtaCore::finalize 完成。本任务仅负责:补发 ACK → 调 finalize → 上报 SHA → 通知前端。
* @param pvParameters 任务参数,当前未使用。 * @param pvParameters 任务参数,当前未使用。
*/ */
static void ble_esp32_finish_and_reboot_task(void *pvParameters) /**
* @brief 0xFA 重启任务:为了不卡 NimBLE host task,仅在这里推送 FINISH 后 500ms 重启。
* @param pvParameters 任务参数,未使用。
*/
static void ble_esp32_restart_task(void *pvParameters)
{ {
(void)pvParameters; (void)pvParameters;
DEBUG_INFO(TAG, "[RESTART] 推送 FINISH_REBOOT_NOW 后 500ms 重启");
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
// 不会返回
}
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
{
// pvParameters: heap-allocated char[65] frontend SHA hex string (or nullptr)
char *frontend_sha_hex = (char *)pvParameters;
DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动"); DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动");
Esp32OtaCore &core = Esp32OtaCore::instance();
// 补发最终 ACK,让前端尽快得知最后一包已收到
if (BleOta::instance().m_esp32LastSeq != 0xFFFFU) { if (BleOta::instance().m_esp32LastSeq != 0xFFFFU) {
sendWindowAck(BleOta::instance().m_esp32LastCmd, BleOta::instance().m_esp32LastSeq, (uint32_t)BleOta::instance().m_esp32Received); sendWindowAck(BleOta::instance().m_esp32LastCmd,
BleOta::instance().m_esp32LastSeq,
(uint32_t)core.getReceived());
DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", BleOta::instance().m_esp32LastSeq); DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", BleOta::instance().m_esp32LastSeq);
} }
vTaskDelay(pdMS_TO_TICKS(150)); vTaskDelay(pdMS_TO_TICKS(150));
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) { if (!core.isActive()) {
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启"); DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
if (frontend_sha_hex) free(frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = false; BleOta::instance().m_esp32RebootPending = false;
vTaskDelete(NULL); vTaskDelete(NULL);
return; return;
} }
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)BleOta::instance().m_esp32Received); DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节", (unsigned)core.getReceived());
// 完成 SHA-256 哈希计算 // 通知前端:开始 finalize
if (BleOta::instance().m_sha256Ctx != nullptr) {
mbedtls_sha256_finish(BleOta::instance().m_sha256Ctx, BleOta::instance().m_esp32Sha256);
mbedtls_sha256_free(BleOta::instance().m_sha256Ctx);
BleOta::instance().m_esp32HashComputed = true;
// 打印SHA-256哈希值
char hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", BleOta::instance().m_esp32Sha256[i]);
}
DEBUG_INFO(TAG, "ESP32 固件 SHA-256: %s", hex);
}
int64_t t0_us = esp_timer_get_time();
DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_end()");
sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0); sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
esp_err_t err = esp_ota_end(BleOta::instance().m_esp32OtaHandle);
int64_t t1_us = esp_timer_get_time(); char device_sha_hex[65] = {0};
if (err != ESP_OK) { esp_err_t err = core.finalize(frontend_sha_hex,
DEBUG_ERR(TAG, "[FINISH] esp_ota_end 失败: %s (耗时=%lld ms)", /*reboot=*/true,
esp_err_to_name(err), (long long)((t1_us - t0_us) / 1000)); device_sha_hex);
// reboot=true 时 finalize 正常不会返回(core 内部 esp_restart)
if (err == ESP_OK) {
// 极端情况:reboot 未执行(理论上不会发生),仍上报并标记
if (device_sha_hex[0] != '\0') {
uint8_t sha_pkt[33] = {0};
sha_pkt[0] = BLE_OTA_NOTIFY_SHA256;
for (int i = 0; i < 32; ++i) {
unsigned int byte = 0;
sscanf(&device_sha_hex[i * 2], "%02x", &byte);
sha_pkt[1 + i] = (uint8_t)byte;
}
BleOta::instance().notifyResponse(sha_pkt, sizeof(sha_pkt));
}
sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, 0);
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
} else if (err == ESP_ERR_INVALID_VERSION) {
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, 0);
BleOta::instance().m_esp32RebootPending = false;
} else {
DEBUG_ERR(TAG, "[FINISH] OTA 收尾失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err); sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32Received = 0;
BleOta::instance().m_esp32RebootPending = false; BleOta::instance().m_esp32RebootPending = false;
vTaskDelete(NULL);
return;
} }
DEBUG_INFO(TAG, "[FINISH] esp_ota_end 完成 (耗时=%lld ms)",
(long long)((t1_us - t0_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, (uint32_t)((t1_us - t0_us) / 1000));
int64_t t2_us = esp_timer_get_time(); if (frontend_sha_hex) free(frontend_sha_hex);
DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_set_boot_partition()"); vTaskDelete(NULL);
sendFinishEvent(BLE_OTA_FINISH_BEGIN_SET_BOOT, 0);
err = esp_ota_set_boot_partition(BleOta::instance().m_esp32UpdatePartition);
int64_t t3_us = esp_timer_get_time();
if (err != ESP_OK) {
DEBUG_ERR(TAG, "[FINISH] 设置启动分区失败: %s (耗时=%lld ms)",
esp_err_to_name(err), (long long)((t3_us - t2_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32Received = 0;
BleOta::instance().m_esp32RebootPending = false;
vTaskDelete(NULL);
return;
}
DEBUG_INFO(TAG, "[FINISH] esp_ota_set_boot_partition 完成 (耗时=%lld ms)",
(long long)((t3_us - t2_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000));
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32Received = 0;
// SHA-256哈希已在之前计算完成,保留供前端读取
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,800ms 后重启...");
int64_t t4_us = esp_timer_get_time();
DEBUG_INFO(TAG, "[FINISH] 即将进入 800ms 重启前等待");
sendFinishEvent(BLE_OTA_FINISH_BEGIN_DELAY, 800);
vTaskDelay(pdMS_TO_TICKS(800));
int64_t t5_us = esp_timer_get_time();
DEBUG_INFO(TAG, "[FINISH] 800ms 等待结束 (实际耗时=%lld ms)",
(long long)((t5_us - t4_us) / 1000));
sendFinishEvent(BLE_OTA_FINISH_DONE_DELAY, (uint32_t)((t5_us - t4_us) / 1000));
DEBUG_INFO(TAG, "ESP32 即将重启");
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
esp_restart();
} }
// ============================================================ // ============================================================
@@ -203,131 +185,37 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
return (int)copy_len; return (int)copy_len;
} }
/** // ─────────────────────────────────────────────────────────────
* @brief 清理 SHA-256 上下文和哈希状态 // ESP32 OTA 薄包装:实际 begin/write/finalize 已抽离到 Esp32OtaCore
*/ // 此处仅保留 BLE 侧的状态复位与日志,供 data_in_access 调用。
static void ble_esp32_ota_cleanup_sha() // ─────────────────────────────────────────────────────────────
{
if (BleOta::instance().m_sha256Ctx != nullptr) {
mbedtls_sha256_free(BleOta::instance().m_sha256Ctx);
delete BleOta::instance().m_sha256Ctx;
BleOta::instance().m_sha256Ctx = nullptr;
}
BleOta::instance().m_esp32HashComputed = false;
memset(BleOta::instance().m_esp32Sha256, 0, sizeof(BleOta::instance().m_esp32Sha256));
}
/** /**
* @brief 初始化 ESP32 本机 OTA 写入会话并选择目标分区。 * @brief 初始化 ESP32 本机 OTA 写入会话。
* 委托 Esp32OtaCore::begin,并复位 BLE 传输相关状态。
* @param total_size 固件总大小(字节)。 * @param total_size 固件总大小(字节)。
* @return ESP_OK 成功,其他为失败错误码。 * @return ESP_OK 成功,其他为失败错误码。
*/ */
static esp_err_t ble_esp32_ota_begin(size_t total_size) static esp_err_t ble_esp32_ota_begin(size_t total_size)
{ {
// ★ 如果 BLE 自身已有未完成的 OTA 会话,先中止并释放全局互斥 esp_err_t err = Esp32OtaCore::instance().begin(total_size);
if (BleOta::instance().m_esp32OtaActive) {
DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA,会先中止旧会话");
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32Received = 0;
BleOta::instance().m_esp32RebootPending = false;
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
}
// ★ 全局互斥:检查 ESP32 OTA 是否已被其他通道占用
if (!esp32_ota_mutex_acquire(1000)) {
DEBUG_ERR(TAG, "ESP32 OTA 被其他通道占用,拒绝 BLE OTA begin");
return ESP_ERR_INVALID_STATE;
}
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
boot_partition->label, boot_partition->size, boot_partition->address);
BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (BleOta::instance().m_esp32UpdatePartition == NULL ||
BleOta::instance().m_esp32UpdatePartition->address == boot_partition->address) {
BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (BleOta::instance().m_esp32UpdatePartition == NULL) {
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
esp32_ota_mutex_release();
return ESP_FAIL;
}
DEBUG_INFO(TAG, "ESP32 OTA 目标分区: %s, size=%u, address=0x%lx",
BleOta::instance().m_esp32UpdatePartition->label,
BleOta::instance().m_esp32UpdatePartition->size,
BleOta::instance().m_esp32UpdatePartition->address);
if (total_size > BleOta::instance().m_esp32UpdatePartition->size) {
DEBUG_ERR(TAG, "ESP32 固件太大: %u > 分区大小: %u",
total_size, BleOta::instance().m_esp32UpdatePartition->size);
BleOta::instance().m_esp32UpdatePartition = NULL;
esp32_ota_mutex_release();
return ESP_FAIL;
}
// 用分区大小初始化 OTA,而非包头声明的 total_size
// esp_ota_end 会从固件镜像头读取实际大小来校验,不依赖此处传入的 size
esp_err_t err = esp_ota_begin(BleOta::instance().m_esp32UpdatePartition,
BleOta::instance().m_esp32UpdatePartition->size,
&BleOta::instance().m_esp32OtaHandle);
if (err != ESP_OK) { if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32OtaHandle = 0;
esp32_ota_mutex_release();
return err; return err;
} }
// 复位 BLE 传输相关状态
BleOta::instance().m_esp32Received = 0;
BleOta::instance().m_esp32OtaActive = true;
BleOta::instance().m_esp32RebootPending = false; BleOta::instance().m_esp32RebootPending = false;
BleOta::instance().m_esp32LastCmd = 0; BleOta::instance().m_esp32LastCmd = 0;
BleOta::instance().m_esp32LastSeq = 0xFFFFU; BleOta::instance().m_esp32LastSeq = 0xFFFFU;
// 初始化 SHA-256 计算上下文
if (BleOta::instance().m_sha256Ctx == nullptr) {
BleOta::instance().m_sha256Ctx = new mbedtls_sha256_context();
}
mbedtls_sha256_init(BleOta::instance().m_sha256Ctx);
mbedtls_sha256_starts(BleOta::instance().m_sha256Ctx, 0);
BleOta::instance().m_esp32HashComputed = false;
memset(BleOta::instance().m_esp32Sha256, 0, sizeof(BleOta::instance().m_esp32Sha256));
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", BleOta::instance().m_esp32UpdatePartition->label);
return ESP_OK; return ESP_OK;
} }
/** /**
* @brief 向 ESP32 OTA 分区写入一段固件数据。 * @brief 中止 ESP32 OTA 会话并清理(薄包装)。
* @param data 待写入数据指针。
* @param len 写入长度(字节)。
* @return ESP_OK 成功,其他为失败错误码。
*/ */
static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len) static void ble_esp32_ota_abort()
{ {
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) { Esp32OtaCore::instance().abort();
return ESP_ERR_INVALID_STATE; BleOta::instance().m_esp32RebootPending = false;
}
// 更新 SHA-256 哈希计算
if (BleOta::instance().m_sha256Ctx != nullptr) {
mbedtls_sha256_update(BleOta::instance().m_sha256Ctx, data, len);
}
esp_err_t err = esp_ota_write(BleOta::instance().m_esp32OtaHandle, data, len);
if (err == ESP_OK) {
BleOta::instance().m_esp32Received += len;
}
return err;
} }
/** /**
@@ -407,14 +295,14 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
return BLE_ATT_ERR_REQ_NOT_SUPPORTED; return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
} }
uint8_t buf[4] = {0}; uint8_t buf[36] = {0};
int len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf)); int len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
if (len < 1) { if (len < 1) {
DEBUG_WARN(TAG, "CTRL 数据为空"); DEBUG_WARN(TAG, "CTRL 数据为空");
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
} }
DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X", buf[0]); DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X len=%d", buf[0], len);
if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) { if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) {
// 检查固件是否存储完毕 → 触发 GD32 OTA 中继 // 检查固件是否存储完毕 → 触发 GD32 OTA 中继
@@ -431,7 +319,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
meta.stored_size, meta.total_size); meta.stored_size, meta.total_size);
} }
} else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) { } else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) { if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级"); DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
return 0; return 0;
} }
@@ -440,66 +328,63 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
return 0; return 0;
} }
// 强制要求 32B 前端 SHA-256:与 WiFi 流程对齐,设备端读回分区计算并比对,
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH。
if (len < 33) {
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33(前端 SHA-256 必传),拒绝触发");
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_INVALID_ARG);
return 0;
}
char *frontend_sha_hex = (char *)malloc(65);
if (!frontend_sha_hex) {
DEBUG_ERR(TAG, "CTRL 0xF8 申请内存失败");
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_NO_MEM);
return 0;
}
for (int i = 0; i < 32; ++i) {
snprintf(&frontend_sha_hex[i * 2], 3, "%02x", buf[1 + i]);
}
frontend_sha_hex[64] = '\0';
DEBUG_INFO(TAG, "前端 SHA-256: %s", frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = true; BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task, BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish", "ble_ota_finish",
4096, 4096,
NULL, frontend_sha_hex, // heap string; task 负责 free
5, 5,
NULL); NULL);
if (ok != pdPASS) { if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败"); DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
free(frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = false; BleOta::instance().m_esp32RebootPending = false;
} else { } else {
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(由设备端异步完成)"); DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
} }
} else if (buf[0] == BLE_OTA_CTRL_VERIFY_SHA256) { } else if (buf[0] == BLE_OTA_CTRL_RESTART_ESP32) {
// SHA-256 哈希验证命令 // 重启 ESP32:GATT 回调中仅推送 FINISH,延时 + 重启交给独立 task,避免卡 NimBLE host
// 期望数据格式: [cmd(1)] + [sha256(32)] DEBUG_INFO(TAG, "收到 0xFA 重启请求,创建 restart task(500ms 后 esp_restart())");
if (len < 33) { if (BleOta::instance().m_esp32RebootPending) {
DEBUG_WARN(TAG, "SHA-256 验证数据长度不足: %d (需要33)", len); DEBUG_WARN(TAG, "0xFA 重启已在进行中,忽略重复 CTRL");
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
// 提取前端发送的SHA-256哈希
uint8_t frontend_sha[32];
memcpy(frontend_sha, &buf[1], 32);
// 打印前端SHA-256
char frontend_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&frontend_hex[i * 2], sizeof(frontend_hex) - i * 2, "%02X", frontend_sha[i]);
}
DEBUG_INFO(TAG, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
DEBUG_INFO(TAG, "📊 SHA-256 哈希对比验证");
DEBUG_INFO(TAG, " 前端计算: %s", frontend_hex);
// 检查设备端是否已计算SHA-256
if (!BleOta::instance().m_esp32HashComputed) {
DEBUG_WARN(TAG, "⚠️ 设备端尚未完成SHA-256计算");
sendFinishEvent(0xA0, 0); // 自定义事件:哈希未就绪
return 0; return 0;
} }
BleOta::instance().m_esp32RebootPending = true;
// 打印设备端SHA-256 BaseType_t ok = xTaskCreate(ble_esp32_restart_task,
char device_hex[65] = {0}; "ble_ota_restart",
for (int i = 0; i < 32; ++i) { 2048,
snprintf(&device_hex[i * 2], sizeof(device_hex) - i * 2, "%02X", BleOta::instance().m_esp32Sha256[i]); NULL,
} 5,
DEBUG_INFO(TAG, " 设备计算: %s", device_hex); NULL);
if (ok != pdPASS) {
// 对比哈希值 DEBUG_ERR(TAG, "创建 0xFA 重启任务失败");
if (memcmp(frontend_sha, BleOta::instance().m_esp32Sha256, 32) == 0) { BleOta::instance().m_esp32RebootPending = false;
DEBUG_INFO(TAG, "✅ SHA-256 验证通过!固件传输完整无误");
DEBUG_INFO(TAG, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
sendFinishEvent(0xA1, 1); // 自定义事件:验证成功
} else {
DEBUG_ERR(TAG, "❌ SHA-256 验证失败!固件传输过程中出现错误");
DEBUG_ERR(TAG, " 请检查BLE链路质量并重试");
DEBUG_ERR(TAG, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
sendFinishEvent(0xA2, 0); // 自定义事件:验证失败
} }
} }
// 0xF7 = 触发 GD32 中继
// 0xF8 = 触发 ESP32 本机 OTA 收尾:设备端读 OTA 分区算 SHA 并与前端 32B SHA 比对,
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH(不重启)
// 0xFA = 重启 ESP32(配套前端 expectedReboot 自动重连)
return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0 return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0
} }
@@ -590,22 +475,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size); DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size);
return BLE_ATT_ERR_UNLIKELY; return BLE_ATT_ERR_UNLIKELY;
} }
const esp_partition_t *boot_partition = esp_ota_get_running_partition(); // 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
const esp_partition_t *check_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (check_partition == NULL || check_partition->address == boot_partition->address) {
check_partition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (check_partition == NULL) {
DEBUG_ERR(TAG, "找不到 ESP32 OTA 目标分区");
return BLE_ATT_ERR_UNLIKELY;
}
if (total_size > check_partition->size) {
DEBUG_ERR(TAG, "ESP32 固件太大: %" PRIu32 " > OTA分区 %u",
total_size, check_partition->size);
return BLE_ATT_ERR_UNLIKELY;
}
esp_err_t err = ble_esp32_ota_begin(total_size); esp_err_t err = ble_esp32_ota_begin(total_size);
if (err != ESP_OK) { if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) { if (err == ESP_ERR_INVALID_STATE) {
@@ -616,66 +486,39 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
} }
} }
esp_err_t err = ble_esp32_ota_write(&buf[data_offset], data_len); esp_err_t err = Esp32OtaCore::instance().write(&buf[data_offset], data_len);
if (err != ESP_OK) { if (err != ESP_OK) {
DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s", DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s",
seq, esp_err_to_name(err)); seq, esp_err_to_name(err));
esp_ota_abort(BleOta::instance().m_esp32OtaHandle); ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
ble_esp32_ota_cleanup_sha(); // 清理SHA状态
return BLE_ATT_ERR_UNLIKELY; return BLE_ATT_ERR_UNLIKELY;
} }
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u", DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
seq, (unsigned int)BleOta::instance().m_esp32Received); seq, (unsigned int)Esp32OtaCore::instance().getReceived());
// 尾包检测: 期望 payload < 标准负载说明是真正的最后一包 // 尾包检测: 期望 payload < 标准负载说明是真正的最后一包
// 使用 expected_payload 而非 data_len,避免缓冲区截断导致误触发重启 // 使用 expected_payload 而非 data_len,避免缓冲区截断导致误触发重启
bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD); bool isLastPacket = (expected_payload < BLE_OTA_CHUNK_PAYLOAD);
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
(total_size > 0 && BleOta::instance().m_esp32Received >= total_size) || (total_size > 0 && Esp32OtaCore::instance().getReceived() >= total_size) ||
isLastPacket) { isLastPacket) {
if (isLastPacket) { if (isLastPacket) {
DEBUG_INFO(TAG, "ESP32 尾包检测: seq=%u data_len=%zu received=%u total=%" PRIu32, DEBUG_INFO(TAG, "ESP32 尾包检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
seq, data_len, (unsigned int)BleOta::instance().m_esp32Received, total_size); seq, data_len, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size);
} }
sendWindowAck(cmd, seq, (uint32_t)BleOta::instance().m_esp32Received); sendWindowAck(cmd, seq, (uint32_t)Esp32OtaCore::instance().getReceived());
} }
if ((total_size > 0 && BleOta::instance().m_esp32Received >= total_size) || isLastPacket) { if ((total_size > 0 && Esp32OtaCore::instance().getReceived() >= total_size) || isLastPacket) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s", DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s",
(unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1, (unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1,
isLastPacket ? " [尾包]" : ""); isLastPacket ? " [尾包]" : "");
BleOta::instance().m_esp32LastCmd = cmd; BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq; BleOta::instance().m_esp32LastSeq = seq;
// 注:与 WiFi 流程对齐,传输完成后由前端发送 0xF8 + SHA-256 触发 finalize,
if (!BleOta::instance().m_esp32RebootPending) { // 设备端不再自动创建收尾任务。
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
NULL,
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
esp32_ota_mutex_release();
BleOta::instance().m_esp32OtaActive = false;
BleOta::instance().m_esp32OtaHandle = 0;
BleOta::instance().m_esp32UpdatePartition = NULL;
BleOta::instance().m_esp32RebootPending = false;
ble_esp32_ota_cleanup_sha(); // 清理SHA状态
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启");
} else {
DEBUG_WARN(TAG, "ESP32 OTA 收尾任务已存在,忽略重复完成触发");
}
} }
} }
// --- GD32 OTA:首个分块时创建存储元数据 --- // --- GD32 OTA:首个分块时创建存储元数据 ---
+8 -16
View File
@@ -1,6 +1,9 @@
/* /*
* ble_ota.h - BLE OTA GATT 服务器 * ble_ota.h - BLE OTA GATT 服务器
* T5.1 - T5.5 * T5.1 - T5.5
*
* ESP32 本机 OTA 的写入/收尾逻辑已抽离到 Esp32OtaCore(WiFi/BLE 共用)。
* 本类仅保留 BLE 传输层相关状态(连接、收尾任务去重、ACK 窗口)。
*/ */
#ifndef BLE_OTA_H #ifndef BLE_OTA_H
#define BLE_OTA_H #define BLE_OTA_H
@@ -9,9 +12,6 @@
#include "esp_err.h" #include "esp_err.h"
#include "esp_ota_ops.h" #include "esp_ota_ops.h"
// 前向声明 mbedtls_sha256_context
typedef struct mbedtls_sha256_context mbedtls_sha256_context;
class BleOta { class BleOta {
public: public:
static BleOta& instance(); static BleOta& instance();
@@ -27,18 +27,10 @@ public:
bool m_bleConnected = false; bool m_bleConnected = false;
uint16_t m_connHandle = 0xFFFF; uint16_t m_connHandle = 0xFFFF;
bool m_esp32OtaActive = false; // BLE 传输相关(OTA 写入状态已移至 Esp32OtaCore)
esp_ota_handle_t m_esp32OtaHandle = 0; bool m_esp32RebootPending = false; // 收尾任务去重标志
const esp_partition_t *m_esp32UpdatePartition = NULL; uint8_t m_esp32LastCmd = 0; // 最后一次数据包命令(用于补发 ACK)
size_t m_esp32Received = 0; uint16_t m_esp32LastSeq = 0xFFFFU; // 最后一次数据包序号(用于补发 ACK)
bool m_esp32RebootPending = false;
uint8_t m_esp32LastCmd = 0;
uint16_t m_esp32LastSeq = 0xFFFFU;
// SHA-256 哈希校验
mbedtls_sha256_context *m_sha256Ctx = nullptr; // SHA-256计算上下文
uint8_t m_esp32Sha256[32] = {0}; // 计算得到的SHA-256哈希值
bool m_esp32HashComputed = false; // 是否已计算哈希
}; };
#endif // BLE_OTA_H #endif // BLE_OTA_H
+53 -222
View File
@@ -28,6 +28,7 @@
#include "app_config.h" #include "app_config.h"
#include "ota_mutex.h" #include "ota_mutex.h"
#include "wifi_ota.h" #include "wifi_ota.h"
#include "esp32_ota_core.h"
#include "ota_relay.h" #include "ota_relay.h"
#include "health_monitor.h" #include "health_monitor.h"
#include "monitor/debug_log.h" #include "monitor/debug_log.h"
@@ -427,89 +428,25 @@ void WifiOta::setStaticIp()
// ---------------------------------------- // ----------------------------------------
// ESP32 OTA 内部函数 // ESP32 OTA 内部函数
// 注:begin/write/finalize/abort 已抽离到 Esp32OtaCore(main/ota/esp32_ota_core.{h,cpp})
// WiFi 与 BLE 通道共用同一核心,本文件仅负责 HTTP 传输层。
/**
* @brief 清理 WiFi OTA 的 SHA-256 上下文和哈希状态
*/
static void wifi_esp32_ota_cleanup_sha()
{
WifiOta::instance().m_esp32HashComputed = false;
memset(WifiOta::instance().m_esp32Sha256, 0, sizeof(WifiOta::instance().m_esp32Sha256));
WifiOta::instance().m_hasFrontendSha256 = false;
memset(WifiOta::instance().m_frontendSha256, 0, sizeof(WifiOta::instance().m_frontendSha256));
}
/**
* @brief 初始化 ESP32 自身的 OTA 写入流程。
* @param total_size
* @return ESP_OK 成功,其他失败。
*/
static esp_err_t esp32_ota_begin(size_t total_size)
{
// ★ 全局互斥:检查 ESP32 OTA 是否已被其他通道占用
if (!esp32_ota_mutex_acquire(1000)) {
DEBUG_ERR("OTA", "ESP32 OTA 被其他通道占用,拒绝 WiFi OTA begin");
return ESP_ERR_INVALID_STATE;
}
const esp_partition_t *boot_partition = esp_ota_get_running_partition();
DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx",
boot_partition->label, boot_partition->size, boot_partition->address);
WifiOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
if (WifiOta::instance().m_esp32UpdatePartition == NULL ||
WifiOta::instance().m_esp32UpdatePartition->address == boot_partition->address) {
WifiOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
}
if (WifiOta::instance().m_esp32UpdatePartition == NULL) {
DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
esp32_ota_mutex_release();
return ESP_FAIL;
}
DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx",
WifiOta::instance().m_esp32UpdatePartition->label,
WifiOta::instance().m_esp32UpdatePartition->size,
WifiOta::instance().m_esp32UpdatePartition->address);
if (total_size > WifiOta::instance().m_esp32UpdatePartition->size) {
DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
total_size, WifiOta::instance().m_esp32UpdatePartition->size);
esp32_ota_mutex_release();
return ESP_FAIL;
}
esp_err_t err = esp_ota_begin(WifiOta::instance().m_esp32UpdatePartition, total_size, &WifiOta::instance().m_esp32OtaHandle);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
WifiOta::instance().m_esp32UpdatePartition = NULL;
esp32_ota_mutex_release();
return err;
}
WifiOta::instance().m_esp32Received = 0;
WifiOta::instance().m_esp32OtaActive = true;
// 清理旧状态(SHA-256 将在重启前从分区读回计算,上传期间不再计算)
wifi_esp32_ota_cleanup_sha();
DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", WifiOta::instance().m_esp32UpdatePartition->label);
return ESP_OK;
}
/** /**
* @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启 * @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
* verify_only=true: 仅校验 SHA-256,不设置启动分区,不重启 * verify_only=true: 仅校验 SHA-256,不设置启动分区,不重启
* verify_only=false或缺省: 完整流程(校验 + 设置启动分区 + 重启) * verify_only=false或缺省: 完整流程(校验 + 设置启动分区 + 重启)
*
* 实际的 end/verify/set_boot/restart 逻辑全部委托给 Esp32OtaCore。
* @param req * @param req
* @return * @return
*/ */
static esp_err_t esp32_ota_update_handler(httpd_req_t *req) static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
{ {
http_set_cors_headers(req); http_set_cors_headers(req);
if (!WifiOta::instance().m_esp32OtaActive) { Esp32OtaCore &core = Esp32OtaCore::instance();
if (!core.isActive()) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started"); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
return ESP_FAIL; return ESP_FAIL;
} }
@@ -532,9 +469,6 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
strncpy(frontend_sha_hex, sha_str, 64); strncpy(frontend_sha_hex, sha_str, 64);
frontend_sha_hex[64] = '\0'; frontend_sha_hex[64] = '\0';
has_frontend_sha = true; has_frontend_sha = true;
strncpy(WifiOta::instance().m_frontendSha256, sha_str, 64);
WifiOta::instance().m_frontendSha256[64] = '\0';
WifiOta::instance().m_hasFrontendSha256 = true;
DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex); DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex);
} }
} }
@@ -547,144 +481,54 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
} }
} }
if (verify_only) { char device_sha_hex[65] = {0};
DEBUG_INFO("OTA", "ESP32 OTA verify-only mode, %u bytes", (unsigned)WifiOta::instance().m_esp32Received);
} else {
DEBUG_INFO("OTA", "ESP32 OTA complete, %u bytes, setting boot partition...", (unsigned)WifiOta::instance().m_esp32Received);
esp_err_t err = esp_ota_end(WifiOta::instance().m_esp32OtaHandle); // ── verify_only 模式:只计算并返回设备端 SHA,不 end/set_boot/reboot ──
if (err != ESP_OK) { if (verify_only) {
DEBUG_ERR("OTA", "esp_ota_end failed: %s", esp_err_to_name(err)); DEBUG_INFO("OTA", "ESP32 OTA verify-only mode, %u bytes", (unsigned)core.getReceived());
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle); esp_err_t sha_err = core.computeDeviceSha(device_sha_hex);
esp32_ota_mutex_release(); if (sha_err != ESP_OK) {
WifiOta::instance().m_esp32OtaActive = false; DEBUG_ERR("OTA", "verify-only: SHA 计算失败: %s", esp_err_to_name(sha_err));
WifiOta::instance().m_esp32OtaHandle = 0; httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(sha_err));
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL; return ESP_FAIL;
} }
} char ok_resp[256] = {0};
// ── SHA-256 验证:从 OTA 分区读回并计算哈希 ──
if (has_frontend_sha && WifiOta::instance().m_esp32UpdatePartition != NULL) {
DEBUG_INFO("OTA", "Verifying: reading OTA partition for SHA-256...");
const esp_partition_t *part = WifiOta::instance().m_esp32UpdatePartition;
mbedtls_sha256_context verify_ctx;
mbedtls_sha256_init(&verify_ctx);
mbedtls_sha256_starts(&verify_ctx, 0);
uint8_t *read_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!read_buf) {
DEBUG_ERR("OTA", "SHA-256 verify: malloc failed");
mbedtls_sha256_free(&verify_ctx);
} else {
size_t offset = 0;
while (offset < WifiOta::instance().m_esp32Received) {
size_t to_read = WifiOta::instance().m_esp32Received - offset;
if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
esp_err_t rd_err = esp_partition_read(part, offset, read_buf, to_read);
if (rd_err != ESP_OK) {
DEBUG_ERR("OTA", "SHA-256 verify: partition read failed at offset %u", (unsigned int)offset);
break;
}
mbedtls_sha256_update(&verify_ctx, read_buf, to_read);
offset += to_read;
}
free(read_buf);
mbedtls_sha256_finish(&verify_ctx, WifiOta::instance().m_esp32Sha256);
mbedtls_sha256_free(&verify_ctx);
WifiOta::instance().m_esp32HashComputed = true;
char device_sha_hex[65] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&device_sha_hex[i * 2], sizeof(device_sha_hex) - i * 2, "%02x", WifiOta::instance().m_esp32Sha256[i]);
}
DEBUG_INFO("OTA", "Device SHA-256: %s", device_sha_hex);
bool match = true;
for (int i = 0; i < 64; i++) {
if (tolower((unsigned char)frontend_sha_hex[i]) != tolower((unsigned char)device_sha_hex[i])) {
match = false;
break;
}
}
if (match) {
DEBUG_INFO("OTA", "SHA-256 verified OK");
if (verify_only) {
char ok_resp[256] = {0};
snprintf(ok_resp, sizeof(ok_resp),
"{\"status\": \"ok\", \"message\": \"SHA-256 verified\", \"sha256\": \"%s\"}",
device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, ok_resp, strlen(ok_resp));
return ESP_OK;
}
} else {
DEBUG_ERR("OTA", "SHA-256 mismatch! frontend=%s device=%s, abort", frontend_sha_hex, device_sha_hex);
if (!verify_only) {
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle);
}
wifi_esp32_ota_cleanup_sha();
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
WifiOta::instance().m_esp32UpdatePartition = NULL;
WifiOta::instance().m_esp32Received = 0;
char err_resp[256] = {0};
snprintf(err_resp, sizeof(err_resp),
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", \"frontend\": \"%s\", \"device\": \"%s\"}",
frontend_sha_hex, device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, err_resp, strlen(err_resp));
return ESP_FAIL;
}
}
}
// ── 非verify_only模式:设置启动分区并重启 ──
if (verify_only) {
// 没有前端SHA或验证被跳过,在verify_only下也返回ok
char ok_resp[128] = {0};
snprintf(ok_resp, sizeof(ok_resp), snprintf(ok_resp, sizeof(ok_resp),
"{\"status\": \"ok\", \"message\": \"verify skipped, no SHA-256 provided\"}"); "{\"status\": \"ok\", \"message\": \"SHA-256 computed\", \"sha256\": \"%s\"}",
device_sha_hex);
httpd_resp_set_type(req, "application/json"); httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, ok_resp, strlen(ok_resp)); httpd_resp_send(req, ok_resp, strlen(ok_resp));
return ESP_OK; return ESP_OK;
} }
esp_err_t err = esp_ota_set_boot_partition(WifiOta::instance().m_esp32UpdatePartition); // ── 完整模式:finalize(end → 校验 → set_boot)后由 core 内部重启 ──
if (err != ESP_OK) { DEBUG_INFO("OTA", "ESP32 OTA finalize, %u bytes...", (unsigned)core.getReceived());
DEBUG_ERR("OTA", "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err)); esp_err_t err = core.finalize(has_frontend_sha ? frontend_sha_hex : nullptr,
esp32_ota_mutex_release(); /*reboot=*/true,
WifiOta::instance().m_esp32OtaActive = false; device_sha_hex);
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
return ESP_FAIL;
}
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
DEBUG_INFO("OTA", "ESP32 OTA finished, rebooting...");
char resp[256] = {0}; if (err == ESP_OK) {
if (WifiOta::instance().m_esp32HashComputed) { // 正常不会走到这里(reboot=true 时 core 会 esp_restart)
char device_sha_hex[65] = {0}; char resp[256] = {0};
for (int i = 0; i < 32; ++i) {
snprintf(&device_sha_hex[i * 2], sizeof(device_sha_hex) - i * 2, "%02x", WifiOta::instance().m_esp32Sha256[i]);
}
snprintf(resp, sizeof(resp), snprintf(resp, sizeof(resp),
"{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\", \"sha256\": \"%s\"}", "{\"status\": \"ok\", \"message\": \"OTA finished\"}");
device_sha_hex); httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// 失败:core 已内部清理(abort + mutex release)
if (err == ESP_ERR_INVALID_VERSION) {
char err_resp[256] = {0};
snprintf(err_resp, sizeof(err_resp),
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", \"frontend\": \"%s\", \"device\": \"%s\"}",
frontend_sha_hex, device_sha_hex);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, err_resp, strlen(err_resp));
} else { } else {
snprintf(resp, sizeof(resp), httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
"{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}");
} }
return ESP_FAIL;
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
return ESP_OK;
} }
/** /**
@@ -717,11 +561,10 @@ static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
{ {
http_set_cors_headers(req); http_set_cors_headers(req);
char resp[128]; char resp[128];
if (WifiOta::instance().m_esp32OtaActive) { if (Esp32OtaCore::instance().isActive()) {
snprintf(resp, sizeof(resp), snprintf(resp, sizeof(resp),
"{\"active\": true, \"received\": %u, \"partition\": \"%s\"}", "{\"active\": true, \"received\": %u}",
(unsigned int)WifiOta::instance().m_esp32Received, (unsigned int)Esp32OtaCore::instance().getReceived());
WifiOta::instance().m_esp32UpdatePartition ? WifiOta::instance().m_esp32UpdatePartition->label : "null");
} else { } else {
snprintf(resp, sizeof(resp), "{\"active\": false}"); snprintf(resp, sizeof(resp), "{\"active\": false}");
} }
@@ -820,7 +663,8 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
return ESP_FAIL; return ESP_FAIL;
} }
esp_err_t err = esp32_ota_begin(total_size); Esp32OtaCore &core = Esp32OtaCore::instance();
esp_err_t err = core.begin(total_size);
if (err != ESP_OK) { if (err != ESP_OK) {
if (err == ESP_ERR_INVALID_STATE) { if (err == ESP_ERR_INVALID_STATE) {
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel"); httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "ESP32 OTA occupied by another channel");
@@ -833,11 +677,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
// 堆分配缓冲区,避免栈溢出(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) {
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle); core.abort();
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
return ESP_FAIL; return ESP_FAIL;
} }
@@ -854,31 +694,22 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
if (ret <= 0) { if (ret <= 0) {
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret); DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA", ret);
free(fw_buf); free(fw_buf);
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle); core.abort();
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "HTTP recv failed");
return ESP_FAIL; return ESP_FAIL;
} }
err = esp_ota_write(WifiOta::instance().m_esp32OtaHandle, fw_buf, (size_t)ret); err = core.write(fw_buf, (size_t)ret);
if (err != ESP_OK) { if (err != ESP_OK) {
DEBUG_ERR("WIFI", "OTA write failed"); DEBUG_ERR("WIFI", "OTA write failed: %s", esp_err_to_name(err));
free(fw_buf); free(fw_buf);
esp_ota_abort(WifiOta::instance().m_esp32OtaHandle); core.abort();
esp32_ota_mutex_release();
WifiOta::instance().m_esp32OtaActive = false;
WifiOta::instance().m_esp32OtaHandle = 0;
wifi_esp32_ota_cleanup_sha();
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed"); httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA write failed");
return ESP_FAIL; return ESP_FAIL;
} }
total_firmware_written += ret; total_firmware_written += ret;
total_received += ret; total_received += ret;
WifiOta::instance().m_esp32Received += 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);
@@ -891,7 +722,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
} }
free(fw_buf); free(fw_buf);
DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)WifiOta::instance().m_esp32Received, json_header_len); DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)core.getReceived(), json_header_len);
const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}"; const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
httpd_resp_set_type(req, "application/json"); httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp)); httpd_resp_send(req, resp, strlen(resp));
+3 -13
View File
@@ -3,11 +3,11 @@
* T4.1 - T4.5 * T4.1 - T4.5
* *
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。 * WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
* ESP32 本机 OTA 的写入/收尾逻辑已抽离到 Esp32OtaCore(WiFi/BLE 共用)。
*/ */
#ifndef WIFI_OTA_H #ifndef WIFI_OTA_H
#define WIFI_OTA_H #define WIFI_OTA_H
#include "ota_relay.h"
#include "esp_ota_ops.h" #include "esp_ota_ops.h"
#include "esp_http_server.h" #include "esp_http_server.h"
#include "esp_event.h" #include "esp_event.h"
@@ -31,7 +31,7 @@ public:
static int8_t getRssi(); static int8_t getRssi();
static bool isConnected(); static bool isConnected();
// ── WiFi 连接状态(WiFi 事件回调 / HTTP / reconnect 任务共享,需 mutex 保护)── // ── WiFi 连接状态(WiFi 事件回调 / HTTP / reconnect task 共享,需 mutex 保护)──
bool getStaConnected() const { return m_staConnected.load(); } bool getStaConnected() const { return m_staConnected.load(); }
void setStaConnected(bool v) { m_staConnected.store(v); } void setStaConnected(bool v) { m_staConnected.store(v); }
int getStaRetryCount() const { return m_staRetryCount.load(); } int getStaRetryCount() const { return m_staRetryCount.load(); }
@@ -55,18 +55,8 @@ public:
SemaphoreHandle_t m_staGotIpSem = NULL; SemaphoreHandle_t m_staGotIpSem = NULL;
TaskHandle_t m_reconnectTask = nullptr; TaskHandle_t m_reconnectTask = nullptr;
bool m_esp32OtaActive = false;
esp_ota_handle_t m_esp32OtaHandle = 0;
const esp_partition_t *m_esp32UpdatePartition = NULL;
size_t m_esp32Received = 0;
httpd_handle_t m_httpServer = NULL; httpd_handle_t m_httpServer = NULL;
// ── SHA-256 哈希校验 ──
uint8_t m_esp32Sha256[32] = {0}; // 重启前验证计算的SHA-256哈希值
bool m_esp32HashComputed = false; // 是否已计算哈希
char m_frontendSha256[65] = {0}; // 前端提供的SHA-256(上传时存储)
bool m_hasFrontendSha256 = false; // 前端是否提供了SHA-256
private: private:
// ── 跨任务共享状态(atomic + m_stateMutex 保护)── // ── 跨任务共享状态(atomic + m_stateMutex 保护)──
std::atomic<bool> m_staConnected{false}; std::atomic<bool> m_staConnected{false};
@@ -86,4 +76,4 @@ private:
esp_err_t waitStaConnected(char *ip_str, size_t ip_len); esp_err_t waitStaConnected(char *ip_str, size_t ip_len);
}; };
#endif // WIFI_OTA_H #endif // WIFI_OTA_H
+304
View File
@@ -0,0 +1,304 @@
import sys
with open('main/server/ble_ota.cpp','r',encoding='utf-8-sig') as f:
data=f.read()
# 1. 添加 SHA_MISMATCH 错误码宏
old='#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小'
new='#define BLE_OTA_FINISH_ERR_GD32_SIZE 0x84 // GD32 固件超出分区大小\n#define BLE_OTA_FINISH_ERR_SHA_MISMATCH 0x85 // ESP32 OTA SHA-256 不匹配'
assert old in data, 'macro old not found'
data=data.replace(old,new)
# 2. 修改 ble_esp32_finish_and_reboot_task 接受 frontend SHA 参数
old_task='''static void ble_esp32_finish_and_reboot_task(void *pvParameters)
{
(void)pvParameters;
DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动");
Esp32OtaCore &core = Esp32OtaCore::instance();
// 补发最终 ACK,让前端尽快得知最后一包已收到
if (BleOta::instance().m_esp32LastSeq != 0xFFFFU) {
sendWindowAck(BleOta::instance().m_esp32LastCmd,
BleOta::instance().m_esp32LastSeq,
(uint32_t)core.getReceived());
DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", BleOta::instance().m_esp32LastSeq);
}
vTaskDelay(pdMS_TO_TICKS(150));
if (!core.isActive()) {
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
BleOta::instance().m_esp32RebootPending = false;
vTaskDelete(NULL);
return;
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节", (unsigned)core.getReceived());
// 通知前端:开始 finalize
sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
// 委托核心完成: esp_ota_end → SHA 计算 → set_boot_partition → esp_restart
// BLE 不传前端 SHA(BLE 协议无 finalize 前校验机制),直接应用;SHA 通过 0xC1 上报供前端比对
char device_sha_hex[65] = {0};
esp_err_t err = core.finalize(/*frontend_sha_hex=*/nullptr,
/*reboot=*/true,
device_sha_hex);
// reboot=true 时 finalize 正常不会返回(core 内部 esp_restart)
if (err == ESP_OK) {
// 极端情况:reboot 未执行(理论上不会发生),仍上报并标记
if (device_sha_hex[0] != '\\0') {
// 上报设备端 SHA-256 供前端比对显示([0xC1] + [32 字节])
uint8_t sha_pkt[33] = {0};
sha_pkt[0] = BLE_OTA_NOTIFY_SHA256;
for (int i = 0; i < 32; ++i) {
unsigned int byte = 0;
sscanf(&device_sha_hex[i * 2], "%02x", &byte);
sha_pkt[1 + i] = (uint8_t)byte;
}
BleOta::instance().notifyResponse(sha_pkt, sizeof(sha_pkt));
}
sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, 0);
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
} else {
DEBUG_ERR(TAG, "[FINISH] OTA 收尾失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
BleOta::instance().m_esp32RebootPending = false;
}
vTaskDelete(NULL);
}'''
new_task='''static void ble_esp32_finish_and_reboot_task(void *pvParameters)
{
// pvParameters: heap-allocated char[65] frontend SHA hex string (or nullptr)
char *frontend_sha_hex = (char *)pvParameters;
DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动");
Esp32OtaCore &core = Esp32OtaCore::instance();
// 补发最终 ACK,让前端尽快得知最后一包已收到
if (BleOta::instance().m_esp32LastSeq != 0xFFFFU) {
sendWindowAck(BleOta::instance().m_esp32LastCmd,
BleOta::instance().m_esp32LastSeq,
(uint32_t)core.getReceived());
DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", BleOta::instance().m_esp32LastSeq);
}
vTaskDelay(pdMS_TO_TICKS(150));
if (!core.isActive()) {
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
if (frontend_sha_hex) free(frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = false;
vTaskDelete(NULL);
return;
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节", (unsigned)core.getReceived());
// 通知前端:开始 finalize
sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
char device_sha_hex[65] = {0};
esp_err_t err = core.finalize(frontend_sha_hex,
/*reboot=*/true,
device_sha_hex);
// reboot=true 时 finalize 正常不会返回(core 内部 esp_restart)
if (err == ESP_OK) {
// 极端情况:reboot 未执行(理论上不会发生),仍上报并标记
if (device_sha_hex[0] != '\\0') {
uint8_t sha_pkt[33] = {0};
sha_pkt[0] = BLE_OTA_NOTIFY_SHA256;
for (int i = 0; i < 32; ++i) {
unsigned int byte = 0;
sscanf(&device_sha_hex[i * 2], "%02x", &byte);
sha_pkt[1 + i] = (uint8_t)byte;
}
BleOta::instance().notifyResponse(sha_pkt, sizeof(sha_pkt));
}
sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, 0);
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
} else if (err == ESP_ERR_INVALID_VERSION) {
DEBUG_ERR(TAG, "[FINISH] SHA-256 不匹配,OTA 已中止");
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH, 0);
BleOta::instance().m_esp32RebootPending = false;
} else {
DEBUG_ERR(TAG, "[FINISH] OTA 收尾失败: %s", esp_err_to_name(err));
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
BleOta::instance().m_esp32RebootPending = false;
}
if (frontend_sha_hex) free(frontend_sha_hex);
vTaskDelete(NULL);
}'''
assert old_task in data, 'old_task not found'
data=data.replace(old_task,new_task)
# 3. 修改 ble_ota_ctrl_access: 扩展 buf 到 36,解析前端 SHA
old_ctrl=''' uint8_t buf[4] = {0};
int len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
if (len < 1) {
DEBUG_WARN(TAG, "CTRL 数据为空");
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X", buf[0]);
if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) {
// 检查固件是否存储完毕 → 触发 GD32 OTA 中继
ota_meta_t meta;
esp_err_t err = OtaRelay::metaLoad(&meta);
if (err == ESP_OK &&
meta.total_size > 0 &&
meta.stored_size >= meta.total_size) {
DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继",
meta.stored_size, meta.total_size);
BleOta::instance().signalRelay();
} else {
DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")",
meta.stored_size, meta.total_size);
}
} else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
return 0;
}
if (BleOta::instance().m_esp32RebootPending) {
DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL");
return 0;
}
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
NULL,
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
BleOta::instance().m_esp32RebootPending = false;
} else {
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(由设备端异步完成)");
}
}
// 注: BLE_OTA_CTRL_VERIFY_SHA256 (0xF9) 路径已移除。
// SHA-256 现由 Esp32OtaCore 在 finalize 时统一从分区读回计算,
// 并通过 BLE_OTA_NOTIFY_SHA256 (0xC1) 通知前端比对,校验逻辑与 WiFi 完全一致。'''
new_ctrl=''' uint8_t buf[36] = {0};
int len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
if (len < 1) {
DEBUG_WARN(TAG, "CTRL 数据为空");
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X len=%d", buf[0], len);
if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) {
// 检查固件是否存储完毕 → 触发 GD32 OTA 中继
ota_meta_t meta;
esp_err_t err = OtaRelay::metaLoad(&meta);
if (err == ESP_OK &&
meta.total_size > 0 &&
meta.stored_size >= meta.total_size) {
DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继",
meta.stored_size, meta.total_size);
BleOta::instance().signalRelay();
} else {
DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")",
meta.stored_size, meta.total_size);
}
} else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
return 0;
}
if (BleOta::instance().m_esp32RebootPending) {
DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL");
return 0;
}
// 解析前端 SHA-256: [0xF8] + [32 bytes binary SHA]
char *frontend_sha_hex = nullptr;
if (len >= 33) {
frontend_sha_hex = (char *)malloc(65);
if (frontend_sha_hex) {
for (int i = 0; i < 32; ++i) {
snprintf(&frontend_sha_hex[i * 2], 3, "%02x", buf[1 + i]);
}
frontend_sha_hex[64] = '\\0';
DEBUG_INFO(TAG, "前端 SHA-256: %s", frontend_sha_hex);
}
} else {
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33,无法获取前端 SHA-256");
}
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
frontend_sha_hex, // heap string; task 负责 free
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
if (frontend_sha_hex) free(frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = false;
} else {
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
}
}
// 注: BLE_OTA_CTRL_VERIFY_SHA256 (0xF9) 路径已移除。
// SHA-256 现由 Esp32OtaCore 在 finalize 时统一从分区读回计算,
// 并通过 BLE_OTA_NOTIFY_SHA256 (0xC1) 通知前端比对,校验逻辑与 WiFi 完全一致。'''
assert old_ctrl in data, 'old_ctrl not found'
data=data.replace(old_ctrl,new_ctrl)
# 4. 移除 DATA_IN 中的自动收尾代码(ESP32 收到最后一包后自动创建 finish task)
old_auto=''' if ((total_size > 0 && Esp32OtaCore::instance().getReceived() >= total_size) || isLastPacket) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s",
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1,
isLastPacket ? " [尾包]" : "");
BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq;
if (!BleOta::instance().m_esp32RebootPending) {
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
NULL,
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
ble_esp32_ota_abort(); // core 内部完成 abort + mutex release + 状态复位
BleOta::instance().m_esp32RebootPending = false;
return BLE_ATT_ERR_UNLIKELY;
}
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启");
} else {
DEBUG_WARN(TAG, "ESP32 OTA 收尾任务已存在,忽略重复完成触发");
}
}
}'''
new_auto=''' if ((total_size > 0 && Esp32OtaCore::instance().getReceived() >= total_size) || isLastPacket) {
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)%s",
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1,
isLastPacket ? " [尾包]" : "");
BleOta::instance().m_esp32LastCmd = cmd;
BleOta::instance().m_esp32LastSeq = seq;
// 注:与 WiFi 流程对齐,传输完成后由前端发送 0xF8 + SHA-256 触发 finalize,
// 设备端不再自动创建收尾任务。
}
}'''
assert old_auto in data, 'old_auto not found'
data=data.replace(old_auto,new_auto)
with open('main/server/ble_ota.cpp','w',encoding='utf-8-sig') as f:
f.write(data)
print('ble_ota.cpp updated')
+147
View File
@@ -0,0 +1,147 @@
import sys
path = r'main\server\ble_ota.cpp'
with open(path, 'r', encoding='utf-8-sig') as f:
data = f.read()
# 1. 文件头注释:补充 0xFA 说明
old = ' * - CTRL (0x0001, Write): 0xF7=触发GD32中继, 0xF8=触发ESP32 OTA升级\n'
new = ' * - CTRL (0x0001, Write): 0xF7=触发GD32中继, 0xF8=触发ESP32 OTA升级(含 32B SHA-256 校验并 reboot),0xFA=重启ESP32\n'
assert old in data, 'header old not found'
data = data.replace(old, new, 1)
# 2. 0xF8 分支:移除 "无 SHA 跳过比对" 路径,强制要求 32B SHA
old_f8 = ''' } else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
return 0;
}
if (BleOta::instance().m_esp32RebootPending) {
DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL");
return 0;
}
// 解析前端 SHA-256: [0xF8] + [32 bytes binary SHA]
char *frontend_sha_hex = nullptr;
if (len >= 33) {
frontend_sha_hex = (char *)malloc(65);
if (frontend_sha_hex) {
for (int i = 0; i < 32; ++i) {
snprintf(&frontend_sha_hex[i * 2], 3, "%02x", buf[1 + i]);
}
frontend_sha_hex[64] = '\\0';
DEBUG_INFO(TAG, "前端 SHA-256: %s", frontend_sha_hex);
}
} else {
// 前端未传 SHA,说明已经走过 0xF9 先校验、收尾不再重复比对
DEBUG_INFO(TAG, "CTRL 0xF8 未传入 SHA-256,finalize 将跳过 SHA 比对与重启前控制流");
}
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
frontend_sha_hex, // heap string; task 负责 free
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
if (frontend_sha_hex) free(frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = false;
} else {
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
}
} else if (buf[0] == BLE_OTA_CTRL_VERIFY_SHA256) {
// 仅校验:从 OTA 分区读回计算 SHA-256,通过 0xC1 上报。不 end / 不 set_boot / 不重启。
Esp32OtaCore &core = Esp32OtaCore::instance();
if (!core.isActive()) {
DEBUG_WARN(TAG, "0xF9 请求被拒绝:ESP32 OTA 会话未激活");
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_INVALID_STATE);
return 0;
}
char device_sha_hex[65] = {0};
esp_err_t sha_err = core.computeDeviceSha(device_sha_hex);
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "0xF9 请求:从 OTA 分区计算 SHA-256 失败: %s", esp_err_to_name(sha_err));
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)sha_err);
return 0;
}
uint8_t sha_pkt[33] = {0};
sha_pkt[0] = BLE_OTA_NOTIFY_SHA256;
for (int i = 0; i < 32; ++i) {
unsigned int byte = 0;
sscanf(&device_sha_hex[i * 2], "%02x", &byte);
sha_pkt[1 + i] = (uint8_t)byte;
}
esp_err_t notify_err = BleOta::instance().notifyResponse(sha_pkt, sizeof(sha_pkt));
if (notify_err != ESP_OK) {
DEBUG_WARN(TAG, "0xF9 请求:上报 SHA-256 notify 失败: %s", esp_err_to_name(notify_err));
} else {
DEBUG_INFO(TAG, "0xF9 请求:已上报设备端 SHA-256,前端可继续比对,%s%s",
device_sha_hex, BleOta::instance().m_esp32RebootPending ? "" : "");
}
} else if (buf[0] == BLE_OTA_CTRL_RESTART_ESP32) {'''
new_f8 = ''' } else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
if (!Esp32OtaCore::instance().isActive()) {
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
return 0;
}
if (BleOta::instance().m_esp32RebootPending) {
DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL");
return 0;
}
// 强制要求 32B 前端 SHA-256:与 WiFi 流程对齐,设备端读回分区计算并比对,
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH。
if (len < 33) {
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33(前端 SHA-256 必传),拒绝触发");
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_INVALID_ARG);
return 0;
}
char *frontend_sha_hex = (char *)malloc(65);
if (!frontend_sha_hex) {
DEBUG_ERR(TAG, "CTRL 0xF8 申请内存失败");
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)ESP_ERR_NO_MEM);
return 0;
}
for (int i = 0; i < 32; ++i) {
snprintf(&frontend_sha_hex[i * 2], 3, "%02x", buf[1 + i]);
}
frontend_sha_hex[64] = '\\0';
DEBUG_INFO(TAG, "前端 SHA-256: %s", frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = true;
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
"ble_ota_finish",
4096,
frontend_sha_hex, // heap string; task 负责 free
5,
NULL);
if (ok != pdPASS) {
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
free(frontend_sha_hex);
BleOta::instance().m_esp32RebootPending = false;
} else {
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(前端 SHA 已传入)");
}
} else if (buf[0] == BLE_OTA_CTRL_RESTART_ESP32) {'''
assert old_f8 in data, '0xF8 block old not found'
data = data.replace(old_f8, new_f8, 1)
# 3. 结尾协议说明注释:删除 0xF9 行
old_tail = ''' // 0xF7 = 触发 GD32 中继
// 0xF8 = 触发 ESP32 本机 OTA 收尾(end + set_boot + reboot,默认不重复校验 SHA,前端需先走 0xF9 确认)
// 0xF9 = 仅从 OTA 分区读回计算 SHA-256,通过 0xC1 notify 上报前端(不 end / 不 reboot)
// 0xFA = 重启 ESP32(配套前端 expectedReboot 自动重连)'''
new_tail = ''' // 0xF7 = 触发 GD32 中继
// 0xF8 = 触发 ESP32 本机 OTA 收尾:设备端读 OTA 分区算 SHA 并与前端 32B SHA 比对,
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH(不重启)
// 0xFA = 重启 ESP32(配套前端 expectedReboot 自动重连)'''
assert old_tail in data, 'tail old not found'
data = data.replace(old_tail, new_tail, 1)
with open(path, 'w', encoding='utf-8-sig') as f:
f.write(data)
print('ble_ota.cpp updated')
+37
View File
@@ -0,0 +1,37 @@
path = r'tools\style.css'
with open(path, 'r', encoding='utf-8-sig') as f:
css = f.read()
# 在 .svi-val.err 之后追加 stage-result 样式
old = """.svi-val.err { color: var(--red); font-weight: 600; }"""
new = """.svi-val.err { color: var(--red); font-weight: 600; }
/* Stage result panel - 上位机确认 0x07/0x85 收尾 */
.stage-result {
margin-top: 12px; padding: 14px 16px;
border-radius: var(--radius-sm);
background: var(--bg-elevated); border: 1px solid var(--border);
display: flex; flex-direction: column; gap: 10px;
}
.stage-result.ok { border-color: rgba(34,197,94,.45); background: rgba(34,197,94,.08); }
.stage-result.err { border-color: rgba(239,68,68,.45); background: rgba(239,68,68,.08); }
.stage-result .sr-head { display: flex; align-items: center; gap: 10px; }
.stage-result .sr-ico {
width: 32px; height: 32px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 700; color: #07121c;
}
.stage-result.ok .sr-ico { background: var(--green); }
.stage-result.err .sr-ico { background: var(--red); }
.stage-result .sr-title { font-size: 16px; font-weight: 700; }
.stage-result.ok .sr-title { color: var(--green); }
.stage-result.err .sr-title { color: var(--red); }
.stage-result .sr-detail { font-size: 13px; color: var(--text); line-height: 1.5; }
.stage-result .sr-actions { display: flex; justify-content: flex-end; gap: 8px; }"""
assert old in css, 'svi-val.err CSS not found'
css = css.replace(old, new, 1)
with open(path, 'w', encoding='utf-8-sig') as f:
f.write(css)
print('style.css updated')
+20
View File
@@ -0,0 +1,20 @@
path = r'tools\ble_ota.html'
with open(path, 'r', encoding='utf-8-sig') as f:
html = f.read()
old_hint = " hint: '当前目标:ESP32。页面会使用 <b>cmd=0x01</b> 发送数据,<mark>写入完成后会先发 0xF9 让设备端读回算 SHA-256</mark>,前端比对一致后才发 0xF8 触发收尾(end + set_boot + reboot)。'"
new_hint = " hint: '当前目标:ESP32。页面会使用 <b>cmd=0x01</b> 发送数据,写入完成后发 <b>0xF8</b> + 32 字节 SHA-256 触发设备端收尾:设备端读 OTA 分区算 SHA 并比对,一致则 end+set_boot+reboot;不一致上报 0x85,不重启。'"
print('old in html:', old_hint in html)
html2 = html.replace(old_hint, new_hint, 1)
print('changed:', html2 != html)
print('new still present:', new_hint in html2)
with open(path, 'w', encoding='utf-8-sig') as f:
f.write(html2)
print('written, file len:', len(html2))
# verify
with open(path, 'r', encoding='utf-8-sig') as f:
reread = f.read()
print('re-read len:', len(reread))
print('new in reread:', new_hint in reread)
print('old in reread:', old_hint in reread)
+25
View File
@@ -0,0 +1,25 @@
import os
os.chdir(r'G:\Robot_v2\DCU\ESP32_COMM')
path = 'tools/ble_ota.html'
with open(path, 'r', encoding='utf-8-sig') as f:
html = f.read()
# 计数
print('0xF9 BEFORE:', html.count('0xF9'))
print('shaWaiter BEFORE:', html.count('shaWaiter'))
# 1) TARGETS esp32 hint
old_hint = " hint: '当前目标:ESP32。页面会使用 <b>cmd=0x01</b> 发送数据,<mark>写入完成后会先发 0xF9 让设备端读回算 SHA-256</mark>,前端比对一致后才发 0xF8 触发收尾(end + set_boot + reboot)。'"
new_hint = " hint: 'TEST NEW'"
print('hint in html:', old_hint in html)
html = html.replace(old_hint, new_hint, 1)
with open(path, 'w', encoding='utf-8-sig') as f:
f.write(html)
# 验证
with open(path, 'r', encoding='utf-8-sig') as f:
reread = f.read()
print('TEST NEW present:', 'TEST NEW' in reread)
print('old hint present:', old_hint in reread)
+20
View File
@@ -0,0 +1,20 @@
import os
os.chdir(r'G:\Robot_v2\DCU\ESP32_COMM')
with open('tools/ble_ota.html', 'r', encoding='utf-8-sig') as f:
text = f.read()
# Try a small, single line replace
old = " fileSha256: null,"
print('old line found:', text.count(old))
# Now do a multi-line block test
old_block = """ fileSha256: null,
deviceSha256: null,
expectedReboot: false, // 发送 0xFA 后置 true,重连后清
reconnecting: false, // 自动重连中
reconnectAttempts: 0, // 重连尝试次数
shaWaiter: null, // {resolve, reject, timer} 0xC1 等待者
};"""
print('block found:', old_block in text)
print('block repr (last 200):', repr(old_block[-200:]))
+1 -5
View File
@@ -1390,17 +1390,13 @@ async function pollStatus() {
updateEsp32StagesFromPhase(6); // 启动完成 updateEsp32StagesFromPhase(6); // 启动完成
addLog('I', '系统', 'ESP32 已恢复在线'); addLog('I', '系统', 'ESP32 已恢复在线');
showToast('ESP32 已恢复在线', 'success'); showToast('ESP32 已恢复在线', 'success');
} else if (currentOtaTarget === 'esp32' && esp32OtaPhase >= 3 && esp32OtaPhase < 6) {
updateEsp32StagesFromPhase(6);
addLog('I', '系统', 'ESP32 已恢复在线');
showToast('ESP32 已恢复在线', 'success');
} }
updateSystemStatusUi(data); updateSystemStatusUi(data);
} }
} catch (e) { } catch (e) {
consecutivePollFailures++; consecutivePollFailures++;
// 传输中屏蔽网关无响应的检测功能(上传期间设备忙,无法响应轮询是正常的) // 传输中屏蔽网关无响应的检测功能(上传期间设备忙,无法响应轮询是正常的)
if (consecutivePollFailures === 5 && !pollSlowMode && !isUploading) { if (consecutivePollFailures === 5 && !pollSlowMode && !isUploading && !isEsp32Rebooting) {
addLog('W', '诊断', '设备连续 5 次无响应,正在诊断网络...'); addLog('W', '诊断', '设备连续 5 次无响应,正在诊断网络...');
var diag = await diagnoseNetwork(); var diag = await diagnoseNetwork();
addLog('W', '诊断', diag); addLog('W', '诊断', diag);
+175 -23
View File
@@ -35,6 +35,7 @@
</div> </div>
<div class="sub-actions"> <div class="sub-actions">
<button id="versionBtn" class="btn btn-outline" disabled>读取版本</button> <button id="versionBtn" class="btn btn-outline" disabled>读取版本</button>
<button id="restartBtn" class="btn btn-outline" disabled title="确认后会重启 ESP32,重连后页面会自动恢复">🔄 重启设备</button>
<a class="btn btn-outline" href="index.html" target="_blank">回 WiFi 页面 ↗</a> <a class="btn btn-outline" href="index.html" target="_blank">回 WiFi 页面 ↗</a>
</div> </div>
</div> </div>
@@ -47,7 +48,7 @@
<label class="form-label" for="targetChip">目标芯片</label> <label class="form-label" for="targetChip">目标芯片</label>
<select id="targetChip"> <select id="targetChip">
<option value="gd32">GD32(写入后自动触发中继)</option> <option value="gd32">GD32(写入后自动触发中继)</option>
<option value="esp32">ESP32(写入后重启升级)</option> <option value="esp32" selected>ESP32(写入后重启升级)</option>
</select> </select>
</div> </div>
<div class="row-2"> <div class="row-2">
@@ -149,8 +150,8 @@
<div class="stage-node" id="bs2"><div class="stage-dot">2</div><div class="stage-label">GATT 准备</div><div class="stage-detail" id="bsd2"></div></div> <div class="stage-node" id="bs2"><div class="stage-dot">2</div><div class="stage-label">GATT 准备</div><div class="stage-detail" id="bsd2"></div></div>
<div class="stage-node" id="bs3"><div class="stage-dot">3</div><div class="stage-label">文件准备</div><div class="stage-detail" id="bsd3"></div></div> <div class="stage-node" id="bs3"><div class="stage-dot">3</div><div class="stage-label">文件准备</div><div class="stage-detail" id="bsd3"></div></div>
<div class="stage-node" id="bs4"><div class="stage-dot">4</div><div class="stage-label">数据写入</div><div class="stage-detail" id="bsd4"></div></div> <div class="stage-node" id="bs4"><div class="stage-dot">4</div><div class="stage-label">数据写入</div><div class="stage-detail" id="bsd4"></div></div>
<div class="stage-node" id="bs5"><div class="stage-dot">5</div><div class="stage-label">完成触发</div><div class="stage-detail" id="bsd5"></div></div> <div class="stage-node" id="bs5"><div class="stage-dot">5</div><div class="stage-label" id="bsl5">校验比对</div><div class="stage-detail" id="bsd5"></div></div>
<div class="stage-node" id="bs6"><div class="stage-dot">6</div><div class="stage-label">校验比对</div><div class="stage-detail" id="bsd6"></div></div> <div class="stage-node" id="bs6"><div class="stage-dot">6</div><div class="stage-label" id="bsl6">完成触发</div><div class="stage-detail" id="bsd6"></div></div>
</div> </div>
<div class="stage-status-bar" id="bleStageBar"> <div class="stage-status-bar" id="bleStageBar">
<span class="ssb-dot"></span> <span class="ssb-dot"></span>
@@ -197,7 +198,11 @@
ctrlHex: 'F7', ctrlHex: 'F7',
recommendedPayload: 480, recommendedPayload: 480,
finishText: '自动触发中继', finishText: '自动触发中继',
hint: '当前目标:<b>GD32</b>。页面会使用 <b>cmd=0x00</b> 发送数据,写入完成后自动触发 SPI OTA 中继。' hint: '当前目标:<b>GD32</b>。页面会使用 <b>cmd=0x00</b> 发送数据,写入完成后自动触发 SPI OTA 中继。',
verifyStage: 6,
finalizeStage: 5,
stage5Label: '完成触发',
stage6Label: '校验比对'
}, },
esp32: { esp32: {
label: 'ESP32', label: 'ESP32',
@@ -205,7 +210,11 @@
ctrlHex: 'F8', ctrlHex: 'F8',
recommendedPayload: 480, recommendedPayload: 480,
finishText: '自动完成 OTA', finishText: '自动完成 OTA',
hint: '当前目标:<b>ESP32</b>。页面会使用 <b>cmd=0x01</b> 发送数据,最后一包到齐后由设备端自动完成 OTA 并重启。' hint: '当前目标:ESP32。页面会使用 <b>cmd=0x01</b> 发送数据,写入完成后发 <b>0xF8</b> + 32 字节 SHA-256 触发设备端收尾:设备端读 OTA 分区算 SHA 并比对,一致则 end+set_boot+reboot;不一致上报 0x85,不重启。',
verifyStage: 5,
finalizeStage: 6,
stage5Label: '校验比对',
stage6Label: '完成触发'
} }
}; };
@@ -231,6 +240,10 @@
ackWaiter: null, ackWaiter: null,
fileSha256: null, fileSha256: null,
deviceSha256: null, deviceSha256: null,
expectedReboot: false, // 发送 0xFA 后置 true,重连后清
reconnecting: false, // 自动重连中
reconnectAttempts: 0, // 重连尝试次数
uploadTargetKey: null,
}; };
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@@ -258,6 +271,7 @@
otaState: $('otaState'), otaState: $('otaState'),
fileInfo: $('fileInfo'), fileInfo: $('fileInfo'),
clearFileBtn: $('clearFileBtn'), clearFileBtn: $('clearFileBtn'),
restartBtn: $('restartBtn'),
progressFill: $('progressFill'), progressFill: $('progressFill'),
progressText: $('progressText'), progressText: $('progressText'),
progressPercent: $('progressPercent'), progressPercent: $('progressPercent'),
@@ -273,6 +287,10 @@
return TARGETS[els.targetChip.value] || TARGETS.gd32; return TARGETS[els.targetChip.value] || TARGETS.gd32;
} }
function flowTarget() {
return TARGETS[state.uploadTargetKey] || currentTarget();
}
const MAX_LOG_LINES = 1000; const MAX_LOG_LINES = 1000;
function log(tag, message, level = 'I') { function log(tag, message, level = 'I') {
@@ -298,6 +316,14 @@
// ====== BLE OTA Stage Flow Management ====== // ====== BLE OTA Stage Flow Management ======
var bleStageIds = ['bs1','bs2','bs3','bs4','bs5','bs6']; var bleStageIds = ['bs1','bs2','bs3','bs4','bs5','bs6'];
var bleStageDetailIds = ['bsd1','bsd2','bsd3','bsd4','bsd5','bsd6']; var bleStageDetailIds = ['bsd1','bsd2','bsd3','bsd4','bsd5','bsd6'];
var bleStageLabelIds = ['bsl5', 'bsl6'];
function updateBleStageLabels(cfg) {
var stage5 = document.getElementById(bleStageLabelIds[0]);
var stage6 = document.getElementById(bleStageLabelIds[1]);
if (stage5) stage5.textContent = cfg.stage5Label || '完成触发';
if (stage6) stage6.textContent = cfg.stage6Label || '校验比对';
}
function resetBleStages() { function resetBleStages() {
bleStageIds.forEach(function(id) { bleStageIds.forEach(function(id) {
@@ -442,6 +468,10 @@
} }
els.disconnectBtn.disabled = !connected; els.disconnectBtn.disabled = !connected;
els.versionBtn.disabled = !connected; els.versionBtn.disabled = !connected;
els.targetChip.disabled = state.uploading;
els.payloadSize.disabled = state.uploading;
els.ctrlHex.disabled = state.uploading;
if (els.restartBtn) els.restartBtn.disabled = !connected || state.reconnecting || state.expectedReboot || state.uploading;
els.uploadBtn.disabled = !idle; els.uploadBtn.disabled = !idle;
els.autoFlashBtn.disabled = !idle; els.autoFlashBtn.disabled = !idle;
els.abortBtn.disabled = !state.uploading; els.abortBtn.disabled = !state.uploading;
@@ -502,9 +532,10 @@
function refreshTargetUi() { function refreshTargetUi() {
const cfg = currentTarget(); const cfg = currentTarget();
updateBleStageLabels(cfg);
els.ctrlHex.value = cfg.ctrlHex; els.ctrlHex.value = cfg.ctrlHex;
els.payloadSize.value = String(cfg.recommendedPayload); els.payloadSize.value = String(cfg.recommendedPayload);
els.protocolHint.innerHTML = `${cfg.hint} 推荐 payload:<b>${cfg.recommendedPayload}B</b>。协议格式:<b>cmd(1) + seq(2) + total_size(4) + payload</b>。<br><b>传输机制:</b>每 <b>${BLE_OTA_ACK_WINDOW}</b> 包等待一次 DATA_OUT ACK。${cfg.label === 'ESP32' ? ' <br><b>ESP32 模式:</b>最后一包到齐后由设备端自动完成 OTA 并重启。': '<br><b>GD32 模式:</b>数据写入后自动触发 SPI OTA 中继。'}`; els.protocolHint.innerHTML = `${cfg.hint} 推荐 payload:<b>${cfg.recommendedPayload}B</b>。协议格式:<b>cmd(1) + seq(2) + total_size(4) + payload</b>。<br><b>传输机制:</b>每 <b>${BLE_OTA_ACK_WINDOW}</b> 包等待一次 DATA_OUT ACK。${cfg.label === 'ESP32' ? ' <br><b>ESP32 模式:</b>数据写入完成后发送 <mark>0xF8 + 32B 文件 SHA-256</mark> 触发设备端收尾;设备端自行读回 OTA 分区、完成 SHA-256 比对,并决定是否 end + set_boot + reboot。': '<br><b>GD32 模式:</b>数据写入后自动触发 SPI OTA 中继。'}`;
if (state.selectedFile) updateFileInfo(state.selectedFile); if (state.selectedFile) updateFileInfo(state.selectedFile);
setConnectionState(!!state.server); setConnectionState(!!state.server);
log('CFG', `目标切换为 ${cfg.label}: packetCmd=0x${cfg.packetCmd.toString(16).padStart(2, '0').toUpperCase()}, payload=${cfg.recommendedPayload}`); log('CFG', `目标切换为 ${cfg.label}: packetCmd=0x${cfg.packetCmd.toString(16).padStart(2, '0').toUpperCase()}, payload=${cfg.recommendedPayload}`);
@@ -528,7 +559,7 @@
}); });
state.device = device; state.device = device;
state.device.addEventListener('gattserverdisconnected', onDisconnected); state.device.addEventListener('gattserverdisconnected', onDisconnected, { once: true });
els.deviceName.textContent = device.name || '未知设备'; els.deviceName.textContent = device.name || '未知设备';
els.deviceId.textContent = device.id || '-'; els.deviceId.textContent = device.id || '-';
@@ -567,10 +598,20 @@
function onDisconnected() { function onDisconnected() {
log('BLE', '设备已断开', 'W'); log('BLE', '设备已断开', 'W');
var wasExpected = state.expectedReboot;
resetUiAfterDisconnect(); resetUiAfterDisconnect();
if (wasExpected && state.device) {
// 是主动重启引起的断开,启动自动重连
state.reconnecting = true;
state.reconnectAttempts = 0;
tryReconnect();
}
} }
async function disconnectBle() { async function disconnectBle() {
// 用户主动断开:取消任何进行中的重连 / expectedReboot
state.expectedReboot = false;
state.reconnecting = false;
try { try {
if (state.dataOutChar && state.notificationsEnabled) { if (state.dataOutChar && state.notificationsEnabled) {
await state.dataOutChar.stopNotifications(); await state.dataOutChar.stopNotifications();
@@ -653,17 +694,17 @@
}; };
const finishText = finishMap[stage] || `[FINISH] 未知阶段 stage=0x${stage.toString(16).padStart(2, '0').toUpperCase()} value=${value}`; const finishText = finishMap[stage] || `[FINISH] 未知阶段 stage=0x${stage.toString(16).padStart(2, '0').toUpperCase()} value=${value}`;
log('FINISH', finishText); log('FINISH', finishText);
const finalizeStage = TARGETS.esp32.finalizeStage;
// Update BLE stage flow based on FINISH notifications // Update BLE stage flow based on FINISH notifications
if (stage >= 0x01 && stage <= 0x04) { if (stage >= 0x01 && stage <= 0x04) {
setBleStage(5, 'OTA 刷写中...', null); setBleStage(finalizeStage, 'OTA 刷写中...', null);
setBleStageText(finishText.replace('[FINISH] ', '')); setBleStageText(finishText.replace('[FINISH] ', ''));
} else if (stage >= 0x05 && stage <= 0x06) { } else if (stage >= 0x05 && stage <= 0x06) {
setBleStage(5, '等待重启...', null); setBleStage(finalizeStage, '等待重启...', null);
setBleStageText(finishText.replace('[FINISH] ', '')); setBleStageText(finishText.replace('[FINISH] ', ''));
} else if (stage === 0x07) { } else if (stage === 0x07) {
setBleStage(5, '即将重启', null); setBleStage(finalizeStage, '设备重启', null);
setBleStage(6, '设备重启', null);
setBleStageText('ESP32 即将重启'); setBleStageText('ESP32 即将重启');
var bar = document.getElementById('bleStageBar'); var bar = document.getElementById('bleStageBar');
if (bar) bar.className = 'stage-status-bar ok'; if (bar) bar.className = 'stage-status-bar ok';
@@ -686,6 +727,13 @@
state.lastUploadStatus = 'failed'; state.lastUploadStatus = 'failed';
setBleStageError('OTA 刷写失败'); setBleStageError('OTA 刷写失败');
showBleVerifyInfo(state.fileSha256 || '-', state.deviceSha256 || '设备未上报', { text: '刷写失败', err: true }); showBleVerifyInfo(state.fileSha256 || '-', state.deviceSha256 || '设备未上报', { text: '刷写失败', err: true });
} else if (stage === 0x85) {
showToast('ESP32 OTA 校验失败,未触发重启', 'error');
setProgress(0, '设备端 SHA-256 校验失败');
state.lastUploadStatus = 'failed';
setBleStage(5, 'SHA-256 ✗', null);
setBleStageError('设备端 SHA-256 校验失败');
showBleVerifyInfo(state.fileSha256 || '-', state.deviceSha256 || '设备未上报', { text: '设备端校验失败,已中止 OTA', err: true });
} else if (stage === 0x07) { } else if (stage === 0x07) {
showToast('ESP32 OTA 刷写完成,等待重启', 'success'); showToast('ESP32 OTA 刷写完成,等待重启', 'success');
// If device SHA-256 hasn't arrived yet, show what we have // If device SHA-256 hasn't arrived yet, show what we have
@@ -716,21 +764,22 @@
state.deviceSha256 = devSha; state.deviceSha256 = devSha;
log('VERIFY', '设备端 SHA-256: ' + devSha); log('VERIFY', '设备端 SHA-256: ' + devSha);
// Compare with front-end SHA-256 // 设备端上报 SHA-256 后,前端仅做展示与状态同步。
var frontSha = state.fileSha256; var frontSha = state.fileSha256;
var verifyStage = flowTarget().verifyStage;
if (frontSha && frontSha.toUpperCase() === devSha) { if (frontSha && frontSha.toUpperCase() === devSha) {
log('VERIFY', 'SHA-256 校验通过,前端与设备端一致', 'I'); log('VERIFY', 'SHA-256 校验通过,前端与设备端一致', 'I');
showBleVerifyInfo(frontSha, devSha, { text: '校验通过 — SHA-256 一致', ok: true }); showBleVerifyInfo(frontSha, devSha, { text: '校验通过 — SHA-256 一致', ok: true });
setBleStage(6, 'SHA-256 ✓', null); setBleStage(verifyStage, 'SHA-256 ✓', null);
setBleStageText('校验通过 — 固件完整性已确认'); setBleStageText('校验通过 — 固件完整性已确认');
} else if (frontSha) { } else if (frontSha) {
log('VERIFY', 'SHA-256 校验失败!前端: ' + frontSha + ' 设备: ' + devSha, 'E'); log('VERIFY', 'SHA-256 校验失败!前端: ' + frontSha + ' 设备: ' + devSha, 'E');
showBleVerifyInfo(frontSha, devSha, { text: '校验失败 — SHA-256 不一致', err: true }); showBleVerifyInfo(frontSha, devSha, { text: '校验失败 — SHA-256 不一致', err: true });
setBleStage(6, 'SHA-256 ✗', null); setBleStage(verifyStage, 'SHA-256 ✗', null);
setBleStageError('SHA-256 不一致'); setBleStageError('SHA-256 不一致');
} else { } else {
showBleVerifyInfo(undefined, devSha, { text: '前端 SHA-256 不可用,无法比对' }); showBleVerifyInfo(undefined, devSha, { text: '前端 SHA-256 不可用,无法比对' });
setBleStage(6, '无法比对', null); setBleStage(verifyStage, '无法比对', null);
} }
return; return;
} }
@@ -739,6 +788,88 @@
log('NOTIFY', hex); log('NOTIFY', hex);
} }
// ===== 重启设备 + 自动重连 =====
async function restartEsp32() {
if (!state.ctrlChar || !state.device) return;
if (!confirm('确定要重启 ESP32 吗?\n\n重启后页面会自动重连。')) return;
if (state.expectedReboot) return;
state.expectedReboot = true;
state.reconnectAttempts = 0;
els.connState.textContent = '重启中';
if (els.connState.parentElement) els.connState.parentElement.className = 'pill processing';
if (els.otaState) { els.otaState.textContent = '重启中'; els.otaState.className = 'v warn'; }
setConnectionState(!!state.server);
log('SYS', '=== 触发 ESP32 重启 (0xFA) ===');
showToast('ESP32 重启中...', 'success');
try {
await state.ctrlChar.writeValue(new Uint8Array([0xFA]));
log('SYS', '0xFA 已发,等待设备端推送 FINISH_REBOOT_NOW 后断开');
} catch (e) {
log('SYS', '0xFA 发送失败: ' + e.message, 'E');
showToast('0xFA 发送失败', 'error');
state.expectedReboot = false;
setConnectionState(!!state.server);
}
}
async function tryReconnect() {
if (!state.device || !state.expectedReboot) {
state.reconnecting = false;
return;
}
if (!state.device.gatt) {
// 设备引用已丧失,重连不了
state.reconnecting = false;
state.expectedReboot = false;
showToast('设备引用丧失,请手动重连', 'error');
return;
}
state.reconnecting = true;
state.reconnectAttempts += 1;
var attempt = state.reconnectAttempts;
// 退避:1s, 2s, 4s, 8s, 上限 10s
var backoff = Math.min(10000, 1000 * Math.pow(1.6, attempt - 1));
if (backoff > 200) await sleep(backoff);
els.connState.textContent = '重连中(' + attempt + ')';
if (els.connState.parentElement) els.connState.parentElement.className = 'pill processing';
if (els.otaState) { els.otaState.textContent = '重连中'; els.otaState.className = 'v warn'; }
setConnectionState(!!state.server);
log('BLE', '自动重连第 ' + attempt + ' 次' + (backoff > 0 ? '(等待 ' + Math.round(backoff) + 'ms)' : '') + '...', 'W');
try {
var server = await state.device.gatt.connect();
state.server = server;
state.service = await server.getPrimaryService(UUIDS.service);
state.ctrlChar = await state.service.getCharacteristic(UUIDS.ctrl);
state.dataInChar = await state.service.getCharacteristic(UUIDS.dataIn);
state.dataOutChar = await state.service.getCharacteristic(UUIDS.dataOut);
state.versionChar = await state.service.getCharacteristic(UUIDS.version);
state.notificationsEnabled = false;
await enableNotifications();
state.expectedReboot = false;
state.reconnecting = false;
state.reconnectAttempts = 0;
// 重连后重注 onDisconnected({once: true},旧的已被上次断开时消耗)
state.device.addEventListener('gattserverdisconnected', onDisconnected, { once: true });
log('BLE', '✅ 自动重连成功', 'I');
showToast('BLE 已重连', 'success');
setConnectionState(true);
// 重连后默认刷新一下版本
try { await readVersion(); } catch (_) {}
} catch (e) {
log('BLE', '重连第 ' + attempt + ' 次失败: ' + e.message, 'W');
if (state.expectedReboot && attempt < 12) {
tryReconnect();
} else {
state.reconnecting = false;
state.expectedReboot = false;
showToast('重连失败,请手动连接', 'error');
log('BLE', '自动重连放弃,请点击「连接设备」重连', 'E');
setConnectionState(!!state.server);
}
}
}
function waitForAck(cmd, expectedSeq, timeoutMs = 8000, opts = {}) { function waitForAck(cmd, expectedSeq, timeoutMs = 8000, opts = {}) {
const gen = state.ackGeneration; const gen = state.ackGeneration;
if (state.lastAckCmd === cmd && state.lastAckSeq >= expectedSeq && gen === state.ackGeneration) { if (state.lastAckCmd === cmd && state.lastAckSeq >= expectedSeq && gen === state.ackGeneration) {
@@ -937,6 +1068,7 @@
if (!state.selectedFile || !state.dataInChar) return false; if (!state.selectedFile || !state.dataInChar) return false;
const cfg = currentTarget(); const cfg = currentTarget();
state.uploadTargetKey = els.targetChip.value;
const payloadSize = Math.max(32, Math.min(498, Number(els.payloadSize.value) || 480)); const payloadSize = Math.max(32, Math.min(498, Number(els.payloadSize.value) || 480));
const file = state.selectedFile; const file = state.selectedFile;
@@ -1079,15 +1211,33 @@
setProgress(100, `[${cfg.label}] 固件数据已全部写入 DATA_IN`); setProgress(100, `[${cfg.label}] 固件数据已全部写入 DATA_IN`);
state.lastUploadStatus = 'success'; state.lastUploadStatus = 'success';
// Stage 5: Completion trigger // Stage 5: 发送 0xF8 + 32B 文件 SHA-256,由设备端执行收尾并完成比对
if (cfg.label === 'ESP32') { if (cfg.label === 'ESP32') {
setBleStage(5, '自动完成', null);
setBleStage(5, '完成', null);
setBleStage(6, '等待设备校验...', null);
setBleStageText('ESP32 OTA 数据写入完成,等待设备端处理与校验');
// Show verify info — wait for device SHA-256
state.deviceSha256 = null; state.deviceSha256 = null;
showBleVerifyInfo(fileSha256.toUpperCase(), '等待设备上报...', { text: '等待设备端处理并上报 SHA-256' }); const frontSha = fileSha256.toUpperCase();
const frontShaBytes = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
frontShaBytes[i] = parseInt(frontSha.slice(i * 2, i * 2 + 2), 16);
}
const trigger = new Uint8Array(33);
trigger[0] = 0xF8;
trigger.set(frontShaBytes, 1);
showBleVerifyInfo(frontSha, '等待设备端比对...', { text: '已发送文件 SHA-256,等待设备端校验结果' });
setBleStage(5, '发送校验...', 'active');
setBleStageText('发送 0xF8 + SHA-256,等待设备端收尾...');
try {
await state.ctrlChar.writeValue(trigger);
log('OTA', '0xF8 + 32B SHA-256 已发送,等待设备端比对并收尾');
} catch (e) {
state.lastUploadStatus = 'failed';
setProgress(0, `0xF8 发送失败: ${e.message}`);
log('OTA', `0xF8 发送失败: ${e.message}`, 'E');
setBleStageError('0xF8 发送失败');
return false;
}
setBleStage(5, '等待设备端校验', null);
setBleStage(6, '设备端收尾中', 'active');
setBleStageText('0xF8 已发,等待设备端 SHA 比对与 end+set_boot+reboot...');
return true; return true;
} }
@@ -1140,6 +1290,7 @@
state.ackWaiter = null; state.ackWaiter = null;
} }
state.uploading = false; state.uploading = false;
state.uploadTargetKey = null;
setConnectionState(!!state.server); setConnectionState(!!state.server);
} }
} }
@@ -1190,7 +1341,8 @@
els.connectBtn.addEventListener('click', connectBle); els.connectBtn.addEventListener('click', connectBle);
els.disconnectBtn.addEventListener('click', disconnectBle); els.disconnectBtn.addEventListener('click', disconnectBle);
els.versionBtn.addEventListener('click', readVersion); if (els.versionBtn) els.versionBtn.addEventListener('click', readVersion);
if (els.restartBtn) els.restartBtn.addEventListener('click', restartEsp32);
els.uploadBtn.addEventListener('click', uploadFirmware); els.uploadBtn.addEventListener('click', uploadFirmware);
els.autoFlashBtn.addEventListener('click', autoFlash); els.autoFlashBtn.addEventListener('click', autoFlash);
els.abortBtn.addEventListener('click', abortTask); els.abortBtn.addEventListener('click', abortTask);
+1 -1
View File
@@ -128,7 +128,7 @@
</div> </div>
<div style="margin-top:12px;"> <div style="margin-top:12px;">
<button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button> <button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button>
<a class="btn btn-outline btn-block" href="ble_ota.html?v=20260509-0046" target="_blank" style="margin-top:10px;text-decoration:none;"> <a class="btn btn-outline btn-block" href="ble_ota.html?v=20260614-ota-order-fix" target="_blank" style="margin-top:10px;text-decoration:none;">
切换到 BLE 蓝牙刷写 ↗ 切换到 BLE 蓝牙刷写 ↗
</a> </a>
</div> </div>
+23
View File
@@ -474,6 +474,29 @@ select option { background: var(--bg-panel); color: var(--text); }
.svi-val.ok { color: var(--green); font-weight: 600; } .svi-val.ok { color: var(--green); font-weight: 600; }
.svi-val.err { color: var(--red); font-weight: 600; } .svi-val.err { color: var(--red); font-weight: 600; }
/* Stage result panel - 上位机确认 0x07/0x85 收尾 */
.stage-result {
margin-top: 12px; padding: 14px 16px;
border-radius: var(--radius-sm);
background: var(--bg-elevated); border: 1px solid var(--border);
display: flex; flex-direction: column; gap: 10px;
}
.stage-result.ok { border-color: rgba(34,197,94,.45); background: rgba(34,197,94,.08); }
.stage-result.err { border-color: rgba(239,68,68,.45); background: rgba(239,68,68,.08); }
.stage-result .sr-head { display: flex; align-items: center; gap: 10px; }
.stage-result .sr-ico {
width: 32px; height: 32px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 18px; font-weight: 700; color: #07121c;
}
.stage-result.ok .sr-ico { background: var(--green); }
.stage-result.err .sr-ico { background: var(--red); }
.stage-result .sr-title { font-size: 16px; font-weight: 700; }
.stage-result.ok .sr-title { color: var(--green); }
.stage-result.err .sr-title { color: var(--red); }
.stage-result .sr-detail { font-size: 13px; color: var(--text); line-height: 1.5; }
.stage-result .sr-actions { display: flex; justify-content: flex-end; gap: 8px; }
/* === LOG PANEL === */ /* === LOG PANEL === */
.log-panel { display: flex; flex-direction: column; min-height: 380px; } .log-panel { display: flex; flex-direction: column; min-height: 380px; }
.log-panel .card-body { flex: 1; display: flex; flex-direction: column; padding: 0; } .log-panel .card-body { flex: 1; display: flex; flex-direction: column; padding: 0; }