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
+301 -252
View File
@@ -27,6 +27,30 @@
// ★ H4 解耦:OtaRelay 不再直接 #include "ota_coordinator.h",
// 通过 IRelayListener 接口回调通知协调器,依赖方向变为单向。
// ============================================================
// 公共拒绝事件判断(文件内工具)
// ============================================================
// 统一各状态 handler 头部的 ABORT/TIMEOUT/HEALTH_CRIT 拒绝判断。
// 返回 OTA_RESULT_OK 表示非拒绝事件;其他为对应错误码。
// allowHealth: 是否将 RELAY_EVT_HEALTH_CRIT 视为拒绝(仅 TRANSFERRING 传 true)。
static ota_result_t commonReject(relay_event_t e, bool allowHealth = false)
{
if (e == RELAY_EVT_ABORT) return OTA_RESULT_ERR_USER_ABORT;
if (e == RELAY_EVT_TIMEOUT) return OTA_RESULT_ERR_TIMEOUT;
if (allowHealth && e == RELAY_EVT_HEALTH_CRIT) return OTA_RESULT_ERR_HEALTH;
return OTA_RESULT_OK;
}
// ============================================================
// doRelayPackets 重试阈值(与历史代码字面值保持一致,仅命名化)
// ============================================================
// 原代码中 retry_count 阈值:行 1015/1075/1084 用 10U,行 1032/1050 用 3U。
// 含义:NACK/轮询耗尽属于"通信异常",给予较多重试机会(10次);
// 结构化错误属于"GD32 报告具体错误",较少重试(3次)。
static constexpr uint32_t kMaxResendOnNack = 10U; // ACK 失败/轮询耗尽重发上限
static constexpr uint32_t kMaxResendOnStructError = 3U; // 结构化错误重发上限
static constexpr uint32_t kLastPacketAckSeq = 0xF0F0F0F0U; // 末包 ACK 序号约定
// ============================================================
// 静态成员定义
// ============================================================
@@ -177,7 +201,7 @@ TaskHandle_t OtaRelay::getTaskHandle()
// ============================================================
ota_result_t OtaRelay::processEvent(relay_event_t event, const uint8_t *payload, size_t len)
{
relay_state_t cur = relayState;
relay_state_t cur = getState();
// ERROR 状态:仅接受 reset(通过 handleError 的 EVT_START → IDLE)
if (cur == RELAY_ERROR) {
@@ -237,16 +261,11 @@ ota_result_t OtaRelay::handleHandshaking(relay_event_t event, const uint8_t *pay
(void)payload;
(void)len;
if (event == RELAY_EVT_ABORT) {
DEBUG_ERR("OTA", "HANDSHAKING 收到 ABORT");
ota_result_t rej = commonReject(event);
if (rej != OTA_RESULT_OK) {
DEBUG_ERR("OTA", "HANDSHAKING 拒绝事件: %d", (int)event);
setState(RELAY_ERROR);
return OTA_RESULT_ERR_USER_ABORT;
}
if (event == RELAY_EVT_TIMEOUT) {
DEBUG_ERR("OTA", "HANDSHAKING 超时");
setState(RELAY_ERROR);
return OTA_RESULT_ERR_TIMEOUT;
return rej;
}
if (event != RELAY_EVT_START && event != RELAY_EVT_RESP_OK) {
@@ -291,16 +310,11 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
(void)payload;
(void)len;
if (event == RELAY_EVT_ABORT) {
DEBUG_ERR("OTA", "SIZE_NEG 收到 ABORT");
ota_result_t rej = commonReject(event);
if (rej != OTA_RESULT_OK) {
DEBUG_ERR("OTA", "SIZE_NEG 拒绝事件: %d", (int)event);
setState(RELAY_ERROR);
return OTA_RESULT_ERR_USER_ABORT;
}
if (event == RELAY_EVT_TIMEOUT) {
DEBUG_ERR("OTA", "SIZE_NEG 超时");
setState(RELAY_ERROR);
return OTA_RESULT_ERR_TIMEOUT;
return rej;
}
// 加载元数据获取固件大小
@@ -324,8 +338,8 @@ ota_result_t OtaRelay::handleSizeNeg(relay_event_t event, const uint8_t *payload
m_transferCurrentSeq = 0;
m_transferFlashOffset = 0;
m_transferRetryCount = 0;
relayTotalPackets = m_transferTotalPackets;
relaySeq = 0;
setRelayTotalPackets(m_transferTotalPackets);
setRelaySeq(0);
setState(RELAY_TRANSFERRING);
DEBUG_INFO("OTA", "=== 阶段2 完成: 总包数=%" PRIu32 ", → TRANSFERRING ===", m_transferTotalPackets);
@@ -340,16 +354,11 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa
(void)payload;
(void)len;
if (event == RELAY_EVT_ABORT) {
DEBUG_ERR("OTA", "TRANSFERRING 收到 ABORT");
ota_result_t rej = commonReject(event, /*allowHealth=*/true);
if (rej != OTA_RESULT_OK) {
DEBUG_ERR("OTA", "TRANSFERRING 拒绝事件: %d", (int)event);
setState(RELAY_ERROR);
return OTA_RESULT_ERR_USER_ABORT;
}
if (event == RELAY_EVT_HEALTH_CRIT) {
DEBUG_ERR("OTA", "TRANSFERRING 健康检查危险,中止");
setState(RELAY_ERROR);
return OTA_RESULT_ERR_HEALTH;
return rej;
}
DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets);
@@ -373,16 +382,11 @@ ota_result_t OtaRelay::handleFinishing(relay_event_t event, const uint8_t *paylo
(void)payload;
(void)len;
if (event == RELAY_EVT_ABORT) {
DEBUG_ERR("OTA", "FINISHING 收到 ABORT");
ota_result_t rej = commonReject(event);
if (rej != OTA_RESULT_OK) {
DEBUG_ERR("OTA", "FINISHING 拒绝事件: %d", (int)event);
setState(RELAY_ERROR);
return OTA_RESULT_ERR_USER_ABORT;
}
if (event == RELAY_EVT_TIMEOUT) {
DEBUG_ERR("OTA", "FINISHING 超时");
setState(RELAY_ERROR);
return OTA_RESULT_ERR_TIMEOUT;
return rej;
}
DEBUG_INFO("OTA", "=== 阶段4-5: 结束OTA + APP验证 ===");
@@ -469,31 +473,21 @@ ota_result_t OtaRelay::forwardFirmware()
global_timeout_ms, (uint32_t)start_tick, (uint32_t)m_transferDeadline);
// ── 驱动状态机 ──
// 步骤0: IDLE → HANDSHAKING(handleIdle 内部 reset() + setState(HANDSHAKING))
ota_result_t res = processEvent(RELAY_EVT_START, nullptr, 0);
if (res != OTA_RESULT_OK) goto cleanup;
// 5 步顺序推进:IDLE → HANDSHAKING → SIZE_NEG → TRANSFERRING → FINISHING → DONE
// 每步同步执行对应 handler(内部完成协议交互并推进状态)。
// 任一步返回非 OK 即跳出并复位。
ota_result_t res = OTA_RESULT_OK;
while (true) {
relay_state_t cur = getState();
if (cur == RELAY_DONE || cur == RELAY_ERROR) break;
res = processEvent(RELAY_EVT_START, nullptr, 0);
if (res != OTA_RESULT_OK) {
break;
}
}
// 步骤1: HANDSHAKING(内部完成握手+认证 → SIZE_NEG)
// ★ 注意:processEvent(START) 在 HANDSHAKING handler 内同步执行 doHandshakeReq + doChallengeAndAuth
// 期间 SPI 阻塞轮询 8~30 秒。超时检查必须放在同步 handler 执行之后。
res = processEvent(RELAY_EVT_START, nullptr, 0);
if (res != OTA_RESULT_OK) goto cleanup;
// 步骤2: SIZE_NEG(内部完成大小协商 → TRANSFERRING)
res = processEvent(RELAY_EVT_START, nullptr, 0);
if (res != OTA_RESULT_OK) goto cleanup;
// 步骤3: TRANSFERRING(内部完成所有数据包 → FINISHING)
res = processEvent(RELAY_EVT_START, nullptr, 0);
if (res != OTA_RESULT_OK) goto cleanup;
// 步骤4: FINISHING(结束OTA + APP验证 → DONE)
res = processEvent(RELAY_EVT_START, nullptr, 0);
if (res != OTA_RESULT_OK) goto cleanup;
cleanup:
if (res != OTA_RESULT_OK) {
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)relayState);
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
reset();
}
return res;
@@ -951,6 +945,158 @@ ota_result_t OtaRelay::doSendFirmwareSize(uint32_t size)
* 流程:从 Flash 读取 → AES 加密 → SPI 发送 → 等待 ACK → 失败则重发。
* 每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包执行一次健康检查。
*/
ota_result_t OtaRelay::prepareRelayPacket(uint32_t seq, const ota_meta_t &meta, uint8_t tx_buf[64], size_t *out_chunk_size)
{
uint8_t plain48[GD32_OTA_PACKET_SIZE];
memset(plain48, 0xFF, sizeof(plain48));
size_t chunk_size = GD32_OTA_PACKET_SIZE;
if (seq == m_transferTotalPackets - 1) {
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
}
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
return OTA_RESULT_ERR_STORAGE;
}
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
tx_buf[4] = (uint8_t)(seq & 0xFFU);
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
tx_buf[6] = 0x00;
tx_buf[7] = 0x00;
if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq);
return OTA_RESULT_ERR_AUTH;
}
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
tx_buf[56] = (uint8_t)(if_end & 0xFFU);
tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU);
tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU);
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
seq, (unsigned)chunk_size, if_end,
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
m_transferCurrentSeq = seq;
if (out_chunk_size) *out_chunk_size = chunk_size;
return OTA_RESULT_OK;
}
bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], size_t chunk_size, ota_result_t *out_result)
{
uint8_t resp_type = 0;
uint8_t resp_payload[32] = {0};
size_t resp_len = 0;
uint32_t retry_count = 0;
ota_result_t result = OTA_RESULT_OK;
const uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? kLastPacketAckSeq : (seq + 1U);
// 注意:goto resend_packet 保持原有语义,仅作用范围缩小到本 helper。
// 完全去除 goto 需配合 GD32 全链路 OTA 回归验证后再进行。
resend_packet:
while (1) {
resp_len = 0;
esp_err_t err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, 64,
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
if (err == ESP_OK) {
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
if (++retry_count >= kMaxResendOnNack) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_TIMEOUT;
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
continue; // 重发本包
}
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
uint32_t err_seq = 0;
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
if (err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
if (++retry_count >= kMaxResendOnStructError) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break;
}
continue;
}
}
bool got_expected = false;
for (uint32_t retry = 0; retry < 10; ++retry) {
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
if (++retry_count >= kMaxResendOnStructError) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32;
break;
}
// ★ resend 前延迟
vTaskDelay(pdMS_TO_TICKS(1));
goto resend_packet;
}
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
got_expected = true;
break;
}
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
break;
}
}
if (got_expected) {
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
if (++retry_count >= kMaxResendOnNack) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break;
}
continue;
}
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
if (++retry_count >= kMaxResendOnNack) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32;
break;
}
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
vTaskDelay(pdMS_TO_TICKS(1));
goto resend_packet;
}
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
if (result != OTA_RESULT_OK) {
*out_result = result;
return true; // 中止外层循环
}
return false; // 继续下一包
}
ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
{
DEBUG_INFO("OTA", "开始转发 %" PRIu32 " 包", m_transferTotalPackets);
@@ -962,13 +1108,10 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
}
uint8_t tx_buf[64] = {0};
uint8_t resp_type = 0;
uint8_t resp_payload[32] = {0};
size_t resp_len = 0;
ota_result_t result = OTA_RESULT_OK;
for (uint32_t seq = 0; seq < m_transferTotalPackets; seq++) {
uint32_t retry_count = 0;
// 健康检查(每 OTA_PACKET_HEALTH_CHECK_INTERVAL 包 + 首末包)
if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == m_transferTotalPackets - 1) {
health_status_t health = HealthMonitor::instance().checkBeforeOtaRelay();
if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
@@ -977,150 +1120,32 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
}
}
// 全局超时检查
if (deadline_tick != 0 && xTaskGetTickCount() >= deadline_tick) {
DEBUG_ERR("OTA", "全局超时: 包 %" PRIu32 "/%" PRIu32 " 已超限,中止转发", seq, m_transferTotalPackets);
return OTA_RESULT_ERR_USER_ABORT;
}
uint8_t plain48[GD32_OTA_PACKET_SIZE];
memset(plain48, 0xFF, sizeof(plain48));
size_t chunk_size = GD32_OTA_PACKET_SIZE;
if (seq == m_transferTotalPackets - 1) {
chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE);
}
esp_err_t err = Gd32FlashStore::instance().flashReadChunk(m_transferFlashOffset, plain48, chunk_size);
if (err != ESP_OK) {
DEBUG_ERR("OTA", "Flash 读取失败, offset=%" PRIu32 ", err=%d", m_transferFlashOffset, err);
return OTA_RESULT_ERR_STORAGE;
}
tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00;
tx_buf[4] = (uint8_t)(seq & 0xFFU);
tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU);
tx_buf[6] = 0x00;
tx_buf[7] = 0x00;
if (!aes128EcbEncrypt48Bytes(m_kSession, plain48, &tx_buf[8])) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 加密失败", seq);
return OTA_RESULT_ERR_AUTH;
}
uint32_t if_end = (seq == m_transferTotalPackets - 1) ? 0xFFFFFFFFU : 0x00000000U;
tx_buf[56] = (uint8_t)(if_end & 0xFFU);
tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU);
tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU);
tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU);
calculateCrc32WithKey(m_kSession, plain48, &tx_buf[60]);
DEBUG_DBG("OTA", "包 %" PRIu32 ": chunk=%u if_end=0x%08" PRIX32 " crc32=%02X%02X%02X%02X",
seq, (unsigned)chunk_size, if_end,
tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]);
m_transferCurrentSeq = seq;
resend_packet:
while (1) {
resp_len = 0;
err = SpiMaster::instance().otaTransfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf),
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000);
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
if (err == ESP_OK) {
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10);
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "数据包 %" PRIu32 " 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len);
if (++retry_count >= 10U) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_TIMEOUT;
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
continue; // 重发本包
}
uint32_t expected_ack = (seq == m_transferTotalPackets - 1) ? 0xF0F0F0F0U : (seq + 1U);
if (!isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
uint32_t err_seq = 0;
uint8_t err_code = 0;
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code)) {
if (err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
if (++retry_count >= 3U) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break;
}
continue;
}
}
bool got_expected = false;
for (uint32_t retry = 0; retry < 10; ++retry) {
DEBUG_DBG("OTA", "包 %" PRIu32 " 收到非目标ACK(seq不匹配),继续轮询(%" PRIu32 "/5)...", seq, retry + 1);
err = SpiMaster::instance().otaPollResponse(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000);
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
err = waitUntilNotWait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5);
}
if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) {
DEBUG_WARN("OTA", "包 %" PRIu32 " poll 超时/非ACK,重发 (retry=%" PRIu32 "/3)", seq, retry_count + 1);
if (++retry_count >= 3U) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32;
break;
}
// ★ resend 前延迟
vTaskDelay(pdMS_TO_TICKS(1));
goto resend_packet;
}
if (isStructuredAckWithSeq(resp_payload, resp_len, expected_ack)) {
got_expected = true;
break;
}
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
break;
}
}
if (got_expected) {
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
if (parseStructuredError(resp_payload, resp_len, &err_seq, &err_code) && err_seq == seq) {
DEBUG_WARN("OTA", "包 %" PRIu32 " 收到结构化错误 err=0x%02X,准备重发", seq, err_code);
if (++retry_count >= 10U) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = (err_code == 0x04U) ? OTA_RESULT_ERR_CRC : OTA_RESULT_ERR_GD32;
break;
}
continue;
}
DEBUG_WARN("OTA", "包 %" PRIu32 " 轮询耗尽未匹配,重发 (retry=%" PRIu32 "/10)", seq, retry_count + 1);
if (++retry_count >= 10U) {
DEBUG_ERR("OTA", "包 %" PRIu32 " 重发次数超限", seq);
result = OTA_RESULT_ERR_GD32;
break;
}
// ★ resend 前延迟:给 GD32 时间处理上一包并更新 SPI 响应缓冲区
vTaskDelay(pdMS_TO_TICKS(1));
goto resend_packet;
}
m_transferFlashOffset += (uint32_t)chunk_size;
break;
}
// 准备单包:读 Flash + 加密 + 组装
size_t chunk_size = 0;
result = prepareRelayPacket(seq, meta, tx_buf, &chunk_size);
if (result != OTA_RESULT_OK) {
break;
return result;
}
relaySeq = seq + 1;
if (seq % (m_transferTotalPackets / 10 + 1) == 0) {
uint32_t progress = (relaySeq * 100U) / m_transferTotalPackets;
DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", relaySeq, m_transferTotalPackets, progress);
// 发送单包并等待 ACK(内含重试循环)
if (sendRelayPacketWithRetry(seq, tx_buf, chunk_size, &result)) {
return result; // 中止
}
setRelaySeq(seq + 1);
// 每 5% 或前 5 包/最后 5 包打印进度,避免 9 秒+静默期无法判断是否卡死
uint32_t step = m_transferTotalPackets / 20 + 1; // ~5% 间隔
if (seq < 5 || seq >= m_transferTotalPackets - 5 || seq % step == 0) {
// 进度日志直接用本地 seq+1,避免再次加锁读 relaySeq
uint32_t done = seq + 1;
uint32_t progress = (done * 100U) / m_transferTotalPackets;
DEBUG_INFO("OTA", "转发进度: %" PRIu32 "/%" PRIu32 " 包 (%" PRIu32 "%%)", done, m_transferTotalPackets, progress);
}
// ★ 包间延迟:给 GD32 Bootloader 充足时间完成 flash 写入并准备下一包 ACK。
@@ -1132,8 +1157,36 @@ ota_result_t OtaRelay::doRelayPackets(TickType_t deadline_tick)
return result;
}
/** @brief 协议子步骤:发送 FINISH_OTA 指令并验证 GD32 APP 存活。 */
ota_result_t OtaRelay::doFinishAndVerify()
// ============================================================
// ============================================================
// 统一 OtaRelay 内部两处 APP 探测逻辑(doWaitForAppAlive 轮询 + taskEntry 启动探测)。
// 成功:setGd32AppAlive(true) + setGd32Version + 打印版本号;失败:打印 warning。
// 注:OtaCoordinator::probeGd32AppOnce 逻辑相似,但因依赖方向不调用本函数。
bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
{
uint32_t uptime = 0;
gd32_version_t ver = {};
esp_err_t err = SpiMaster::instance().appPing(timeout_ms, &uptime, &ver);
if (err == ESP_OK) {
// 持锁一次性写入两个状态字段,保证 getSnapshot() 读取一致
StateLockGuard lock;
gd32AppAlive = true;
gd32Version = ver;
if (out_uptime) *out_uptime = uptime;
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
uptime,
ver.app_major, ver.app_minor, ver.app_patch, ver.app_build,
ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build);
return true;
}
if (out_uptime) *out_uptime = 0;
DEBUG_WARN("OTA", "GD32 APP 探测失败: %s", esp_err_to_name(err));
return false;
}
/** @brief 协议子步骤:发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试)。 */
ota_result_t OtaRelay::doSendFinishOta()
{
DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC),等待 GD32 确认即将跳转 APP");
@@ -1193,6 +1246,43 @@ ota_result_t OtaRelay::doFinishAndVerify()
return OTA_RESULT_ERR_GD32;
}
return OTA_RESULT_OK;
}
/** @brief 协议子步骤:轮询等待 GD32 APP 存活(30 次重试)。 */
ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
{
// ★ APP 存活验证
setGd32AppAlive(false);
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(500));
if (probeAndCacheAppAlive(timeout_ms)) {
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
break;
}
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1);
}
if (!getGd32AppAlive()) {
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
reset();
return OTA_RESULT_ERR_GD32;
}
return OTA_RESULT_OK;
}
/** @brief 协议子步骤:发送 FINISH_OTA + 等待 APP 存活(顺序两阶段 + 会话清理)。 */
ota_result_t OtaRelay::doFinishAndVerify()
{
// 阶段1: 发送 FINISH_OTA 并等待 ACK
ota_result_t res = doSendFinishOta();
if (res != OTA_RESULT_OK) {
return res;
}
// 会话密钥/上下文清理
Protocol::sessionKeyClear(&m_sessionCtx);
memset(&m_sessionCtx, 0, sizeof(session_ctx_t));
memset(m_kSession, 0, sizeof(m_kSession));
@@ -1203,34 +1293,8 @@ ota_result_t OtaRelay::doFinishAndVerify()
Gd32FlashStore::instance().metaClear();
// ★ APP 存活验证
gd32AppAlive = false;
for (uint32_t attempt = 0; attempt < 30; ++attempt) {
vTaskDelay(pdMS_TO_TICKS(500));
uint32_t uptime = 0;
gd32_version_t ping_ver = {};
esp_err_t ping_err = SpiMaster::instance().appPing(1000, &uptime, &ping_ver);
if (ping_err == ESP_OK) {
gd32AppAlive = true;
gd32Version = ping_ver;
DEBUG_INFO("OTA", "GD32 APP 存活确认: uptime=%" PRIu32 " sec, APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
uptime,
ping_ver.app_major, ping_ver.app_minor, ping_ver.app_patch, ping_ver.app_build,
ping_ver.boot_major, ping_ver.boot_minor, ping_ver.boot_patch, ping_ver.boot_build);
DEBUG_INFO("OTA", "=== OTA 全流程完成 (APP 已启动) ===");
break;
}
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/30)", attempt + 1);
}
if (!gd32AppAlive) {
DEBUG_ERR("OTA", "GD32 APP alive confirm timeout, OTA failed");
reset();
return OTA_RESULT_ERR_GD32;
}
return OTA_RESULT_OK;
// 阶段2: APP 存活轮询(单次 appPing 超时 1000ms,共 30 次)
return doWaitForAppAlive(1000);
}
// ============================================================
@@ -1258,24 +1322,7 @@ void OtaRelay::taskEntry(void *pvParameters)
{
DEBUG_INFO("OTA", "启动初始 GD32 探测...");
vTaskDelay(pdMS_TO_TICKS(3000)); // 等待 GD32 APP 就绪
uint32_t uptime = 0;
gd32_version_t ping_ver = {};
esp_err_t ping_err = SpiMaster::instance().appPing(2000, &uptime, &ping_ver);
if (ping_err == ESP_OK) {
StateLockGuard lock;
gd32AppAlive = true;
gd32Version = ping_ver;
DEBUG_INFO("OTA", "启动 GD32 探测成功: uptime=%" PRIu32 "s, "
"APP=v%d.%d.%d.%d, BOOT=v%d.%d.%d.%d",
uptime,
ping_ver.app_major, ping_ver.app_minor,
ping_ver.app_patch, ping_ver.app_build,
ping_ver.boot_major, ping_ver.boot_minor,
ping_ver.boot_patch, ping_ver.boot_build);
} else {
DEBUG_WARN("OTA", "启动 GD32 探测失败: %s (GD32 可能未就绪)",
esp_err_to_name(ping_err));
}
probeAndCacheAppAlive(2000);
}
while (1) {
@@ -1287,7 +1334,8 @@ void OtaRelay::taskEntry(void *pvParameters)
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
// ★ H4: 通过 IRelayListener 接口回调通知协调器(不再直接依赖 OtaCoordinator)
if (s_relayListener) s_relayListener->onRelayStarted();
IRelayListener *listener = getRelayListener();
if (listener) listener->onRelayStarted();
ota_result_t result = instance().forwardFirmware();
uint32_t result_msg = (uint32_t)result;
@@ -1303,6 +1351,7 @@ void OtaRelay::taskEntry(void *pvParameters)
}
instance().reset();
// ★ H4: 通过 IRelayListener 接口回调通知协调器中继结束
if (s_relayListener) s_relayListener->onRelayComplete(result == OTA_RESULT_OK);
listener = getRelayListener();
if (listener) listener->onRelayComplete(result == OTA_RESULT_OK);
}
}