feat(ota): 会话 epoch/通道标识 + BLE 中止命令 + NVS 线程安全

- 新增 ota_channel_t(WiFi/BLE) + m_otaEpoch 会话代次,
  abortCurrentIfEpoch 防止同类型重启后旧 handler 误清新会话
- 同通道再次发起 OTA 时自动中止旧会话并重开
- 统一 prepareGd32Storage(validate+begin+erase+meta),
  消除 BLE/WiFi 两路径隐式差异
- OtaRelay 拆分子步骤(doRelayPackets/doFinishAndVerify),
  状态成员私有化消除数据竞争
- Gd32FlashStore NVS 读写加 mutex,metaUpdate 原子化
- BLE OTA 新增 0xFB 中止命令(保持连接)+ 0x20 ABORTED 处理
- HTTP handler 改 per-request buffer 支持并发
- 修复 CRITICAL 状态 RSSI 恢复无法降级的缺陷
- main.cpp 关键任务失败触发看门狗重启
- style.css 增强动画 + prefers-reduced-motion 兼容
This commit is contained in:
小龙虾
2026-06-28 04:32:18 +08:00
parent 6ef97307c3
commit a93dba60d2
41 changed files with 3142 additions and 6349 deletions
+40 -25
View File
@@ -69,7 +69,7 @@ Esp32OtaCore& Esp32OtaCore::instance()
/**
* @brief 复位所有状态变量。
*/
*/
void Esp32OtaCore::resetState()
{
m_active = false;
@@ -81,6 +81,39 @@ void Esp32OtaCore::resetState()
m_ended = false;
}
/**
* @brief begin() 失败后的统一清理。
* 仅释放 mutex + 复位状态(不调 esp_ota_abort,
* begin 失败时 m_handle 未成功创建,无需也不能 abort)。
* @param code 返回给调用方的错误码。
* @return 透传 code。
*/
esp_err_t Esp32OtaCore::beginCleanupAndReturn(esp_err_t code)
{
releaseMutex();
resetState();
return code;
}
/**
* @brief finalize() 失败后的统一清理。
* 若 esp_ota_end 未成功(m_ended=false),调 esp_ota_abort 释放 handle;
* 然后复位 m_ended、释放 mutex、复位状态,返回 code。
* 与现有 abort() 的 m_ended 守卫语义一致,避免 end 成功后误调 abort。
* @param code 返回给调用方的错误码。
* @return 透传 code。
*/
esp_err_t Esp32OtaCore::finalizeCleanupAndReturn(esp_err_t code)
{
if (!m_ended) {
esp_ota_abort(m_handle);
}
m_ended = false;
releaseMutex();
resetState();
return code;
}
/**
* @brief 开启 OTA 会话。
* @param total_size 固件总大小(字节)。
@@ -125,8 +158,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
if (m_partition == NULL) {
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
releaseMutex();
return ESP_FAIL;
return beginCleanupAndReturn(ESP_FAIL);
}
DEBUG_INFO(TAG, "OTA 目标分区: %s, size=%u, address=0x%lx",
@@ -135,9 +167,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
if (total_size > m_partition->size) {
DEBUG_ERR(TAG, "固件太大: %u > 分区大小: %u",
(unsigned)total_size, (unsigned)m_partition->size);
m_partition = nullptr;
releaseMutex();
return ESP_FAIL;
return beginCleanupAndReturn(ESP_FAIL);
}
// BLE OTA 修复:传固件实际大小而非分区全尺寸,避免全分区擦除导致
@@ -151,10 +181,7 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
esp_err_t err = esp_ota_begin(m_partition, erase_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;
releaseMutex();
return err;
return beginCleanupAndReturn(err);
}
m_received = 0;
@@ -280,11 +307,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
esp_err_t sha_err = computeShaFromPartition();
if (sha_err != ESP_OK) {
DEBUG_ERR(TAG, "SHA 计算失败: %s,中止", esp_err_to_name(sha_err));
esp_ota_abort(m_handle);
m_ended = false;
releaseMutex();
resetState();
return sha_err;
return finalizeCleanupAndReturn(sha_err);
}
char device_hex[65] = {0};
@@ -311,11 +334,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
} else {
DEBUG_ERR(TAG, "SHA-256 不匹配: frontend=%s device=%s",
frontend_sha_hex, device_hex);
esp_ota_abort(m_handle);
m_ended = false;
releaseMutex();
resetState();
return ESP_ERR_INVALID_VERSION;
return finalizeCleanupAndReturn(ESP_ERR_INVALID_VERSION);
}
} else if (out_device_sha_hex) {
// 未提供前端 SHA,但仍计算设备端 SHA 供输出
@@ -332,11 +351,7 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
if (err != ESP_OK) {
DEBUG_ERR(TAG, "esp_ota_end 失败: %s (0x%x)", esp_err_to_name(err), (unsigned)err);
// end 失败时仍可 abort(未真正 end)
esp_ota_abort(m_handle);
m_ended = false;
releaseMutex();
resetState();
return err;
return finalizeCleanupAndReturn(err);
}
m_ended = true; // ★ end 成功,后续不得再 abort
+18
View File
@@ -130,6 +130,24 @@ private:
*/
void resetState();
// begin() 失败后的统一清理:释放 mutex + resetState() + 返回 code
// 不调 esp_ota_abort(begin 失败时 m_handle 未成功创建)
/**
* @brief begin() 失败后的统一清理。
* @param code 返回给调用方的错误码。
* @return 透传 code。
*/
esp_err_t beginCleanupAndReturn(esp_err_t code);
// finalize() 失败后的统一清理:若 m_ended=false 调 esp_ota_abort 释放 handle;
// 再复位 m_ended + 释放 mutex + resetState() + 返回 code
/**
* @brief finalize() 失败后的统一清理。
* @param code 返回给调用方的错误码。
* @return 透传 code。
*/
esp_err_t finalizeCleanupAndReturn(esp_err_t code);
bool m_active = false;
esp_ota_handle_t m_handle = 0;
const esp_partition_t *m_partition = nullptr;
+216 -88
View File
@@ -124,41 +124,110 @@ void OtaCoordinator::init()
// ============================================================
// GD32 存储阶段
// ============================================================
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size)
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t channel)
{
bool h = lock();
// ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程
if (h) watchdogCheckLocked();
// ★ 同类型重启:若上一次 GD32 存储仍在进行中(STORING)且来自同一通道,
// 自动中止旧存储并重新开始。覆盖"上传过程中刷新网页再上传"场景。
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
// RELAYING 是中继阶段,不可中断,也拒绝。
if (h && m_sysState == OTA_SYS_GD32_STORING) {
if (m_otaChannel != channel) {
DEBUG_WARN(TAG, "GD32 存储中(通道=%d),拒绝跨通道请求(通道=%d)",
(int)m_otaChannel, (int)channel);
unlock(h);
return ESP_ERR_INVALID_STATE;
}
DEBUG_WARN(TAG, "GD32 存储仍在进行中(同通道),中止旧存储并重新开始");
Gd32FlashStore::instance().metaClear();
m_sysState = OTA_SYS_IDLE;
m_busySinceTick = 0;
// epoch 将在下方设置 GD32_STORING 后递增
}
// 系统忙检查:仅拒绝真正"在传输"的活跃状态
// STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖
if (h && (m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
m_sysState == OTA_SYS_ESP32_REBOOTING ||
m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING)) {
DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState);
unlock(h);
return ESP_ERR_INVALID_STATE;
}
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),先清理再开始新存储
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),允许新存储覆盖。
// 注意:清理 NVS meta 不在此处做,避免与紧跟的 prepareGd32Storage 内
// clearGd32Meta + saveGd32Meta 重复一次写入,仅做状态注释说明。
if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) {
DEBUG_INFO(TAG, "清理旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
Gd32FlashStore::instance().metaClear();
DEBUG_INFO(TAG, "覆盖旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
}
if (h) {
m_otaEpoch++; // ★ 新会话开始,递增 epoch
m_otaChannel = channel; // ★ 记录发起通道
m_sysState = OTA_SYS_GD32_STORING;
m_gd32StorageDone = false;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING", total_size);
DEBUG_INFO(TAG, "GD32 固件存储开始: %" PRIu32 " 字节, 系统状态 → GD32_STORING (epoch=%u, channel=%d)",
total_size, m_otaEpoch, (int)channel);
}
unlock(h);
return h ? ESP_OK : ESP_ERR_INVALID_STATE;
}
// GD32 Flash 扇区大小(与 http_handlers.cpp 中的常量保持一致,用于擦除时向上对齐)
static constexpr uint32_t kGd32FlashSectorSize = 4096U;
esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t channel)
{
// 1. 校验固件大小(统一 BLE/HTTP 两路径的大小检查)
esp_err_t err = validateGd32Size(total_size);
if (err != ESP_OK) {
DEBUG_WARN(TAG, "prepareGd32Storage: 固件大小非法 raw=%" PRIu32, total_size);
return err;
}
// 2. 进入 GD32_STORING 状态(系统忙则拒绝)
err = beginGd32Storage(total_size, channel);
if (err != ESP_OK) {
DEBUG_WARN(TAG, "prepareGd32Storage: 协调器拒绝存储(系统忙)");
return err;
}
// 3. 擦除 GD32 OTA 分区(按扇区大小向上对齐)
// ★ 原 BLE 路径未擦除分区,此处统一补齐,确保两路径行为一致。
const uint32_t erase_size =
((total_size + kGd32FlashSectorSize - 1U) / kGd32FlashSectorSize) * kGd32FlashSectorSize;
DEBUG_INFO(TAG, "prepareGd32Storage: 擦除 gd32_ota 分区 %u bytes (fw=%" PRIu32 ")",
(unsigned)erase_size, total_size);
err = eraseGd32Partition(erase_size);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "prepareGd32Storage: 擦除失败: %s", esp_err_to_name(err));
abortCurrent();
return err;
}
// 4. 清空旧 meta + 写入初始 meta(total_size + stored_size=0 + last_seq=0)
clearGd32Meta();
ota_meta_t meta = {};
meta.total_size = total_size;
meta.stored_size = 0;
meta.last_seq = 0;
err = saveGd32Meta(&meta);
if (err != ESP_OK) {
DEBUG_ERR(TAG, "prepareGd32Storage: 元数据保存失败: %s", esp_err_to_name(err));
abortCurrent();
return err;
}
return ESP_OK;
}
void OtaCoordinator::completeGd32Storage()
{
bool h = lock();
@@ -175,47 +244,9 @@ void OtaCoordinator::completeGd32Storage()
// ============================================================
// GD32 存储操作(传输层统一入口,解决 H3 绕过协调器问题)
// ── 以下 8 个 wrapper 已 inline 化至 ota_coordinator.h,直接转发到
// Gd32FlashStore::instance(),消除跨 TU 调用开销。
// ============================================================
esp_err_t OtaCoordinator::writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
{
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
}
esp_err_t OtaCoordinator::saveGd32Meta(const ota_meta_t *meta)
{
return Gd32FlashStore::instance().metaSave(meta);
}
esp_err_t OtaCoordinator::loadGd32Meta(ota_meta_t *meta)
{
return Gd32FlashStore::instance().metaLoad(meta);
}
esp_err_t OtaCoordinator::updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
{
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
}
esp_err_t OtaCoordinator::clearGd32Meta()
{
return Gd32FlashStore::instance().metaClear();
}
esp_err_t OtaCoordinator::verifyGd32Firmware(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32])
{
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
}
esp_err_t OtaCoordinator::finalizeGd32Checksum()
{
return Gd32FlashStore::instance().metaFinalizeChecksum();
}
esp_err_t OtaCoordinator::eraseGd32Partition(uint32_t size)
{
return Gd32FlashStore::instance().erasePartition(size);
}
// ============================================================
// GD32 中继阶段
@@ -369,25 +400,42 @@ void OtaCoordinator::notifyBleCoexEnd()
// ============================================================
// ESP32 OTA 阶段
// ============================================================
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t channel)
{
bool h = lock();
// ★ 卡死自愈检查
if (h) watchdogCheckLocked();
// 系统忙检查:GD32 传输/中继进行中,或另一通道的 ESP32 OTA 进行中,均拒绝。
// ★ 必须显式拒绝 ESP32_RECEIVING/FINALIZING/REBOOTING:
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
// FINALIZING/REBOOTING 是不可逆阶段,也拒绝。
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
if (m_otaChannel != channel) {
DEBUG_WARN(TAG, "ESP32 OTA 接收中(通道=%d),拒绝跨通道请求(通道=%d)",
(int)m_otaChannel, (int)channel);
unlock(h);
return ESP_ERR_INVALID_STATE;
}
DEBUG_WARN(TAG, "ESP32 OTA 仍在接收中(同通道),中止旧会话并重新开始");
Esp32OtaCore::instance().abort();
m_sysState = OTA_SYS_IDLE;
m_busySinceTick = 0;
// epoch 将在下方 begin 成功后递增,自动使旧 handler 的清理代码失效
}
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 收尾/重启中,均拒绝。
// ★ 必须显式拒绝 ESP32_FINALIZING/REBOOTING:
// 若放行,本调用会落到 Esp32OtaCore::begin 去抢全局互斥失败而返回 INVALID_STATE,
// 随后 beginEsp32OtaWithRetry 的 forceCleanup 会把另一通道的活跃会话
// (esp_ota_abort + 释放互斥)误中止 → 两通道并发写同一 OTA 分区。
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
m_sysState == OTA_SYS_GD32_RELAYING ||
m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
DEBUG_WARN(TAG, "系统忙 (state=%d, %s),拒绝 ESP32 OTA",
(int)m_sysState, getSystemStateString());
(int)m_sysState, stateToString(m_sysState));
unlock(h);
return ESP_ERR_INVALID_STATE;
}
@@ -397,9 +445,12 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
// 委托 Esp32OtaCore 处理分区/互斥逻辑
err = Esp32OtaCore::instance().begin(total_size);
if (err == ESP_OK) {
m_otaEpoch++; // ★ 新会话开始,递增 epoch(使旧 handler 的 abortCurrentIfEpoch 失效)
m_otaChannel = channel; // ★ 记录发起通道
m_sysState = OTA_SYS_ESP32_RECEIVING;
m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING", total_size);
DEBUG_INFO(TAG, "ESP32 OTA 开始: %" PRIu32 " 字节, 系统状态 → ESP32_RECEIVING (epoch=%u, channel=%d)",
total_size, m_otaEpoch, (int)channel);
} else {
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
}
@@ -409,9 +460,9 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size)
return err;
}
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel)
{
esp_err_t err = beginEsp32Ota(total_size);
esp_err_t err = beginEsp32Ota(total_size, channel);
if (err == ESP_ERR_INVALID_STATE) {
// ★ 仅当系统确实空闲(无任何活跃 OTA)时才允许 forceCleanup:
// 这种情况只可能是"锁残留"——前一次 OTA 异常退出没释放全局互斥,
@@ -419,15 +470,17 @@ esp_err_t OtaCoordinator::beginEsp32OtaWithRetry(uint32_t total_size)
// 若系统正处于任何活跃 OTA(ESP32/GD32),绝不强制清理——那会
// 把另一通道正在进行的 esp_ota_handle 中止、释放互斥,导致
// 两通道并发写同一 OTA 分区(之前 BLE 与 WiFi 可同时 OTA 的根因)。
if (!isIdle()) {
// 避免连续 isIdle() + getSystemState() 各加锁一次(共 2 次锁)。
ota_system_state_t cur = getSystemState();
if (!isIdleState(cur)) {
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝且系统非空闲 (state=%d),不强制清理,直接失败",
(int)getSystemState());
(int)cur);
return err;
}
// 协调器空闲但 Esp32OtaCore.begin 仍被拒 → 锁残留,强制清理后重试
DEBUG_WARN(TAG, "ESP32 OTA 被拒绝但系统空闲,判定为锁残留,强制清理状态机后重试");
Esp32OtaCore::instance().forceCleanup();
err = beginEsp32Ota(total_size);
err = beginEsp32Ota(total_size, channel);
}
return err;
}
@@ -507,29 +560,60 @@ int OtaCoordinator::esp32FinalizeErrCategory(esp_err_t err)
// ============================================================
// 中止
// ============================================================
void OtaCoordinator::doAbortLocked()
{
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d, epoch=%u)", (int)m_sysState, m_otaEpoch);
if (m_sysState == OTA_SYS_GD32_STORING) {
Gd32FlashStore::instance().metaClear();
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
OtaRelay::instance().reset();
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
Esp32OtaCore::instance().abort();
}
m_sysState = OTA_SYS_IDLE;
m_gd32StorageDone = false;
m_busySinceTick = 0;
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
}
void OtaCoordinator::abortCurrent()
{
bool h = lock();
if (h) {
DEBUG_INFO(TAG, "中止当前 OTA 操作 (state=%d)", (int)m_sysState);
if (m_sysState == OTA_SYS_GD32_STORING) {
Gd32FlashStore::instance().metaClear();
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
OtaRelay::instance().reset();
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
Esp32OtaCore::instance().abort();
}
m_sysState = OTA_SYS_IDLE;
m_gd32StorageDone = false;
m_busySinceTick = 0; // ★ 离开忙状态
DEBUG_INFO(TAG, "已中止, 系统状态 → IDLE");
doAbortLocked();
}
unlock(h);
}
uint32_t OtaCoordinator::getOtaEpoch() const
{
bool h = lock();
uint32_t epoch = m_otaEpoch;
unlock(h);
return epoch;
}
bool OtaCoordinator::abortCurrentIfEpoch(uint32_t epoch)
{
bool h = lock();
if (!h) return false;
if (m_otaEpoch != epoch) {
// ★ epoch 不匹配:新会话已接管,旧 handler 不应中止
unlock(h);
return false;
}
// epoch 匹配:执行中止(共享 doAbortLocked,已持锁)
doAbortLocked();
unlock(h);
return true;
}
// ============================================================
// 状态查询
// ============================================================
@@ -543,35 +627,61 @@ ota_system_state_t OtaCoordinator::getSystemState() const
return s;
}
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
bool OtaCoordinator::isIdle() const
ota_channel_t OtaCoordinator::getOtaChannel() const
{
return (getSystemState() == OTA_SYS_IDLE ||
getSystemState() == OTA_SYS_GD32_STORED ||
getSystemState() == OTA_SYS_GD32_COMPLETE);
bool h = lock();
ota_channel_t ch = m_otaChannel;
unlock(h);
return ch;
}
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
bool OtaCoordinator::isGd32Busy() const
/** @brief 判断给定状态值是否属于空闲态(不加锁,纯判断)。
* 供持锁路径或已取一次状态的调用方复用,避免连续加锁。 */
bool OtaCoordinator::isIdleState(ota_system_state_t s) const
{
return (s == OTA_SYS_IDLE ||
s == OTA_SYS_GD32_STORED ||
s == OTA_SYS_GD32_COMPLETE);
}
/** @brief 判断给定状态值是否属于 GD32 忙态(不加锁,纯判断)。 */
bool OtaCoordinator::isGd32BusyState(ota_system_state_t s) const
{
ota_system_state_t s = getSystemState();
return (s == OTA_SYS_GD32_STORING ||
s == OTA_SYS_GD32_RELAYING);
}
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
bool OtaCoordinator::isEsp32Busy() const
/** @brief 判断给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。 */
bool OtaCoordinator::isEsp32BusyState(ota_system_state_t s) const
{
ota_system_state_t s = getSystemState();
return (s == OTA_SYS_ESP32_RECEIVING ||
s == OTA_SYS_ESP32_FINALIZING ||
s == OTA_SYS_ESP32_REBOOTING);
}
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。 */
const char* OtaCoordinator::getSystemStateString() const
/** @brief 判断系统是否处于空闲状态(IDLE/STORED/COMPLETE)。 */
bool OtaCoordinator::isIdle() const
{
switch (getSystemState()) {
return isIdleState(getSystemState());
}
/** @brief 判断 GD32 通道是否正忙(STORING 或 RELAYING)。 */
bool OtaCoordinator::isGd32Busy() const
{
return isGd32BusyState(getSystemState());
}
/** @brief 判断 ESP32 通道是否正忙(RECEIVING/FINALIZING/REBOOTING)。 */
bool OtaCoordinator::isEsp32Busy() const
{
return isEsp32BusyState(getSystemState());
}
/** @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。 */
const char* OtaCoordinator::stateToString(ota_system_state_t s)
{
switch (s) {
case OTA_SYS_IDLE: return "IDLE";
case OTA_SYS_GD32_STORING: return "GD32_STORING";
case OTA_SYS_GD32_STORED: return "GD32_STORED";
@@ -585,6 +695,13 @@ const char* OtaCoordinator::getSystemStateString() const
}
}
/** @brief 获取系统状态的字符串名称(用于日志和前端显示)。
* 委托 stateToString(getSystemState()),保持向后兼容。 */
const char* OtaCoordinator::getSystemStateString() const
{
return stateToString(getSystemState());
}
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
bool OtaCoordinator::getGd32AppAlive() const
{
@@ -608,14 +725,22 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
{
OtaSnapshot snap{};
// ── coordinator 自身状态(一次 mutex)──
// ── coordinator 自身状态(一次 mutex,避免二次加锁导致快照不一致)──
// 持锁时一次性取 m_sysState,并调不加锁的 stateToString()
// 就地转字符串。旧实现释放锁后再调 getSystemStateString() 二次加锁,
// 两次取值之间状态可能变化,导致 snap.sys_state 与 snap.sys_state_str
// 描述不同时刻状态。
bool h = lock();
if (h) {
snap.sys_state = m_sysState;
snap.storage_done = m_gd32StorageDone;
// 就地把状态枚举转字符串,与 sys_state 来自同一时刻
snap.sys_state_str = stateToString(snap.sys_state);
unlock(h);
} else {
// 加锁失败时也填一个与默认 sys_state(=IDLE) 一致的字符串
snap.sys_state_str = stateToString(snap.sys_state);
}
snap.sys_state_str = getSystemStateString();
// ── GD32 NVS 元数据 ──
Gd32FlashStore::instance().metaLoad(&snap.meta);
@@ -629,6 +754,9 @@ OtaSnapshot OtaCoordinator::getSnapshot() const
// ============================================================
// GD32 APP 探测(PR2: 从 /system/status 移出)
// ============================================================
// 注:本函数与 OtaRelay::probeAndCacheAppAlive 逻辑相似,但因依赖方向
// (OtaCoordinator → OtaRelay)不应调用 OtaRelay 的内部实现细节,
// 故此处保留独立实现。
esp_err_t OtaCoordinator::probeGd32AppOnce(gd32_version_t *out_version, uint32_t *out_uptime)
{
uint32_t uptime = 0;
+155 -12
View File
@@ -19,6 +19,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_err.h"
#include "app_config.h" // OTA_MAX_FIRMWARE_SIZE / GD32_OTA_PACKET_SIZE(validateGd32Size 用)
#include "storage/gd32_flash_store.h"
#include "relay/ota_relay.h" // IRelayListener 接口定义 + RelaySnapshot(H4 解耦)
@@ -37,6 +38,14 @@ typedef enum {
OTA_SYS_ERROR // 错误状态
} ota_system_state_t;
// ============================================================
// OTA 传输通道标识(用于同类型重启时区分同通道/跨通道)
// ============================================================
typedef enum {
OTA_CHANNEL_WIFI = 0,
OTA_CHANNEL_BLE = 1,
} ota_channel_t;
// ============================================================
// OTA 系统全量只读快照(PR1: 收口状态读取)
// ============================================================
@@ -108,9 +117,21 @@ public:
* @brief 传输层开始接收 GD32 固件前调用。
* 若系统正忙(ESP32 OTA 进行中或 GD32 中继中),返回失败。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙。
*/
esp_err_t beginGd32Storage(uint32_t total_size);
esp_err_t beginGd32Storage(uint32_t total_size, ota_channel_t channel);
/**
* @brief 统一 GD32 OTA 写入前的准备工作:校验大小 + begin + 擦除分区 + 清空 meta + 写入初始 meta。
* 供 BLE seq=0 首包 / HTTP GD32 upload(非 prepared 复用路径)调用,
* 消除两路径对"擦除/meta 初始化"序列的隐式差异(原 BLE 路径未擦除分区)。
* 任意步骤失败时内部调 abortCurrent 清理,调用方无需再清理。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE)。
* @return ESP_OK 成功;ESP_ERR_INVALID_SIZE 大小非法;ESP_ERR_INVALID_STATE 系统忙;其他擦除/写入错误。
*/
esp_err_t prepareGd32Storage(uint32_t total_size, ota_channel_t channel);
/**
* @brief GD32 固件接收完毕时调用。
@@ -127,21 +148,30 @@ public:
* @param len 数据长度。
* @return ESP_OK 成功。
*/
esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len);
inline esp_err_t writeGd32Chunk(uint32_t offset, const uint8_t *data, size_t len)
{
return Gd32FlashStore::instance().flashWriteChunk(offset, data, len);
}
/**
* @brief 保存 GD32 OTA 元数据。
* @param meta 元数据结构体。
* @return ESP_OK 成功。
*/
esp_err_t saveGd32Meta(const ota_meta_t *meta);
inline esp_err_t saveGd32Meta(const ota_meta_t *meta)
{
return Gd32FlashStore::instance().metaSave(meta);
}
/**
* @brief 加载 GD32 OTA 元数据。
* @param meta 输出元数据结构体。
* @return ESP_OK 成功。
*/
esp_err_t loadGd32Meta(ota_meta_t *meta);
inline esp_err_t loadGd32Meta(ota_meta_t *meta)
{
return Gd32FlashStore::instance().metaLoad(meta);
}
/**
* @brief 更新 GD32 OTA 元数据的进度。
@@ -149,13 +179,19 @@ public:
* @param last_seq 最后包序号。
* @return ESP_OK 成功。
*/
esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq);
inline esp_err_t updateGd32Meta(uint32_t stored_size, uint32_t last_seq)
{
return Gd32FlashStore::instance().metaUpdate(stored_size, last_seq);
}
/**
* @brief 清除 GD32 OTA 元数据。
* @return ESP_OK 成功。
*/
esp_err_t clearGd32Meta();
inline esp_err_t clearGd32Meta()
{
return Gd32FlashStore::instance().metaClear();
}
/**
* @brief 校验前端 SHA-256 与设备端固件是否一致。
@@ -163,21 +199,52 @@ public:
* @param out_device_sha 输出设备端 32 字节 SHA(可为 nullptr)。
* @return ESP_OK 匹配,ESP_ERR_INVALID_CRC 不匹配。
*/
esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32]);
inline esp_err_t verifyGd32Firmware(const uint8_t frontend_sha32[32],
uint8_t out_device_sha[32])
{
return Gd32FlashStore::instance().verifyFrontendSha(frontend_sha32, out_device_sha);
}
/**
* @brief 计算 GD32 固件 SHA-256 并写入元数据。
* @return ESP_OK 成功。
*/
esp_err_t finalizeGd32Checksum();
inline esp_err_t finalizeGd32Checksum()
{
return Gd32FlashStore::instance().metaFinalizeChecksum();
}
/**
* @brief 擦除 GD32 OTA 分区。
* @param size 擦除大小。
* @return ESP_OK 成功。
*/
esp_err_t eraseGd32Partition(uint32_t size);
inline esp_err_t eraseGd32Partition(uint32_t size)
{
return Gd32FlashStore::instance().erasePartition(size);
}
/**
* @brief 校验 GD32 固件大小是否合法(BLE/HTTP 双通道共用)。
*
* 统一规则:raw==0 或 raw>MAX 拒绝;按 GD32_OTA_PACKET_SIZE 向上取整后仍>MAX 拒绝。
* 调用方各自翻译为通道特定错误码(BLE: BLE_OTA_FINISH_ERR_GD32_SIZE;HTTP: HTTPD_500)。
*
* @param raw_size 固件原始字节数。
* @return ESP_OK 合法;ESP_ERR_INVALID_SIZE 非法。
*/
inline esp_err_t validateGd32Size(uint32_t raw_size) const
{
if (raw_size == 0U || raw_size > (uint32_t)OTA_MAX_FIRMWARE_SIZE) {
return ESP_ERR_INVALID_SIZE;
}
const uint32_t packet_size = (uint32_t)GD32_OTA_PACKET_SIZE;
const uint32_t rounded = ((raw_size + packet_size - 1U) / packet_size) * packet_size;
if (rounded > (uint32_t)OTA_MAX_FIRMWARE_SIZE) {
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
// ── GD32 中继阶段 ──
/**
@@ -205,17 +272,19 @@ public:
* @brief ESP32 OTA 开始接收固件。
* 内部委托 Esp32OtaCore::begin()。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 系统忙或通道被占用。
*/
esp_err_t beginEsp32Ota(uint32_t total_size);
esp_err_t beginEsp32Ota(uint32_t total_size, ota_channel_t channel);
/**
* @brief 发起 ESP32 OTA,若被 ESP_ERR_INVALID_STATE 拒绝则 forceCleanup 后重试一次。
* 封装 BLE/WiFi 两端完全相同的重试模式,调用方只需调一次。
* @param total_size 固件总字节数。
* @param channel 发起通道(WiFi/BLE)。
* @return ESP_OK 成功;其他 = 协调器拒绝或 Esp32OtaCore::begin 失败。
*/
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size);
esp_err_t beginEsp32OtaWithRetry(uint32_t total_size, ota_channel_t channel);
/**
* @brief ESP32 OTA 固件接收完毕,准备收尾。
@@ -257,6 +326,25 @@ public:
*/
void abortCurrent();
/**
* @brief 获取当前 OTA 会话的 epoch(代次)。
* 每次 beginEsp32Ota/beginGd32Storage 成功时递增。
* 传输层 handler 在 begin 成功后捕获 epoch,
* 在失败清理时通过 abortCurrentIfEpoch() 检查是否仍为当前会话,
* 避免旧 handler 的清理代码误中止新会话(同类型重启场景)。
* @return 当前 epoch 值。
*/
uint32_t getOtaEpoch() const;
/**
* @brief 仅当 epoch 匹配当前会话时才中止 OTA。
* 用于旧 handler 的失败清理路径:若新请求已经接管(epoch 不匹配),
* 旧 handler 不应调 abortCurrent,否则会误中止新会话。
* @param epoch 调用方在 begin 成功时捕获的 epoch。
* @return true 已中止(epoch 匹配),false 未中止(新会话已接管)。
*/
bool abortCurrentIfEpoch(uint32_t epoch);
// ── 状态查询 ──
/**
* @brief 一次性读取系统 OTA 全量状态快照(PR1: 收口状态读取)。
@@ -282,26 +370,68 @@ public:
*/
ota_system_state_t getSystemState() const;
/**
* @brief 获取当前 OTA 会话的发起通道(WiFi/BLE)。
* 仅在活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义;
* IDLE 等非活跃状态返回值未定义(默认 OTA_CHANNEL_WIFI)。
* @return 当前会话通道枚举值。
*/
ota_channel_t getOtaChannel() const;
/**
* @brief 把状态枚举转成可读字符串(不加锁,纯函数)。
* 供持锁代码路径和已取一次状态值的调用方复用,避免二次加锁。
* @param s 状态枚举值。
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
*/
static const char* stateToString(ota_system_state_t s);
/**
* @brief 检查给定状态值是否属于空闲态(不加锁,纯判断)。
* 调用方先 getSystemState() 取一次再传入,避免连续加锁。
* @param s 状态枚举值。
* @return true 表示 IDLE/STORED/COMPLETE。
*/
bool isIdleState(ota_system_state_t s) const;
/**
* @brief 检查给定状态值是否属于 GD32 忙态(不加锁,纯判断)。
* @param s 状态枚举值。
* @return true 表示 STORING 或 RELAYING。
*/
bool isGd32BusyState(ota_system_state_t s) const;
/**
* @brief 检查给定状态值是否属于 ESP32 忙态(不加锁,纯判断)。
* @param s 状态枚举值。
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
*/
bool isEsp32BusyState(ota_system_state_t s) const;
/**
* @brief 检查系统是否空闲(可接受新的 OTA 请求)。
* 内部仍加锁取一次状态后调 isIdleState()。
* @return true 系统空闲(IDLE/STORED/COMPLETE)。
*/
bool isIdle() const;
/**
* @brief 检查是否有 GD32 相关操作正在进行。
* 内部仍加锁取一次状态后调 isGd32BusyState()。
* @return true 表示 STORING 或 RELAYING。
*/
bool isGd32Busy() const;
/**
* @brief 检查是否有 ESP32 OTA 正在进行。
* 内部仍加锁取一次状态后调 isEsp32BusyState()。
* @return true 表示 RECEIVING/FINALIZING/REBOOTING。
*/
bool isEsp32Busy() const;
/**
* @brief 获取系统状态的可读字符串描述。
* 内部委托 stateToString(getSystemState()),保持向后兼容。
* @return 状态名称字符串(如 "IDLE"、"GD32_STORING" 等)。
*/
const char* getSystemStateString() const;
@@ -385,6 +515,10 @@ private:
// 必须在已持锁状态下调用。
void watchdogCheckLocked();
// ★ 已持锁版本的中止序列:根据当前 m_sysState 调用对应子模块的清理,
// 并把状态机复位为 IDLE。abortCurrent / abortCurrentIfEpoch 共享此实现。
void doAbortLocked();
// 系统状态
ota_system_state_t m_sysState = OTA_SYS_IDLE;
@@ -394,6 +528,15 @@ private:
// GD32 存储跟踪
bool m_gd32StorageDone = false;
// OTA 会话代次:每次 beginEsp32Ota/beginGd32Storage 成功时递增。
// 传输层 handler 在 begin 后捕获 epoch,失败清理时通过 abortCurrentIfEpoch
// 检查 epoch 是否仍匹配,避免旧 handler 误中止同类型重启后的新会话。
uint32_t m_otaEpoch = 0;
// 当前 OTA 会话的发起通道(WiFi/BLE),用于同类型重启时区分同通道/跨通道。
// 仅在同类型活跃状态(ESP32_RECEIVING / GD32_STORING)下有意义。
ota_channel_t m_otaChannel = OTA_CHANNEL_WIFI;
// 互斥锁(M1/L3: mutable 允许在 const 方法中加锁,消除 const_cast)
mutable SemaphoreHandle_t m_mutex = nullptr;