chore: 解决 stash pop 与 device-link 合并冲突并收回本地 WIP
保留 ConnectionHub(deviceLink 导入/ECAT 自启/pmHold 幂等)与主线 WIP(断线清指标、AUTO_POLL=false、boot_ecat 等)。
This commit is contained in:
@@ -31,7 +31,7 @@ import {
|
||||
markPhaseDone, setProgress, resetProgress, esp32PhaseLabels
|
||||
} from './ota/progress.js';
|
||||
import {
|
||||
updateStagesFromRelayState, updateSystemStatusUi
|
||||
updateStagesFromRelayState, updateSystemStatusUi, clearLiveMetricsOnDisconnect
|
||||
} from './ota/stages.js';
|
||||
import { startEcatLoadPoller } from './ota/ecat_load.js';
|
||||
import { deviceLink } from './common/device-link.js';
|
||||
@@ -45,6 +45,9 @@ import {
|
||||
triggerGd32Relay, markGd32OtaUiComplete, stopRelayProgressWatch,
|
||||
startRelayProgressWatch, emitRelayProgress, ensureRelayProgressComplete
|
||||
} from './ota/upload-gd32.js';
|
||||
import {
|
||||
bootEcatProbe, bootEcatFlash, bootEcatAbort, bootEcatPick, bootEcatReboot, refreshBootEcatUi
|
||||
} from './ota/boot_ecat.js';
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
function formatPercent(value) {
|
||||
@@ -73,6 +76,9 @@ function updateUploadButtonLabel() {
|
||||
var xhrBusy = !!window.isUploading;
|
||||
if (esp32Btn) esp32Btn.disabled = !window.esp32File || xhrBusy;
|
||||
if (gd32Btn) gd32Btn.disabled = !window.gd32File || xhrBusy;
|
||||
/* EtherCAT 直灌那条通道共用同一块 flash 与同一份已选镜像,可用性在这里一次算完,
|
||||
* 不在 file-handle / progress / console-mode 各抄一遍条件。 */
|
||||
refreshBootEcatUi();
|
||||
}
|
||||
function resetStageFlow() {
|
||||
['s1', 's2', 's3', 's4', 's5', 's6'].forEach(function (id) {
|
||||
@@ -91,6 +97,8 @@ function resetStageFlow() {
|
||||
function applyDisconnectedRuntimeState() {
|
||||
document.getElementById('pillIP').textContent = '--';
|
||||
document.getElementById('pillRSSI').textContent = '--';
|
||||
// 断线约 1s(本轮轮询失败)即清实时指标,防止残留旧温度/电压误导
|
||||
clearLiveMetricsOnDisconnect();
|
||||
// ★ 状态机守卫:会话活跃时不重置进度/阶段,避免覆盖进行中的 OTA 进度
|
||||
if (otaSessionActive()) return;
|
||||
// FAILED 态:保留失败进度显示,只清连接胶囊(让用户看到失败原因)
|
||||
@@ -158,6 +166,7 @@ window.resetProgress = resetProgress;
|
||||
|
||||
window.updateStagesFromRelayState = updateStagesFromRelayState;
|
||||
window.updateSystemStatusUi = updateSystemStatusUi;
|
||||
window.clearLiveMetricsOnDisconnect = clearLiveMetricsOnDisconnect;
|
||||
|
||||
window.handleFileSelect = handleFileSelect;
|
||||
window.pickFileViaFSAPI = pickFileViaFSAPI;
|
||||
@@ -179,6 +188,13 @@ window.startRelayProgressWatch = startRelayProgressWatch;
|
||||
window.emitRelayProgress = emitRelayProgress;
|
||||
window.ensureRelayProgressComplete = ensureRelayProgressComplete;
|
||||
|
||||
window.bootEcatProbe = bootEcatProbe;
|
||||
window.bootEcatFlash = bootEcatFlash;
|
||||
window.bootEcatAbort = bootEcatAbort;
|
||||
window.bootEcatPick = bootEcatPick;
|
||||
window.bootEcatReboot = bootEcatReboot;
|
||||
window.refreshBootEcatUi = refreshBootEcatUi;
|
||||
|
||||
// ==================== 挂载 common/* 函数到 window(供 polling.js 等通过 window.* 调用) ====================
|
||||
window.formatBytes = formatBytes;
|
||||
window.formatTime = formatTime;
|
||||
@@ -231,6 +247,11 @@ document.addEventListener('click', function (ev) {
|
||||
case 'ble-pick-file': var fi = document.getElementById('firmwareFile'); if (fi) fi.click(); break;
|
||||
case 'ble-clear-file': if (window.bleOta) window.bleOta.clearFile(); break;
|
||||
case 'start-upload': startUpload(arg); break;
|
||||
case 'boot-ecat-probe': bootEcatProbe(true); break;
|
||||
case 'boot-ecat-pick': bootEcatPick(); break;
|
||||
case 'boot-ecat-run': bootEcatFlash(); break;
|
||||
case 'boot-ecat-abort': bootEcatAbort(); break;
|
||||
case 'boot-ecat-reboot': bootEcatReboot(); break;
|
||||
case 'restart-esp32': restartEsp32(); break;
|
||||
case 'refresh-system-status': refreshSystemStatusNow(ev); break;
|
||||
}
|
||||
|
||||
@@ -286,8 +286,12 @@ export async function pollStatus() {
|
||||
window.applyDeviceBusyRuntimeState('设备忙', '设备忙(OTA 后台运行中),等待回 IDLE', '设备忙');
|
||||
updateConnectionUi(true, '设备忙');
|
||||
} else {
|
||||
// 设备已回 IDLE:连接胶囊显示未连接,但失败进度保留
|
||||
// 设备已回 IDLE:连接胶囊显示未连接,但失败进度保留;
|
||||
// 实时指标同样清空,避免残留断线前温度/电压
|
||||
updateConnectionUi(false, '未连接');
|
||||
if (typeof window.clearLiveMetricsOnDisconnect === 'function') {
|
||||
window.clearLiveMetricsOnDisconnect();
|
||||
}
|
||||
}
|
||||
} else if (window.otaSessionState === window.OtaSessionState.UPLOADING) {
|
||||
window.applyUploadingRuntimeState();
|
||||
|
||||
+67
-21
@@ -31,16 +31,32 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
var esp32El = document.getElementById('esp32Stages');
|
||||
var gd32El = document.getElementById('gd32Stages');
|
||||
var bleEl = document.getElementById('bleStages');
|
||||
var ecatEl = document.getElementById('ecatStages');
|
||||
if (esp32El) esp32El.style.display = 'none';
|
||||
if (gd32El) gd32El.style.display = 'none';
|
||||
if (ecatEl) ecatEl.style.display = 'none';
|
||||
if (bleEl) bleEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
if (target === 'ecat') {
|
||||
window.currentOtaTarget = 'ecat';
|
||||
var esp32E = document.getElementById('esp32Stages');
|
||||
var gd32E = document.getElementById('gd32Stages');
|
||||
var bleE = document.getElementById('bleStages');
|
||||
var ecatE = document.getElementById('ecatStages');
|
||||
if (esp32E) esp32E.style.display = 'none';
|
||||
if (gd32E) gd32E.style.display = 'none';
|
||||
if (bleE) bleE.style.display = 'none';
|
||||
if (ecatE) ecatE.style.display = '';
|
||||
return;
|
||||
}
|
||||
if (typeof originalSwitchStageFlow === 'function') {
|
||||
originalSwitchStageFlow(target);
|
||||
}
|
||||
var bleEl = document.getElementById('bleStages');
|
||||
if (bleEl) bleEl.style.display = 'none';
|
||||
var bleEl2 = document.getElementById('bleStages');
|
||||
if (bleEl2) bleEl2.style.display = 'none';
|
||||
var ecatEl2 = document.getElementById('ecatStages');
|
||||
if (ecatEl2) ecatEl2.style.display = 'none';
|
||||
};
|
||||
|
||||
window.setConsoleMode = function (mode) {
|
||||
@@ -48,24 +64,33 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
// notifyFileChange → updateFileInfo → finalizeFileInfo → setProgress(0) 会覆盖
|
||||
// 进行中的 WiFi/BLE 上传进度(与 selectFlashTarget 同样的守卫策略)。
|
||||
var bleBusy = window.bleOta && window.bleOta.isUploading && window.bleOta.isUploading();
|
||||
if (window.isUploading || bleBusy) {
|
||||
var active = bleBusy ? 'BLE' : ((window.activeUploadChip || 'OTA').toUpperCase());
|
||||
if (window.isUploading || bleBusy || window.bootEcatBusy) {
|
||||
var active = window.bootEcatBusy ? 'ECAT' : (bleBusy ? 'BLE' : ((window.activeUploadChip || 'OTA').toUpperCase()));
|
||||
if (typeof addLog === 'function') addLog('W', 'OTA', active + ' 刷写进行中,无法切换连接模式');
|
||||
if (typeof showToast === 'function') showToast('当前有 ' + active + ' 任务在运行,请等待结束后再切换', 'error');
|
||||
return;
|
||||
}
|
||||
var isBle = mode === 'ble';
|
||||
window.connectionMode = isBle ? 'ble' : 'wifi';
|
||||
var m = (mode === 'ble' || mode === 'ecat') ? mode : 'wifi';
|
||||
var isBle = m === 'ble';
|
||||
window.connectionMode = m;
|
||||
var wifiCards = document.getElementById('wifiModeCards');
|
||||
var bleCards = document.getElementById('bleModeCards');
|
||||
var ecatCards = document.getElementById('ecatModeCards');
|
||||
var wifiBtn = document.getElementById('modeWifiBtn');
|
||||
var bleBtn = document.getElementById('modeBleBtn');
|
||||
if (wifiCards) wifiCards.style.display = isBle ? 'none' : '';
|
||||
if (bleCards) bleCards.style.display = isBle ? '' : 'none';
|
||||
if (wifiBtn) wifiBtn.className = isBle ? 'btn btn-outline' : 'btn btn-primary';
|
||||
if (bleBtn) bleBtn.className = isBle ? 'btn btn-primary' : 'btn btn-outline';
|
||||
var ecatBtn = document.getElementById('modeEcatBtn');
|
||||
if (wifiCards) wifiCards.style.display = m === 'wifi' ? '' : 'none';
|
||||
if (bleCards) bleCards.style.display = m === 'ble' ? '' : 'none';
|
||||
if (ecatCards) ecatCards.style.display = m === 'ecat' ? '' : 'none';
|
||||
if (wifiBtn) wifiBtn.className = 'btn target-tab-btn ' + (m === 'wifi' ? 'btn-primary' : 'btn-outline');
|
||||
if (bleBtn) bleBtn.className = 'btn target-tab-btn ' + (m === 'ble' ? 'btn-primary' : 'btn-outline');
|
||||
if (ecatBtn) ecatBtn.className = 'btn target-tab-btn ' + (m === 'ecat' ? 'btn-primary' : 'btn-outline');
|
||||
applySharedModeUi();
|
||||
syncBleFileFromSelectedTarget();
|
||||
/* 切到 EtherCAT 时自动探测一次,和 WiFi「测试」一样不用手点才知道通没通 */
|
||||
if (m === 'ecat' && typeof window.bootEcatProbe === 'function') {
|
||||
Promise.resolve(window.bootEcatProbe(false)).catch(function () { /* 后端未起时忽略 */ });
|
||||
}
|
||||
if (isBle) {
|
||||
window.switchStageFlow('ble');
|
||||
if (typeof setActivePhase === 'function') {
|
||||
@@ -74,6 +99,15 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
if (window.bleOta && typeof window.bleOta.refreshButtons === 'function') {
|
||||
window.bleOta.refreshButtons();
|
||||
}
|
||||
} else if (m === 'ecat') {
|
||||
window.switchStageFlow('ecat');
|
||||
if (typeof setActivePhase === 'function') setActivePhase('ecatOta');
|
||||
if (typeof window.resetEcatStages === 'function') window.resetEcatStages();
|
||||
if (typeof window.setProgress === 'function') window.setProgress(0, '等待刷写...');
|
||||
var sb = document.getElementById('stageStatusBar');
|
||||
var st = document.getElementById('stageStatusText');
|
||||
if (sb) sb.className = 'stage-status-bar';
|
||||
if (st) st.textContent = '待机';
|
||||
} else {
|
||||
window.switchStageFlow(window.flashTarget || (window.activePhase === 'esp32Ota' ? 'esp32' : 'gd32'));
|
||||
}
|
||||
@@ -83,8 +117,8 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
// OTA 上传进行中禁止切换目标:避免前端状态与后端 relay/upload 不同步导致卡死
|
||||
// ★ 兼容 BLE 模式:BLE 上传用 bleOta.state.uploading,window.isUploading 仅 WiFi 通道使用。
|
||||
var bleBusy = window.bleOta && window.bleOta.isUploading && window.bleOta.isUploading();
|
||||
if (window.isUploading || bleBusy) {
|
||||
var active = bleBusy ? 'BLE' : ((window.activeUploadChip || 'OTA').toUpperCase());
|
||||
if (window.isUploading || bleBusy || window.bootEcatBusy) {
|
||||
var active = window.bootEcatBusy ? 'ECAT' : (bleBusy ? 'BLE' : ((window.activeUploadChip || 'OTA').toUpperCase()));
|
||||
if (typeof addLog === 'function') addLog('W', 'OTA', active + ' 刷写进行中,无法切换目标');
|
||||
if (typeof showToast === 'function') showToast('当前有 ' + active + ' 任务在运行,请等待结束后再切换', 'error');
|
||||
return;
|
||||
@@ -273,31 +307,42 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
};
|
||||
|
||||
window.applySharedModeUi = function () {
|
||||
var isBle = window.connectionMode === 'ble';
|
||||
var m = (window.connectionMode === 'ble' || window.connectionMode === 'ecat')
|
||||
? window.connectionMode : 'wifi';
|
||||
var isBle = m === 'ble';
|
||||
var isEcat = m === 'ecat';
|
||||
var isGd32 = window.flashTarget === 'gd32';
|
||||
var targetEsp32 = document.getElementById('targetEsp32Tab');
|
||||
var targetGd32 = document.getElementById('targetGd32Tab');
|
||||
if (targetEsp32) targetEsp32.className = isGd32 ? 'btn btn-outline' : 'btn btn-primary';
|
||||
if (targetGd32) targetGd32.className = isGd32 ? 'btn btn-primary' : 'btn btn-outline';
|
||||
if (targetEsp32) targetEsp32.className = 'btn target-tab-btn ' + (isGd32 ? 'btn-outline' : 'btn-primary');
|
||||
if (targetGd32) targetGd32.className = 'btn target-tab-btn ' + (isGd32 ? 'btn-primary' : 'btn-outline');
|
||||
/* EtherCAT 只能刷 GD32(引导程序写应用区),目标切换行留着会以为还能选 ESP32 */
|
||||
var targetRow = document.getElementById('targetTabRow');
|
||||
if (targetRow) targetRow.style.display = isEcat ? 'none' : '';
|
||||
var ids = {
|
||||
wifiEsp32UploadPane: !isGd32,
|
||||
wifiGd32UploadPane: isGd32,
|
||||
/* EtherCAT 复用 GD32 上传区(同一 fileInputGd32 / processFile),选文件 UI 与 WiFi/BLE 一致 */
|
||||
wifiEsp32UploadPane: !isEcat && !isGd32,
|
||||
wifiGd32UploadPane: isGd32 || isEcat,
|
||||
bleUploadPane: false,
|
||||
wifiEsp32ActionPane: !isBle && !isGd32,
|
||||
wifiGd32ActionPane: !isBle && isGd32,
|
||||
bleActionPane: isBle
|
||||
wifiEsp32ActionPane: !isBle && !isEcat && !isGd32,
|
||||
wifiGd32ActionPane: !isBle && !isEcat && isGd32,
|
||||
bleActionPane: isBle,
|
||||
bootEcatPane: isEcat
|
||||
};
|
||||
Object.keys(ids).forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.style.display = ids[id] ? '' : 'none';
|
||||
});
|
||||
/* EtherCAT 与 WiFi/BLE 共用同一张进度卡,只是切到 ecatStages + ppEcatOta */
|
||||
var hint = document.getElementById('readyHint');
|
||||
var autoBtn = document.getElementById('autoFlashBtn');
|
||||
if (autoBtn && isBle) {
|
||||
autoBtn.textContent = isGd32 ? '一键刷写 GD32' : '一键刷写 ESP32';
|
||||
}
|
||||
if (hint) {
|
||||
if (isBle) {
|
||||
if (isEcat) {
|
||||
hint.innerHTML = 'EtherCAT 模式:选择 GD32 固件 → 探测对面是谁 → 刷写 APP。';
|
||||
} else if (isBle) {
|
||||
hint.innerHTML = isGd32
|
||||
? 'BLE 模式 · 目标 GD32:连接设备 → 选择 GD32 固件 → 一键刷写。'
|
||||
: 'BLE 模式 · 目标 ESP32:连接设备 → 选择 ESP32 固件 → 一键刷写。';
|
||||
@@ -307,6 +352,7 @@ window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||||
hint.innerHTML = 'WiFi 模式:上传 ESP32 固件后自动执行 OTA 并重启。';
|
||||
}
|
||||
}
|
||||
if (typeof window.refreshBootEcatUi === 'function') window.refreshBootEcatUi();
|
||||
};
|
||||
|
||||
applySharedModeUi();
|
||||
|
||||
+104
-6
@@ -10,8 +10,19 @@
|
||||
* 与 /system/status 轮询完全独立:ESP32 离线不影响本显示,反之亦然;
|
||||
* 失败只静默退避,绝不计入 ESP32 的连接失败计数。
|
||||
*
|
||||
* **自动拉起**(2026-09):读到 invalid/过期时,代替用户走完
|
||||
* 服务→网卡→OP→PDO 循环 四级自愈(与 motor/transport-ecat.js 同一套 API),
|
||||
* 不必再单独打开电机页选 EtherCAT。避让:
|
||||
* - ECAT 刷写中(window.bootEcatBusy)或其它 OTA 在跑时**绝不动总线**
|
||||
* —— open 会 loop_stop、motor/start 会抢 OP,会把正在灌写的会话掐死;
|
||||
* - 冷却 8s,避免无适配器/无从站时每秒刷 open;
|
||||
* - 首次成功只打一条日志,之后静默。
|
||||
* 首页需持有 app 租约(index.html 挂 app-lease.js),否则服务端在
|
||||
* 租约归零宽限期后会把刚拉起的循环又停掉。
|
||||
*
|
||||
* 失效语义(都显示 '--'):
|
||||
* - valid:false —— ECAT 后端没在跑 / 总线从未打开过 / 固件 < 1.5.61(段 5 恒 0);
|
||||
* - valid:false —— ECAT 后端没在跑 / 总线从未打开过 / 固件 < 1.5.61(段 5 恒 0)
|
||||
* / 循环刚拉起、首个 1s 窗口还没滚完(seq 仍 0);
|
||||
* - age_ms > 3000 —— PDO 循环已停(没有页面持有 app 租约时 ecat_native 会
|
||||
* 自动停循环退回 INIT),负载值不再滚动,视为过期;
|
||||
* - 代理 503 —— ecat_native 没在跑。退避轮询(1s→5s→30s),避免不用 EtherCAT
|
||||
@@ -23,9 +34,12 @@ import { deviceLink } from '../common/device-link.js';
|
||||
|
||||
var POLL_MS = 1000; /* 正常轮询节拍(负载窗口 1s 一滚,再快没意义) */
|
||||
var STALE_MS = 3000; /* 超过 3 个窗口没刷新即视为过期 */
|
||||
var START_COOLDOWN_MS = 8000; /* 自动 open/start 冷却,防无设备时刷屏 */
|
||||
|
||||
var failCount = 0; /* 连续失败/过期次数,驱动退避 */
|
||||
var timerId = null;
|
||||
var startAttemptAt = 0; /* 上次尝试自动拉起的时刻 */
|
||||
var startLogged = false; /* 成功拉起只记一条日志 */
|
||||
|
||||
function el() { return document.getElementById('mGd32Load'); }
|
||||
|
||||
@@ -41,8 +55,9 @@ function setTitle(node, title) {
|
||||
}
|
||||
|
||||
var TIP_BASE = 'DCU_APP CPU 负载率:中断+非延迟处理(任务派发/OTA中继)占墙钟比,不含常开轮询与空转;EtherCAT 输入 PDO 段5,1s 窗口';
|
||||
var TIP_OFF = TIP_BASE + ';当前不可用:控制台外壳启动时会自动 open+motor/start;'
|
||||
+ '若仍为 --,检查网线/从站供电,或手动打开电机控制页选 EtherCAT。';
|
||||
var TIP_OFF = TIP_BASE + ';当前不可用:外壳/本页会自动 open+motor/start;'
|
||||
+ '若持续为 --,检查网卡/总线供电,或手动打开电机控制页选 EtherCAT。'
|
||||
+ 'ECAT 刷写期间会暂停自动拉起。';
|
||||
|
||||
/** @description 把 /api/app/load 的 JSON 渲染到 #mGd32Load。返回值是否有效(驱动退避)。 */
|
||||
function render(d) {
|
||||
@@ -71,6 +86,81 @@ function render(d) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function ecatPost(path, body, timeoutMs) {
|
||||
return fetch('/ecat/api/' + path.replace(/^\/+/, ''), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {}),
|
||||
signal: AbortSignal.timeout(timeoutMs || 8000),
|
||||
cache: 'no-store',
|
||||
}).then(function (r) { return r.json(); });
|
||||
}
|
||||
|
||||
function adapterSubstr() {
|
||||
try {
|
||||
var s = localStorage.getItem('dcu_ecat_adapter');
|
||||
if (s && s.trim()) return s.trim();
|
||||
} catch (_) {}
|
||||
return 'I210';
|
||||
}
|
||||
|
||||
/** 刷写/上传进行中:绝不动总线(open=loop_stop,start=抢 OP)。 */
|
||||
function busBusy() {
|
||||
return !!window.bootEcatBusy || !!window.isUploading
|
||||
|| !!(window.bleOta && window.bleOta.isUploading && window.bleOta.isUploading());
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保网卡已开 + OP + 后台 PDO 循环在跑。
|
||||
* 与 motor/transport-ecat.js 的 ensureOpen/ensureStarted 同一套 API,
|
||||
* 只做布尔成败,不引入 transient/慢请求表(本页没有那套基础设施)。
|
||||
*/
|
||||
async function ensurePdoCycle() {
|
||||
var st = null;
|
||||
try {
|
||||
st = await ecatPost('status', {}, 6000);
|
||||
} catch (_) {
|
||||
/* 后端未起:下面 open 会触发代理 _ecat_autostart */
|
||||
}
|
||||
if (!st || st.ok !== true) {
|
||||
var o = await ecatPost('open', { substr: adapterSubstr() }, 25000);
|
||||
if (!o || o.ok !== true) return false;
|
||||
}
|
||||
/* 周期/自旋与电机页共用 localStorage,用户调过的档位不被本页覆盖。 */
|
||||
var body = {};
|
||||
try {
|
||||
var v = parseFloat(localStorage.getItem('dcu_ecat_period_ms'));
|
||||
if (v > 0) body.period_ms = v;
|
||||
var sp = localStorage.getItem('dcu_ecat_spin');
|
||||
if (sp === '0' || sp === '1') body.spin = (sp === '1');
|
||||
} catch (_) {}
|
||||
/* motor/start 内部未进 OP 时自己走 PREOP→SAFEOP→OP,最坏约 24s */
|
||||
var sr = await ecatPost('motor/start', body, 30000);
|
||||
return !!(sr && sr.ok === true);
|
||||
}
|
||||
|
||||
/** 读数无效时尝试自动拉起。冷却 + 刷写避让。返回是否本次调用成功启动。 */
|
||||
async function tryAutoStart() {
|
||||
if (busBusy()) return false;
|
||||
var now = Date.now();
|
||||
if (now - startAttemptAt < START_COOLDOWN_MS) return false;
|
||||
startAttemptAt = now;
|
||||
try {
|
||||
var ok = await ensurePdoCycle();
|
||||
if (ok) {
|
||||
if (!startLogged) {
|
||||
startLogged = true;
|
||||
if (window.addLog) {
|
||||
window.addLog('I', 'ECAT',
|
||||
'已自动拉起 EtherCAT PDO 循环(GD32 负载将在首个 1s 窗口后显示)');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (_) { /* 代理/后端离线、open/start 失败:走退避 */ }
|
||||
return false;
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
var node = el();
|
||||
if (!node) return; /* 页面上没有该指标就不再调度 */
|
||||
@@ -78,10 +168,18 @@ async function tick() {
|
||||
try {
|
||||
ok = render(await deviceLink.ecatPost('app/load', {}, 3000));
|
||||
} catch (_) { /* 代理/后端离线,走退避 */ }
|
||||
if (!ok) {
|
||||
failCount++;
|
||||
} else {
|
||||
if (ok) {
|
||||
failCount = 0;
|
||||
} else {
|
||||
var started = false;
|
||||
try { started = await tryAutoStart(); } catch (_) {}
|
||||
if (started) {
|
||||
/* 刚 start 成功时 seq 仍可能是 0(首个 1s 窗口未滚完),
|
||||
* 温和计数:保持 1s 节拍等窗口,不要立刻 5s/30s 退避。 */
|
||||
failCount = Math.min(failCount, 1);
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
var wait = POLL_MS;
|
||||
if (failCount > 6) wait = 30000;
|
||||
|
||||
+6
-2
@@ -17,7 +17,7 @@ import { addLog } from '../common/log.js';
|
||||
var ESP32_STAGE_IDS = ['es1', 'es2', 'es3', 'es4', 'es5', 'es6'];
|
||||
var ESP32_DETAIL_IDS = ['esd1', 'esd2', 'esd3', 'esd4', 'esd5', 'esd6'];
|
||||
// 顶部步骤 pill → phase 名映射
|
||||
var STEP_MAP = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota' };
|
||||
var STEP_MAP = { esp32Ota: 'ppEsp32', gd32Ota: 'ppGd32Ota', ecatOta: 'ppEcatOta' };
|
||||
// 进度条 DOM 元素懒缓存(首次访问后复用,DOM 未就绪时回退到 getElementById)
|
||||
var progressFillEl = null;
|
||||
var progressTxtEl = null;
|
||||
@@ -228,6 +228,10 @@ export function markPhaseDone(phase) {
|
||||
|
||||
/** @description 更新进度条百分比和文本显示。对 pct 做边界校验,过滤 NaN/undefined。 */
|
||||
export function setProgress(pct, text) {
|
||||
/* EtherCAT 直灌期间进度条由 boot_ecat 的 setEcatPct 独占。
|
||||
* pollStatus→stages 的 GD32 OTA 中继进度(60+0.3*pct,常落在 44–45)
|
||||
* 会把灌写 0–100% 冲成假值,这里一刀切掉。 */
|
||||
if (window.bootEcatBusy) return;
|
||||
// 数值校验:BLE ACK 高频调用路径偶尔传入 undefined/NaN,需兜底避免出现 "undefined%"
|
||||
var n = Number(pct);
|
||||
if (!Number.isFinite(n)) n = 0;
|
||||
@@ -248,7 +252,7 @@ export function resetProgress() {
|
||||
window.activePhase = null;
|
||||
window.esp32OtaPhase = 0;
|
||||
switchStageFlow(null);
|
||||
['ppEsp32', 'ppGd32Ota'].forEach(function (id) {
|
||||
['ppEsp32', 'ppGd32Ota', 'ppEcatOta'].forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.classList.remove('active', 'done');
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*
|
||||
* 结构:
|
||||
* applyDashboardUi — 堆/连接/版本/温湿度等常驻面板
|
||||
* clearLiveMetricsOnDisconnect — 断线后清空实时指标(防残留误导)
|
||||
* applyGd32OtaSessionUi — GD32 会话进度/完成/失败(唯一 UI 应用路径)
|
||||
* updateSystemStatusUi — 入口:仪表盘 →(非 BLE/非 ESP32-OTA)会话
|
||||
*
|
||||
@@ -118,6 +119,32 @@ function setNodeTitle(el, title) {
|
||||
if (el && el.title !== title) el.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通信断开后清空 /system/status 驱动的实时指标,避免残留上次在线数值误导。
|
||||
* 调用时机:轮询失败判定离线(约 1 个轮询周期 ≈ 1s),且非 OTA 忙态路径。
|
||||
* 不清 #mGd32Load:该字段走 EtherCAT 独立通道,与 ESP32 HTTP 轮询无关。
|
||||
*/
|
||||
export function clearLiveMetricsOnDisconnect() {
|
||||
var tip = '通信中断,数据已清空(等待恢复后自动刷新)';
|
||||
var metricIds = ['mTemp', 'mGd32Temp', 'mVcc', 'mVdda', 'mGd32App', 'mGd32Boot', 'mGd32Init'];
|
||||
for (var i = 0; i < metricIds.length; i++) {
|
||||
var el = document.getElementById(metricIds[i]);
|
||||
if (!el) continue;
|
||||
setNodeText(el, '--');
|
||||
setNodeClass(el, 'v');
|
||||
setNodeTitle(el, tip);
|
||||
}
|
||||
var mh = document.getElementById('mHealth');
|
||||
if (mh) {
|
||||
setNodeText(mh, '--');
|
||||
setNodeClass(mh, 'v');
|
||||
}
|
||||
var uptimeEl = document.getElementById('pillUptime');
|
||||
if (uptimeEl) setNodeText(uptimeEl, '⏱ --');
|
||||
/* 不再展示过期 JSON,避免折叠面板里仍是断线前快照 */
|
||||
window.latestSystemStatus = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 常驻仪表盘:堆、连接、健康、版本、温度、IP/RSSI。
|
||||
* @param {object} data /system/status JSON
|
||||
@@ -503,6 +530,10 @@ export function updateSystemStatusUi(data) {
|
||||
if (window.connectionMode === 'ble') {
|
||||
return;
|
||||
}
|
||||
/* EtherCAT 直灌:进度/阶段由 boot_ecat 独占,中继轮询不得覆盖 */
|
||||
if (window.connectionMode === 'ecat' || window.bootEcatBusy) {
|
||||
return;
|
||||
}
|
||||
/* ESP32 OTA 进行中:勿用 GD32 relay 覆盖阶段条 */
|
||||
if (window.currentOtaTarget === 'esp32' && window.esp32OtaPhase > 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user