feat(transport): 添加SPI Ready握手引脚支持并优化GPIO驱动依赖

添加ESP32-C3 GPIO驱动组件以支持SPI Ready握手机制,新增
SPI_READY_GPIO(10)引脚配置用于监测GD32 PB6就绪状态,避免SPI
通信时从机未准备好的丢帧问题。
This commit is contained in:
小龙虾
2026-07-06 19:14:13 +08:00
parent 2fe5393769
commit 069ce8e6a5
17 changed files with 251 additions and 76 deletions
+52 -9
View File
@@ -27,6 +27,7 @@ let latestSystemStatus = null;
let isEsp32Rebooting = false;
let rebootStartTime = 0;
let pollingPausedForUpload = false;
let uploadPollChip = null; // 当前暂停轮询的上传目标,用于区分 WiFi/日志策略
let activeXhr = null; // 当前上传 XHR,用于 abort
let uploadBtnOrigText = ''; // 上传按钮原始文字
let activeUploadChip = null; // 当前上传目标芯片 ('esp32' | 'gd32' | null)
@@ -588,6 +589,9 @@ function updateHeapAndTaskStatusUi(data) {
setText('sOtaState', (ota.sys_state || '--') + (Number.isFinite(ota.sys_state_code) ? ' (' + ota.sys_state_code + ')' : ''));
setText('sRelayState', Number.isFinite(ota.relay_state) ? String(ota.relay_state) : '--');
// 缓存最近一次成功的 sys_state_code / relay_state,供 pollStatus 失败时判断设备是否在忙
if (Number.isFinite(ota.sys_state_code)) lastSysStateCode = ota.sys_state_code;
if (Number.isFinite(ota.relay_state)) lastRelayState = ota.relay_state;
var storedText = '--';
if (ota.total_size || ota.stored_size) {
storedText = formatBytes(ota.stored_size || 0) + ' / ' + formatBytes(ota.total_size || 0);
@@ -795,6 +799,7 @@ function updateSystemStatusUi(data) {
if (stErr) stErr.textContent = 'GD32 错误(待机)';
} else if (isGd32Complete || rs === 5 || (gd32AppAlive && sysState === 'GD32_COMPLETE')) {
markPhaseDone('gd32Ota');
currentOtaTarget = null; // GD32 OTA 完成,允许切换目标
setOtaProgressBadge(100);
setProgress(100, 'GD32 刷写完成');
var sbDone = document.getElementById('stageStatusBar');
@@ -830,6 +835,7 @@ function updateSystemStatusUi(data) {
setOtaProgressBadge(95);
if (gd32AppAlive) {
markPhaseDone('gd32Ota');
currentOtaTarget = null; // GD32 OTA 完成,允许切换目标
setProgress(100, 'GD32 刷写完成');
var sb = document.getElementById('stageStatusBar');
var st = document.getElementById('stageStatusText');
@@ -856,7 +862,7 @@ async function testConnection() {
// 立即发一次请求检测
try {
var resp = await fetch(getBaseUrl() + '/system/status', {
signal: AbortSignal.timeout(3000)
signal: statusAbortCtrl.signal
});
if (resp.ok) {
var data = await resp.json();
@@ -885,10 +891,10 @@ async function refreshSystemStatusNow(event) {
event.preventDefault();
event.stopPropagation();
}
if (pollingPausedForUpload) return;
// ★ ESP32 上传期间也允许日志轮询,让用户看到完整进展
try {
var resp = await fetch(getBaseUrl() + '/system/status', {
signal: AbortSignal.timeout(3000)
signal: statusAbortCtrl.signal
});
if (!resp.ok) {
addLog('E', '状态', '刷新失败: HTTP ' + resp.status);
@@ -1749,6 +1755,9 @@ var logsTimerId = null;
var pollSlowMode = false; // 降速模式(10s 一次)
var lastDiagTime = 0; // 上次诊断时间
var diagResult = ''; // 最近一次诊断结果
var lastSysStateCode = -1; // 上次成功的 sys_state_code,用于失败时判断设备是否在忙
var lastRelayState = 0; // 上次成功的 relay_state
var statusAbortCtrl = null; // 取消上一次 in-flight pollStatus,防止堆积刷新
// 从设备 IP 推导网关 IP(取前三段 + .1)
function getGatewayIp() {
var parts = getEspIp().split('.');
@@ -1818,15 +1827,19 @@ function restorePolling() {
function pausePollingForUpload() {
if (pollingPausedForUpload) return;
pollingPausedForUpload = true;
uploadPollChip = activeUploadChip || window.flashTarget || null;
consecutivePollFailures = 0;
if (statusTimerId) clearInterval(statusTimerId);
if (logsTimerId) clearInterval(logsTimerId);
statusTimerId = null;
logsTimerId = null;
// ★ ESP32/GD32 上传期间均保持轮询,让用户在网页上看到完整 OTA 进展
// ESP32 用 3s 间隔(上传为主),GD32 用 2.5s(中继进度依赖 status)
statusTimerId = setInterval(pollStatus, uploadPollChip === "gd32" ? 2500 : 3000);
logsTimerId = setInterval(pollLogs, uploadPollChip === "gd32" ? 2500 : 3000);
}
function resumePollingAfterUpload() {
if (!pollingPausedForUpload) return;
pollingPausedForUpload = false;
uploadPollChip = null;
consecutivePollFailures = 0;
if (pollSlowMode) {
statusTimerId = setInterval(pollStatus, 10000);
@@ -1840,10 +1853,14 @@ function resumePollingAfterUpload() {
}
/** @description 轮询 ESP32 系统状态(GET /status)并更新 UI。 */
async function pollStatus() {
if (pollingPausedForUpload) return;
// ★ 上传期间状态轮询间隔已在 pausePollingForUpload 中缩短,此处不再拦截
// 取消上一次 in-flight 请求,防止慢响应堆积导致 UI 瞬间刷新多次
if (statusAbortCtrl) { try { statusAbortCtrl.abort(); } catch(_e) {} }
statusAbortCtrl = new AbortController();
var pollTimer = setTimeout(function() { try { statusAbortCtrl.abort(); } catch(_e) {} }, 3000);
try {
var resp = await fetch(getBaseUrl() + '/system/status', {
signal: AbortSignal.timeout(3000)
signal: statusAbortCtrl.signal
});
if (resp.ok) {
var data = await resp.json();
@@ -1865,12 +1882,26 @@ async function pollStatus() {
}
try { updateSystemStatusUi(data); } catch (e2) { console.warn('updateSystemStatusUi 异常:', e2); }
}
clearTimeout(pollTimer);
} catch (e) {
clearTimeout(pollTimer);
// 自身 AbortController 触发的取消不算失败,静默返回
if (e && e.name === "AbortError") return;
// 后台期间 setInterval 会被 Chrome/Edge 节流到最低 1 分钟/次,
// 因此后台累积的失败计数不应递增,避免切回前台瞬间误触发 5-fail 诊断。
if (pageVisible) consecutivePollFailures++;
// 判断设备是否处于"已知忙/重启"状态(依据最近一次成功轮询的 sys_state_code):
// 1=GD32_STORING, 3=GD32_RELAYING, 5=ESP32_RECEIVING,
// 6=ESP32_FINALIZING, 7=ESP32_REBOOTING
// 也包含 relay_state 非 IDLE(1-4 表示握手/协商/传输/收尾)
var lastDeviceBusy =
lastSysStateCode === 1 || lastSysStateCode === 3 ||
lastSysStateCode === 5 || lastSysStateCode === 6 ||
lastSysStateCode === 7 ||
(lastRelayState >= 1 && lastRelayState <= 4);
// OTA/传输/重启中屏蔽网关无响应的检测功能(设备忙或正在重启,无法响应轮询是正常的)
if (pageVisible && consecutivePollFailures >= 5 && !pollSlowMode && !isUploading && !isEsp32Rebooting && !currentOtaTarget) {
// 设备已知忙时:保持 1s 轮询,不触发降速诊断,避免错过设备恢复的即时反馈
if (pageVisible && consecutivePollFailures >= 5 && !pollSlowMode && !isUploading && !isEsp32Rebooting && !currentOtaTarget && !lastDeviceBusy) {
addLog('W', '诊断', '设备连续 5 次无响应,正在诊断网络...');
var diag = await diagnoseNetwork();
addLog('W', '诊断', diag);
@@ -1895,6 +1926,18 @@ async function pollStatus() {
} else {
applyRebootingRuntimeState();
}
} else if (lastDeviceBusy) {
// 设备在 OTA/中继/重启期间不响应是正常的:保持 1s 轮询,UI 标记为"设备忙"
// 不调用 applyRebootingRuntimeState(避免显示"重启中 Xs"误导)
var pc = document.getElementById('pillConn');
var pct = document.getElementById('pillConnTxt');
if (pc) pc.className = 'pill processing';
if (pct) pct.textContent = '设备忙';
var cs = document.getElementById('connStatus');
var cst = document.getElementById('connStatusTxt');
if (cs) cs.className = 'conn-indicator warn';
if (cst) cst.textContent = '设备忙(OTA/重启中),保持 1s 轮询';
updateConnectionUi(true, '设备忙');
} else if (!pollSlowMode) {
updateConnectionUi(false, '未连接');
applyDisconnectedRuntimeState();
@@ -1919,7 +1962,7 @@ document.addEventListener('visibilitychange', function () {
var lastLogTimestamp = 0;
/** @description 轮询 ESP32 日志(GET /logs)并追加到日志面板。 */
async function pollLogs() {
if (pollingPausedForUpload) return;
// ★ ESP32 上传期间也允许日志轮询
try {
var resp = await fetch(getBaseUrl() + '/logs', {
signal: AbortSignal.timeout(2000)