feat(transport): 添加SPI Ready握手引脚支持并优化GPIO驱动依赖
添加ESP32-C3 GPIO驱动组件以支持SPI Ready握手机制,新增 SPI_READY_GPIO(10)引脚配置用于监测GD32 PB6就绪状态,避免SPI 通信时从机未准备好的丢帧问题。
This commit is contained in:
+52
-9
@@ -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)
|
||||
|
||||
+15
-6
@@ -1039,12 +1039,6 @@
|
||||
return json;
|
||||
}
|
||||
|
||||
function buildSendResponseText(result) {
|
||||
if (result.status === 'ok') {
|
||||
return result.recv ? ('0x' + result.recv + ' (' + result.recv_len + ' 字节)') : '无响应数据';
|
||||
}
|
||||
return '发送失败: code=' + result.code;
|
||||
}
|
||||
|
||||
function updateResponseBox(box, textEl, result, mode) {
|
||||
if (!box || !textEl) return;
|
||||
@@ -1478,5 +1472,20 @@
|
||||
setTimeout(testConnection, 150);
|
||||
}
|
||||
|
||||
var CAN_SEND_ERR_DESC = {
|
||||
'-1': '参数错误(通道/ID/DLC 无效)',
|
||||
'-2': 'SPI 通信失败(ESP32 与 GD32 通信异常)',
|
||||
'-3': 'GD32 CAN 总线拒绝(BUS_OFF / TEC 过高 / 邮箱满)',
|
||||
};
|
||||
|
||||
function buildSendResponseText(result) {
|
||||
if (result.status === 'ok') {
|
||||
return result.recv ? ('0x' + result.recv + ' (' + result.recv_len + ' 字节)') : '无响应数据';
|
||||
}
|
||||
var code = String(result.code);
|
||||
var desc = CAN_SEND_ERR_DESC[code] || '未知错误';
|
||||
return '发送失败: ' + desc + ' (code=' + result.code + ')';
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
|
||||
@@ -53,8 +53,12 @@
|
||||
|
||||
// ── 1Hz 轮询 /can/feedback ─────────────────────────────────
|
||||
async function pollFeedback() {
|
||||
// 取消上一次 in-flight 请求,防止 SPI 慢响应堆积导致 UI 瞬间刷新多次
|
||||
if (state.fbAbortCtrl) { try { state.fbAbortCtrl.abort(); } catch(_e) {} }
|
||||
state.fbAbortCtrl = new AbortController();
|
||||
var fbPollTimer = setTimeout(function() { try { state.fbAbortCtrl.abort(); } catch(_e) {} }, 3000);
|
||||
try {
|
||||
var resp = await fetch(getBaseUrl() + '/can/feedback', { signal: AbortSignal.timeout(3000) });
|
||||
var resp = await fetch(getBaseUrl() + '/can/feedback', { signal: state.fbAbortCtrl.signal });
|
||||
if (!resp.ok) return;
|
||||
var data = await resp.json();
|
||||
var channels = data.channels || [];
|
||||
@@ -86,7 +90,11 @@
|
||||
el.fbPollStatus.className = 'pill online';
|
||||
}
|
||||
setConnState(true, '已连接');
|
||||
clearTimeout(fbPollTimer);
|
||||
} catch (e) {
|
||||
clearTimeout(fbPollTimer);
|
||||
// 自身 AbortController 触发的取消不算失败,静默返回
|
||||
if (e && e.name === "AbortError") return;
|
||||
if (el.fbPollStatus) {
|
||||
el.fbPollStatus.textContent = '轮询: 离线';
|
||||
el.fbPollStatus.className = 'pill offline';
|
||||
|
||||
+9
-2
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
index.html - ESP32 OTA 中继控制台主页面
|
||||
支持 WiFi OTA(ESP32 本机 + GD32 中继)和 BLE OTA 双通道固件升级。
|
||||
配套脚本:app.js (WiFi OTA), ble_ota.js (BLE OTA), style.css (UI 样式)
|
||||
@@ -427,7 +427,7 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=20260626a"></script>
|
||||
<script src="app.js?v=20260705b"></script>
|
||||
<script>
|
||||
window.connectionMode = window.connectionMode || 'wifi';
|
||||
window.flashTarget = window.flashTarget || 'esp32';
|
||||
@@ -484,6 +484,13 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
};
|
||||
|
||||
window.selectFlashTarget = function (target) {
|
||||
// OTA 上传进行中禁止切换目标:避免前端状态与后端 relay/upload 不同步导致卡死
|
||||
if (window.isUploading) {
|
||||
var active = (window.activeUploadChip || 'OTA').toUpperCase();
|
||||
if (typeof addLog === 'function') addLog('W', 'OTA', active + ' 刷写进行中,无法切换目标');
|
||||
if (typeof showToast === 'function') showToast('当前有 ' + active + ' 任务在运行,请等待结束后再切换', 'error');
|
||||
return;
|
||||
}
|
||||
window.flashTarget = target === 'gd32' ? 'gd32' : 'esp32';
|
||||
applySharedModeUi();
|
||||
syncBleFileFromSelectedTarget();
|
||||
|
||||
Reference in New Issue
Block a user