diff --git a/main/include/app_config.h b/main/include/app_config.h index 7fc09b9..badbd5b 100644 --- a/main/include/app_config.h +++ b/main/include/app_config.h @@ -10,7 +10,7 @@ // ============================================================ // 固件版本 // ============================================================ -inline constexpr const char *FW_VERSION = "0.2.10"; +inline constexpr const char *FW_VERSION = "0.2.11"; // ============================================================ // WiFi 工作模式选择 diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index cc43d8c..72a32bd 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -1,4 +1,4 @@ -/* +/* * ota_relay.c - OTA 中继核心状态机实现 * T2.3, T2.4, T3.1 - T3.7 */ @@ -21,6 +21,8 @@ /** @brief 当前 OTA 中继状态机状态。 */ relay_state_t g_relay_state = RELAY_IDLE; +uint32_t g_relay_seq = 0; +uint32_t g_relay_total_packets = 0; volatile bool g_gd32_app_alive = false; static const esp_partition_t *g_gd32_partition = NULL; @@ -69,7 +71,6 @@ static ota_result_t relay_packets_from_flash(void); */ static ota_result_t finish_ota(void); static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out); -static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out); static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len); static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type, uint8_t *resp_payload, size_t resp_max, @@ -333,24 +334,6 @@ static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, return ret; } -/** - * @brief 使用 AES-128 ECB 解密单个 16 字节分组。 - * @param key 16 字节密钥。 - * @param in 输入密文分组(16 字节)。 - * @param out 输出明文分组(16 字节)。 - * @return 0 表示成功,非 0 表示失败。 - */ -static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out) -{ - esp_aes_context aes; - esp_aes_init(&aes); - int ret = esp_aes_setkey(&aes, key, 128); - if (ret == 0) { - ret = esp_aes_crypt_ecb(&aes, ESP_AES_DECRYPT, in, out); - } - esp_aes_free(&aes); - return ret; -} /** * @brief 按 3 个分组对 48 字节数据执行 AES-128 ECB 加密。 @@ -825,6 +808,8 @@ static ota_result_t relay_packets_from_flash(void) DEBUG_INFO("OTA", "总包数: %u, 每包 %u 字节", total_packets, (unsigned)GD32_OTA_PACKET_SIZE); DEBUG_INFO("OTA", "状态: RELAY_RELAY_ACTIVE"); g_relay_state = RELAY_RELAY_ACTIVE; + g_relay_total_packets = total_packets; + g_relay_seq = 0; for (uint32_t seq = 0; seq < total_packets; seq++) { uint32_t retry_count = 0; @@ -873,6 +858,7 @@ static ota_result_t relay_packets_from_flash(void) g_relay_state = RELAY_PACKET_SENT; + resend_packet: while (1) { resp_len = 0; err = spi_ota_transfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf), @@ -910,16 +896,27 @@ static ota_result_t relay_packets_from_flash(void) } } - /* 非期望 ACK,也非错误帧 → 可能是残留 ACK,循环 poll 直到拿到正确的 */ + /* 非期望 ACK,也非错误帧 -> 可能是残留 ACK,循环 poll 直到拿到正确的。 + * 超时/非ACK 退出时需要重发当前包(GD32 Flash 缓冲满时会发 WAIT 后排空, + * ESP32 必须重发才能让 GD32 重新处理)。 */ bool got_expected = false; for (uint32_t retry = 0; retry < 5; ++retry) { DEBUG_WARN("OTA", "包 %u 收到非目标ACK(seq不匹配),继续轮询(%u/5)...", seq, retry + 1); err = spi_ota_poll_response(&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 2000); if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) { - err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10); + err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 100, 5); } if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK) { - break; + /* poll 超时或收到非 ACK(WAIT/ERROR)-> GD32 Flash 缓冲可能已满, + * 正在排空。重发当前包让 GD32 重新入队处理。 */ + DEBUG_WARN("OTA", "包 %u poll 超时/非ACK,重发 (retry=%u/3)", seq, retry_count + 1); + g_relay_state = RELAY_PACKET_ACK_RETRY; + if (++retry_count >= 3U) { + DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); + result = OTA_RESULT_ERR_GD32; + break; + } + goto resend_packet; } if (is_structured_ack_with_seq(resp_payload, resp_len, expected_ack)) { got_expected = true; @@ -949,9 +946,15 @@ static ota_result_t relay_packets_from_flash(void) continue; /* 重发本包 */ } - DEBUG_ERR("OTA", "包 %u ACK/ERROR 不匹配,len=%zu", seq, resp_len); - result = OTA_RESULT_ERR_GD32; - break; + /* 轮询耗尽仍未拿到期望 ACK -> 重发当前包 */ + DEBUG_WARN("OTA", "包 %u 轮询耗尽未匹配,重发 (retry=%u/3)", seq, retry_count + 1); + g_relay_state = RELAY_PACKET_ACK_RETRY; + if (++retry_count >= 3U) { + DEBUG_ERR("OTA", "包 %u 重发次数超限", seq); + result = OTA_RESULT_ERR_GD32; + break; + } + goto resend_packet; } /* 匹配成功 */ @@ -964,12 +967,12 @@ static ota_result_t relay_packets_from_flash(void) break; } + g_relay_seq = seq + 1; if (seq % (total_packets / 10 + 1) == 0) { - uint32_t progress = (seq * 100U) / total_packets; - DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)", seq, total_packets, progress); + uint32_t progress = (g_relay_seq * 100U) / total_packets; + DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)", g_relay_seq, total_packets, progress); } } - g_relay_state = RELAY_RECEIVE_DONE; DEBUG_DBG("OTA", "状态: RELAY_RECEIVE_DONE"); return result; @@ -1146,6 +1149,8 @@ void ota_relay_reset(void) { /** @brief 当前 OTA 中继状态机状态。 */ g_relay_state = RELAY_IDLE; + g_relay_seq = 0; + g_relay_total_packets = 0; session_key_clear(&g_session_ctx); memset(&g_session_ctx, 0, sizeof(session_ctx_t)); memset(g_k_session, 0, sizeof(g_k_session)); diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index eb9a0c4..a0347ec 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -152,5 +152,7 @@ extern volatile bool g_gd32_app_alive; // 当前中继状态(供健康监控外部访问) extern relay_state_t g_relay_state; +extern uint32_t g_relay_seq; +extern uint32_t g_relay_total_packets; #endif // OTA_RELAY_H \ No newline at end of file diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 0bbf1c2..0b27f72 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -1201,9 +1201,9 @@ static esp_err_t system_status_handler(httpd_req_t *req) memset(&meta, 0, sizeof(meta)); meta_load(&meta); - char resp[700]; + char resp[800]; int len = snprintf(resp, sizeof(resp), - "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}", + "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu}}", FW_VERSION,health_get_current(), (unsigned long)esp_get_free_heap_size(), (unsigned long)esp_get_minimum_free_heap_size(), @@ -1216,7 +1216,9 @@ static esp_err_t system_status_handler(httpd_req_t *req) (unsigned long)meta.total_size, (unsigned long)meta.stored_size, s_gd32_storage_done ? "true" : "false", - g_relay_state); + g_relay_state, + (unsigned long)g_relay_seq, + (unsigned long)g_relay_total_packets); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); diff --git a/tools/app.js b/tools/app.js index c858097..a424d43 100644 --- a/tools/app.js +++ b/tools/app.js @@ -13,6 +13,53 @@ let logSocket = null; let latestSystemStatus = null; let isEsp32Rebooting = false; let rebootStartTime = 0; +// ==================== 刷写进度 ==================== +var activePhase = null; // 'esp32Ota' | 'gd32Upload' | 'gd32Relay' | null + +function setActivePhase(phase) { + activePhase = phase; + var stepMap = { esp32Ota: 'ppEsp32', gd32Upload: 'ppGd32Up', gd32Relay: 'ppGd32Relay' }; + Object.keys(stepMap).forEach(function(p) { + var el = document.getElementById(stepMap[p]); + if (!el) return; + el.classList.remove('active', 'done'); + if (p === phase) el.classList.add('active'); + }); + // Reset progress fill color + var fill = document.getElementById('progressFill'); + if (fill) fill.classList.remove('done'); +} + +function markPhaseDone(phase) { + var stepMap = { esp32Ota: 'ppEsp32', gd32Upload: 'ppGd32Up', gd32Relay: 'ppGd32Relay' }; + var el = document.getElementById(stepMap[phase]); + if (el) { el.classList.remove('active'); el.classList.add('done'); } + // All done? turn progress bar green + if (phase === 'gd32Relay') { + var fill = document.getElementById('progressFill'); + if (fill) fill.classList.add('done'); + } +} + +function setProgress(pct, text) { + var fillEl = document.getElementById('progressFill'); + var txtEl = document.getElementById('progressTxt'); + var pctEl = document.getElementById('progressPct'); + if (fillEl) fillEl.style.width = pct + '%'; + if (txtEl) txtEl.textContent = text; + if (pctEl) pctEl.textContent = pct + '%'; +} + +function resetProgress() { + setProgress(0, '等待操作...'); + activePhase = null; + ['ppEsp32', 'ppGd32Up', 'ppGd32Relay'].forEach(function(id) { + var el = document.getElementById(id); + if (el) el.classList.remove('active', 'done'); + }); + var fill = document.getElementById('progressFill'); + if (fill) fill.classList.remove('done'); +} // ==================== 工具函数 ==================== function formatBytes(bytes) { @@ -183,6 +230,7 @@ function applyDisconnectedRuntimeState() { document.getElementById('mRSSI').textContent = '--'; document.getElementById('bOTAProgress').textContent = '--'; document.getElementById('bOTAProgress').className = 'v err'; + resetProgress(); resetStageFlow(); } @@ -362,13 +410,32 @@ function updateSystemStatusUi(data) { else if (rs === 17) pr.className = 'pill online'; else pr.className = 'pill idle'; - // OTA progress + // OTA cache progress (GD32 upload to ESP32 flash) var total = ota.total_size || 0; var stored = ota.stored_size || 0; - var progress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0; + var storageProgress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0; var bp = document.getElementById('bOTAProgress'); - bp.textContent = progress + '%'; - bp.className = 'v ' + (progress === 100 ? 'ok' : progress > 0 ? 'warn' : ''); + bp.textContent = storageProgress + '%'; + bp.className = 'v ' + (storageProgress === 100 ? 'ok' : storageProgress > 0 ? 'warn' : ''); + + // GD32 relay forwarding progress + var relaySeq = ota.relay_seq || 0; + var relayTotal = ota.relay_total_packets || 0; + var relayProgress = relayTotal > 0 ? Math.min(100, Math.round(relaySeq * 100 / relayTotal)) : 0; + + if (rs > 0 && rs < 17) { + // Relay is active — show relay progress + setActivePhase('gd32Relay'); + setProgress(relayProgress, '已转发 ' + relaySeq + ' / ' + relayTotal + ' 包'); + } else if (total > 0 && stored < total && !isUploading) { + // GD32 upload to cache in progress (handled by upload progress if uploading) + setActivePhase('gd32Upload'); + setProgress(storageProgress, '已缓存 ' + formatBytes(stored) + ' / ' + formatBytes(total)); + } else if (rs === 17) { + // Relay done + markPhaseDone('gd32Relay'); + setProgress(100, 'GD32 刷写完成'); + } // stage flow from real state updateStagesFromRelayState(rs); @@ -814,7 +881,7 @@ async function startUpload(chip) { var origText = uploadBtn.textContent; uploadBtn.disabled = true; uploadBtn.textContent = '上传中...'; - document.getElementById('progressWrap').classList.add('show'); + setActivePhase(chip === 'esp32' ? 'esp32Ota' : 'gd32Upload'); addLog('I', '上传', '开始上传: ' + file.name + ' \u2192 ' + chip.toUpperCase()); showToast('开始上传固件到 ' + chip.toUpperCase(), 'success'); @@ -841,10 +908,7 @@ async function startUpload(chip) { if (e.lengthComputable) { var pct = Math.round((e.loaded / e.total) * 100); uploadProgress = pct; - document.getElementById('progressFill').style.width = pct + '%'; - document.getElementById('progressTxt').textContent = - '已上传 ' + formatBytes(e.loaded) + ' / ' + formatBytes(e.total); - document.getElementById('progressPct').textContent = pct + '%'; + setProgress(pct, '已上传 ' + formatBytes(e.loaded) + ' / ' + formatBytes(e.total)); document.getElementById('bOTAProgress').textContent = pct + '%'; } }); @@ -857,7 +921,9 @@ async function startUpload(chip) { if (xhr.status === 200) { addLog('I', '上传', '固件上传完成!'); showToast('固件上传成功', 'success'); + setProgress(100, '上传完成'); if (chip === 'esp32') { + markPhaseDone('esp32Ota'); var countdown = 3; addLog('I', 'OTA', countdown + ' 秒后自动调用 POST /ota/update 完成升级并重启...'); var timer = setInterval(function () { @@ -869,6 +935,10 @@ async function startUpload(chip) { triggerEsp32Update(); } }, 1000); + } else { + markPhaseDone('gd32Upload'); + setActivePhase('gd32Relay'); + setProgress(0, '等待触发转发...'); } } else { var detail = formatHttpErrorDetail(xhr.responseText); diff --git a/tools/index.html b/tools/index.html index 8952b1a..711332c 100644 --- a/tools/index.html +++ b/tools/index.html @@ -4,7 +4,7 @@