Files
ESP32_COMM/tools/js/ota/upload-gd32.js
T
小龙虾 51c89b3065 feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制
移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2
CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发
实现流量控制,提高OTA传输稳定性。

refactor(stack): 统一任务栈大小配置并添加CPU统计功能

- 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配
- 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样
- 更新main.cpp中的任务创建使用新的栈大小常量
- 调整health_monitor和ota_relay等任务的栈分配

perf(ota): 改进ESP32和GD32 OTA分区预擦策略

- ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞
- GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量
- 优化OTA协调器状态管理,修复FINALIZING状态残留问题

fix(log): 改进UDP日志发送的堆内存管理

- 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer
- 降低UDP发送跳过的内存阈值从10KB到6KB
- 降低日志排水最小内存阈值从8KB到4KB

docs(readme): 更新OTA配置参数文档说明

移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为
基于CREDIT背压机制的描述。
2026-07-16 01:24:12 +08:00

257 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* upload-gd32.js - GD32 中继 OTA 触发 + relay 态跟踪
*
* 包含:relay 进度观察定时器、markGd32OtaUiComplete、triggerGd32Relay。
*
* 共享状态(通过 window.* 引用):
* window.gd32CompleteAnnounced / window.relayWatchLastSeq / window.gd32RelaySessionLive
* window.pollingPausedForUpload / window.statusPollIntervalMs
* window.otaSessionState / window.lastSysStateCode / window.lastRelayState
*
* 复用的函数(通过 window.* 引用):
* window.otaSessionTransition / window.OtaSessionState / window.otaSessionActive
* window.markPhaseDone / window.setProgress / window.updateSystemStatusUi
* window.cleanupAfterOtaFailure / window.scheduleStatusPoll / window.pollLogs
* window.resumePollingAfterUpload / window.getBaseUrl / window.showToast / window.addLog
*/
import { showToast } from '../common/toast.js';
import { addLog } from '../common/log.js';
import {
otaSessionTransition, otaSessionActive, OtaSessionState
} from './session.js';
import { markPhaseDone, setProgress } from './progress.js';
/**
* GD32 中继进度观察:以 /system/status 为唯一真相源。
* 日志环只有 64 条,中继/擦除期间 INFO 会被挤掉,时间戳还是浏览器本地时间——
* 绝不能用「最后一条日志停在 SEND_SIZE」判断卡死。
*
* 时序约定(避免「设备已 FINISH / 完成,UI 还在 94%」):
* 1) 包进度变化先刷进度条 + 日志;
* 2) 一旦 rseq>=rtot 或 rs>=4(FINISHING),先强制打出 100% 再进收尾/完成;
* 3) markGd32OtaUiComplete 前再兜底一次 100%。
*/
let relayWatchTimerId = null;
window.gd32CompleteAnnounced = false;
window.relayWatchLastSeq = -1;
/** 本会话是否已观察到真实中继态(1~4),避免旧 relay_state=5 残留导致一点击就“完成” */
window.gd32RelaySessionLive = false;
/**
* 包进度变更时更新进度条并写一条「转发」日志(同 seq 不重复)。
* @param {number} rseq
* @param {number} rtot
* @param {{force100?: boolean}} [opts]
* @returns {boolean} 是否实际输出了进度
*/
export function emitRelayProgress(rseq, rtot, opts) {
opts = opts || {};
rseq = Number(rseq) || 0;
rtot = Number(rtot) || 0;
if (rtot <= 0) return false;
if (opts.force100 || rseq >= rtot) {
rseq = rtot;
}
if (rseq <= 0) return false;
if (rseq === window.relayWatchLastSeq) return false;
window.relayWatchLastSeq = rseq;
var pct = Math.min(100, Math.round(rseq * 100 / rtot));
setProgress(60 + Math.round(pct * 0.30),
'已转发 ' + rseq + ' / ' + rtot + ' 包 (' + pct + '%)');
addLog('I', 'OTA', '转发 ' + rseq + '/' + rtot + ' (' + pct + '%)');
return true;
}
/** 完成/收尾前:若尚未打出 100%,用 status 里的包数补一条 */
export function ensureRelayProgressComplete() {
var data = window.latestSystemStatus;
var o = (data && data.ota) || {};
var rtot = Number(o.relay_total_packets) || 0;
var rseq = Number(o.relay_seq) || 0;
if (rtot <= 0) return;
/* FINISHING/DONE 时后端 seq 已到 total;status 滞后时也强制 100% */
if (rseq < rtot && (Number(o.relay_state) >= 4 || Number(o.sys_state_code) === 4)) {
rseq = rtot;
}
if (rseq >= rtot) {
emitRelayProgress(rtot, rtot, { force100: true });
}
}
export function markGd32OtaUiComplete(msg) {
// 已公告过:禁止再改 className / setProgress / toast,否则定时器重触发动效会「疯狂跳 ✓」
if (window.gd32CompleteAnnounced) {
return;
}
/* 仅本会话 RELAYING/FAILED 可宣布完成;IDLE/UPLOADING 时 status 里的旧 relay 残留一律忽略 */
if (window.otaSessionState !== OtaSessionState.RELAYING &&
window.otaSessionState !== OtaSessionState.FAILED) {
return;
}
/* 先 100% 转发进度,再宣布刷写完成 —— 避免日志里「完成」早于「1089/1089」 */
ensureRelayProgressComplete();
window.gd32CompleteAnnounced = true;
stopRelayProgressWatch();
markPhaseDone('gd32Ota');
window.currentOtaTarget = null;
setProgress(100, msg || 'GD32 刷写完成');
var sbDone = document.getElementById('stageStatusBar');
var stDone = document.getElementById('stageStatusText');
if (sbDone) sbDone.className = 'stage-status-bar ok';
if (stDone) stDone.textContent = msg || 'GD32 刷写完成';
if (window.otaSessionState === OtaSessionState.RELAYING ||
window.otaSessionState === OtaSessionState.FAILED) {
otaSessionTransition(OtaSessionState.DONE);
}
addLog('I', 'OTA', msg || 'GD32 刷写完成');
showToast(msg || 'GD32 刷写完成', 'success');
}
export function stopRelayProgressWatch() {
if (relayWatchTimerId) {
clearInterval(relayWatchTimerId);
relayWatchTimerId = null;
}
/* 中继结束:恢复常态 1s status 轮询 */
if (typeof window.setStatusPollIntervalMs === 'function') {
window.setStatusPollIntervalMs(1000);
}
}
/**
* 中继进度观察:只读 pollStatus 写入的 latestSystemStatus,不再另起 /system/status。
* 中继期 status 加速到 400ms,使「转发 N/M」更贴近设备真实进度。
*/
export function startRelayProgressWatch() {
stopRelayProgressWatch();
if (window.pollingPausedForUpload) {
window.resumePollingAfterUpload();
}
/* 中继期加速 status,缩小 UI 进度相对设备日志的滞后 */
if (typeof window.setStatusPollIntervalMs === 'function') {
window.setStatusPollIntervalMs(400);
}
window.scheduleStatusPoll(0);
window.pollLogs();
addLog('I', 'OTA', '跟踪中继进度:看进度条与 relay_seq(/system/status),勿只看日志');
relayWatchTimerId = setInterval(function () {
var active = window.otaSessionState === OtaSessionState.RELAYING ||
(window.otaSessionState === OtaSessionState.FAILED &&
(window.lastSysStateCode === 3 || window.lastRelayState >= 1 && window.lastRelayState <= 4));
if (!active) {
if (window.otaSessionState === OtaSessionState.DONE) {
stopRelayProgressWatch();
}
return;
}
var data = window.latestSystemStatus;
if (!data || !data.ota) return;
var o = data.ota || {};
var code = Number(o.sys_state_code);
var rstate = Number(o.relay_state);
var alive = !!o.gd32_app_alive;
var rseq = Number(o.relay_seq) || 0;
var rtot = Number(o.relay_total_packets) || 0;
if (rstate >= 1 && rstate <= 4) {
window.gd32RelaySessionLive = true;
}
if (code === 8 || rstate === 255) {
stopRelayProgressWatch();
window.pollLogs();
return;
}
/*
* 顺序:先刷包进度(含强制 100%),再判定完成。
* FINISHING/DONE 时若 status 滞后未到 total,仍按 total 打 100%。
*/
var transferDone = rtot > 0 && (rseq >= rtot || rstate >= 4 || code === 4);
if (transferDone) {
emitRelayProgress(rtot, rtot, { force100: true });
} else if (rtot > 0 && rseq > 0) {
emitRelayProgress(rseq, rtot);
}
if (window.gd32RelaySessionLive &&
(code === 4 || rstate === 5 ||
(alive && rtot > 0 && rseq >= rtot && rstate >= 3))) {
markGd32OtaUiComplete('GD32 刷写完成');
window.pollLogs();
}
}, 400);
}
/** @description 触发 GD32 中继 OTA 流程(通过 HTTP 接口)。 */
export async function triggerGd32Relay() {
var sha256 = window._gd32FileSha256;
if (!sha256) {
addLog('E', 'OTA', '缺少 GD32 文件 SHA-256,请先上传文件');
showToast('SHA-256 缺失', 'error');
window.cleanupAfterOtaFailure('SHA-256 缺失');
return;
}
addLog('I', 'OTA', 'GD32 SHA-256: ' + sha256 + ' → 触发中继...');
showToast('正在触发 GD32 OTA', 'success');
setProgress(48, '校验 SHA 并触发 GD32 中继…');
try {
// 仅触发中继(设备端异步跑),不阻塞到刷写结束;SHA 读分区可能数秒
var resp = await fetch(window.getBaseUrl() + '/ota/relay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sha256: sha256 }),
signal: AbortSignal.timeout(120000)
});
var text = await resp.text();
if (resp.ok) {
try {
var data = JSON.parse(text);
if (data.sha256) addLog('I', 'OTA', '设备端 SHA-256: ' + data.sha256);
} catch (_) {}
addLog('I', 'OTA', 'GD32 OTA 已触发;进度看状态栏包数(勿只看日志时间戳)');
showToast('GD32 OTA 已触发', 'success');
setProgress(50, '中继已启动,同步进度…');
window.gd32CompleteAnnounced = false;
window.relayWatchLastSeq = -1;
startRelayProgressWatch();
} else {
// 尝试解析错误响应中的 device SHA
try {
var err = JSON.parse(text);
if (err.device) {
addLog('E', 'OTA', 'GD32 SHA-256 不匹配: 设备=' + err.device + ' 前端=' + sha256);
showToast('SHA-256 不匹配', 'error');
window.cleanupAfterOtaFailure('SHA-256 不匹配');
return;
}
} catch (_) {}
addLog('E', 'OTA', 'GD32 触发失败: HTTP ' + resp.status + ' ' + text);
showToast('GD32 触发失败', 'error');
window.cleanupAfterOtaFailure('触发失败 HTTP ' + resp.status);
}
} catch (e) {
// 请求超时/断连 ≠ 设备未启动中继:先探测 status 再决定是否标失败
try {
var probe = await fetch(window.getBaseUrl() + '/system/status', {
signal: AbortSignal.timeout(3000)
});
if (probe.ok) {
var st = await probe.json();
var o = st.ota || {};
var code = Number(o.sys_state_code);
var rstate = Number(o.relay_state);
if (code === 3 || (rstate >= 1 && rstate <= 4)) {
addLog('W', 'OTA', '触发请求中断(' + e.message + '),但设备已在中继,继续跟踪进度');
showToast('设备已在刷写,同步进度中', 'success');
if (window.otaSessionState !== OtaSessionState.RELAYING) {
otaSessionTransition(OtaSessionState.RELAYING);
}
try { window.updateSystemStatusUi(st); } catch (_) {}
startRelayProgressWatch();
return;
}
}
} catch (_) { /* probe 失败再走失败清理 */ }
addLog('E', 'OTA', 'GD32 触发失败: ' + e.message);
showToast('GD32 触发失败', 'error');
window.cleanupAfterOtaFailure('触发异常');
}
}