feat(ota): 优化 GD32 OTA 流程和 CAN 反馈机制

- 实现 GD32 主动推送 CAN 反馈架构,替代 ESP32 1Hz 轮询方式,
  减少 SPI 总线占用提升带宽效率

- 添加 GD32 Bootloader 就绪检测机制,通过 PB6 硬件信号等待
  保证 OTA 过程稳定可靠

- 集成 CREDIT 感知功能,当 GD32 写 Flash 缓冲时自动等待,
  避免数据包丢失提高传输成功率

- 重构 canFdPollFeedback 为 canFdParseFeedbackPage 函数,
  支持被动解析 GD32 主动推送的数据页

- 更新版本号从 0.37 到 0.3.38 并添加 Kconfig 配置选项支持
This commit is contained in:
小龙虾
2026-07-08 02:51:38 +08:00
parent 069ce8e6a5
commit 661ae54424
13 changed files with 974 additions and 595 deletions
+68 -24
View File
@@ -860,9 +860,12 @@ async function testConnection() {
restorePolling();
}
// 立即发一次请求检测
// 手动检测使用独立的 AbortController,避免打断递归调度的 pollStatus
var testAbort = new AbortController();
var testTimer = setTimeout(function() { try { testAbort.abort(); } catch(_e) {} }, 3000);
try {
var resp = await fetch(getBaseUrl() + '/system/status', {
signal: statusAbortCtrl.signal
signal: testAbort.signal
});
if (resp.ok) {
var data = await resp.json();
@@ -884,6 +887,8 @@ async function testConnection() {
var diag = await diagnoseNetwork();
addLog('W', '诊断', diag);
showToast('无法连接: ' + diag, 'error');
} finally {
clearTimeout(testTimer);
}
}
async function refreshSystemStatusNow(event) {
@@ -891,10 +896,12 @@ async function refreshSystemStatusNow(event) {
event.preventDefault();
event.stopPropagation();
}
// ★ ESP32 上传期间也允许日志轮询,让用户看到完整进展
// 手动刷新使用独立的 AbortController,避免与递归调度的 pollStatus 互相干扰
var manualAbort = new AbortController();
var manualTimer = setTimeout(function() { try { manualAbort.abort(); } catch(_e) {} }, 3000);
try {
var resp = await fetch(getBaseUrl() + '/system/status', {
signal: statusAbortCtrl.signal
signal: manualAbort.signal
});
if (!resp.ok) {
addLog('E', '状态', '刷新失败: HTTP ' + resp.status);
@@ -910,6 +917,8 @@ async function refreshSystemStatusNow(event) {
} catch (e) {
addLog('E', '状态', '刷新失败: ' + e.message);
showToast('状态刷新失败', 'error');
} finally {
clearTimeout(manualTimer);
}
}
// ==================== CAN 帧发送 ====================
@@ -1757,7 +1766,9 @@ var lastDiagTime = 0; // 上次诊断时间
var diagResult = ''; // 最近一次诊断结果
var lastSysStateCode = -1; // 上次成功的 sys_state_code,用于失败时判断设备是否在忙
var lastRelayState = 0; // 上次成功的 relay_state
var statusAbortCtrl = null; // 取消上一次 in-flight pollStatus,防止堆积刷新
var statusAbortCtrl = null; // 当前 /system/status 请求的 AbortController(3s 超时用)
var statusPollIntervalMs = 1000; // 状态轮询目标间隔
var statusPollInFlight = false; // 是否正在请求 /system/status,防止重叠
// 从设备 IP 推导网关 IP(取前三段 + .1)
function getGatewayIp() {
var parts = getEspIp().split('.');
@@ -1798,64 +1809,88 @@ async function diagnoseNetwork() {
diagResult = result.join(';');
return diagResult;
}
/** @description 调度下一次 /system/status 轮询(递归 setTimeout)。
* 上一次请求完成(或 3s 超时)后才调度下一次,避免 setInterval 把慢响应
* 直接 abort 而导致堆/任务栈/CPU 等卡片刷新率不稳。
*/
function scheduleStatusPoll(delayMs) {
if (statusTimerId) clearTimeout(statusTimerId);
statusTimerId = setTimeout(function() {
statusTimerId = null;
pollStatus();
}, delayMs !== undefined ? delayMs : statusPollIntervalMs);
}
/** @description 启动定时轮询(状态 + 日志)。 */
function startPolling() {
consecutivePollFailures = 0;
pollSlowMode = false;
if (statusTimerId) clearInterval(statusTimerId);
statusPollIntervalMs = 1000;
if (statusTimerId) clearTimeout(statusTimerId);
if (logsTimerId) clearInterval(logsTimerId);
statusTimerId = setInterval(pollStatus, 1000);
scheduleStatusPoll(0); // 立即开始第一次,完成后按 interval 递归调度
logsTimerId = setInterval(pollLogs, 1000);
}
function slowDownPolling() {
pollSlowMode = true;
if (statusTimerId) clearInterval(statusTimerId);
statusPollIntervalMs = 10000;
// 不中断正在进行的请求;它完成后会按新的 10s 间隔调度下一次
if (!statusPollInFlight) {
if (statusTimerId) clearTimeout(statusTimerId);
scheduleStatusPoll(statusPollIntervalMs);
}
if (logsTimerId) clearInterval(logsTimerId);
statusTimerId = setInterval(pollStatus, 10000);
logsTimerId = setInterval(pollLogs, 10000);
}
function restorePolling() {
if (pollSlowMode) {
pollSlowMode = false;
consecutivePollFailures = 0;
if (statusTimerId) clearInterval(statusTimerId);
statusPollIntervalMs = 1000;
if (logsTimerId) clearInterval(logsTimerId);
statusTimerId = setInterval(pollStatus, 1000);
logsTimerId = setInterval(pollLogs, 1000);
}
// 如果当前没有请求在飞,立即触发一次,确保 UI 尽快恢复
if (!statusPollInFlight) {
if (statusTimerId) clearTimeout(statusTimerId);
scheduleStatusPoll(0);
}
}
function pausePollingForUpload() {
if (pollingPausedForUpload) return;
pollingPausedForUpload = true;
uploadPollChip = activeUploadChip || window.flashTarget || null;
consecutivePollFailures = 0;
if (statusTimerId) clearInterval(statusTimerId);
if (logsTimerId) clearInterval(logsTimerId);
statusPollIntervalMs = uploadPollChip === "gd32" ? 2500 : 3000;
// ★ ESP32/GD32 上传期间均保持轮询,让用户在网页上看到完整 OTA 进展
// ESP32 用 3s 间隔(上传为主),GD32 用 2.5s(中继进度依赖 status)
statusTimerId = setInterval(pollStatus, uploadPollChip === "gd32" ? 2500 : 3000);
logsTimerId = setInterval(pollLogs, uploadPollChip === "gd32" ? 2500 : 3000);
if (statusTimerId) clearTimeout(statusTimerId);
if (logsTimerId) clearInterval(logsTimerId);
scheduleStatusPoll(statusPollIntervalMs);
logsTimerId = setInterval(pollLogs, statusPollIntervalMs);
}
function resumePollingAfterUpload() {
if (!pollingPausedForUpload) return;
pollingPausedForUpload = false;
uploadPollChip = null;
consecutivePollFailures = 0;
if (pollSlowMode) {
statusTimerId = setInterval(pollStatus, 10000);
logsTimerId = setInterval(pollLogs, 10000);
statusPollIntervalMs = pollSlowMode ? 10000 : 1000;
if (logsTimerId) clearInterval(logsTimerId);
logsTimerId = setInterval(pollLogs, statusPollIntervalMs);
// 上传结束立即刷新一次状态;若当前已在请求中,则让它完成后按新间隔继续
if (statusPollInFlight) {
if (statusTimerId) clearTimeout(statusTimerId);
} else {
statusTimerId = setInterval(pollStatus, 1000);
logsTimerId = setInterval(pollLogs, 1000);
scheduleStatusPoll(0);
}
pollStatus();
pollLogs();
}
/** @description 轮询 ESP32 系统状态(GET /status)并更新 UI。 */
async function pollStatus() {
// ★ 上传期间状态轮询间隔已在 pausePollingForUpload 中缩短,此处不再拦截
// 取消上一次 in-flight 请求,防止慢响应堆积导致 UI 瞬间刷新多次
if (statusAbortCtrl) { try { statusAbortCtrl.abort(); } catch(_e) {} }
// 递归 setTimeout 调度,正常情况下不会重叠;做个保险避免并发请求
if (statusPollInFlight) return;
statusPollInFlight = true;
statusAbortCtrl = new AbortController();
var pollTimer = setTimeout(function() { try { statusAbortCtrl.abort(); } catch(_e) {} }, 3000);
try {
@@ -1942,6 +1977,9 @@ async function pollStatus() {
updateConnectionUi(false, '未连接');
applyDisconnectedRuntimeState();
}
} finally {
statusPollInFlight = false;
scheduleStatusPoll();
}
}
startPolling();
@@ -1953,7 +1991,13 @@ document.addEventListener('visibilitychange', function () {
pageVisible = !document.hidden;
if (!pageVisible) return;
consecutivePollFailures = 0;
pollStatus();
// 切回前台立即刷新状态;若当前已在请求中,取消等待中的下一次让它尽快重排
if (statusPollInFlight) {
if (statusTimerId) clearTimeout(statusTimerId);
} else {
if (statusTimerId) clearTimeout(statusTimerId);
scheduleStatusPoll(0);
}
pollLogs();
// 之前若在慢速模式(10s),切回前台先用 1s 观察一会儿
if (pollSlowMode) restorePolling();