chore: 解决 stash pop 与 device-link 合并冲突并收回本地 WIP
保留 ConnectionHub(deviceLink 导入/ECAT 自启/pmHold 幂等)与主线 WIP(断线清指标、AUTO_POLL=false、boot_ecat 等)。
This commit is contained in:
+14
-1
@@ -445,6 +445,11 @@
|
||||
border-color: var(--red); color: #fff;
|
||||
box-shadow: 0 0 12px rgba(255,107,107,.4);
|
||||
}
|
||||
.stage-node.warn .stage-dot {
|
||||
background: linear-gradient(135deg, #e6a23c, #cf9236);
|
||||
border-color: #e6a23c; color: #1a1203;
|
||||
box-shadow: 0 0 12px rgba(230,162,60,.4);
|
||||
}
|
||||
.stage-label {
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px; color: var(--text-label); text-align: center; line-height: 1.3;
|
||||
@@ -457,10 +462,12 @@
|
||||
}
|
||||
.stage-node.live .stage-label,
|
||||
.stage-node.done .stage-label,
|
||||
.stage-node.err .stage-label { color: var(--text); }
|
||||
.stage-node.err .stage-label,
|
||||
.stage-node.warn .stage-label { color: var(--text); }
|
||||
.stage-node.live .stage-detail { color: var(--esp32-hi); }
|
||||
.stage-node.done .stage-detail { color: var(--green); }
|
||||
.stage-node.err .stage-detail { color: var(--red); }
|
||||
.stage-node.warn .stage-detail { color: #e6a23c; }
|
||||
|
||||
/* ---- 阶段状态条 ---- */
|
||||
.stage-status-bar {
|
||||
@@ -501,6 +508,12 @@
|
||||
}
|
||||
.stage-status-bar.err .ssb-dot { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.4); animation: none; }
|
||||
.stage-status-bar.err .ssb-text { color: var(--red); }
|
||||
.stage-status-bar.warn {
|
||||
border-color: rgba(230,162,60,.35);
|
||||
background: linear-gradient(135deg, rgba(230,162,60,.09), rgba(230,162,60,.02));
|
||||
}
|
||||
.stage-status-bar.warn .ssb-dot { background: #e6a23c; box-shadow: 0 0 8px rgba(230,162,60,.4); animation: none; }
|
||||
.stage-status-bar.warn .ssb-text { color: #e6a23c; }
|
||||
|
||||
/* ---- 校验信息面板 ---- */
|
||||
.stage-verify-info {
|
||||
|
||||
+125
-2
@@ -249,6 +249,69 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
color: #04142b;
|
||||
box-shadow: 0 4px 16px rgba(95,168,255,.3), inset 0 1px 0 rgba(255,255,255,.25);
|
||||
}
|
||||
#modeEcatBtn.btn-primary {
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
|
||||
color: #0f0a1f;
|
||||
box-shadow: 0 4px 16px rgba(139,92,246,.3), inset 0 1px 0 rgba(255,255,255,.25);
|
||||
}
|
||||
#modeWifiBtn.btn-outline {
|
||||
border-color: rgba(47,216,193,.45);
|
||||
color: var(--esp32);
|
||||
}
|
||||
#modeBleBtn.btn-outline {
|
||||
border-color: rgba(95,168,255,.45);
|
||||
color: var(--ble);
|
||||
}
|
||||
#modeEcatBtn.btn-outline {
|
||||
border-color: rgba(139,92,246,.45);
|
||||
color: #a78bfa;
|
||||
}
|
||||
#modeWifiBtn.btn-outline:hover:not(:disabled) {
|
||||
border-color: var(--esp32);
|
||||
background: rgba(47,216,193,.08);
|
||||
color: var(--esp32);
|
||||
}
|
||||
#modeBleBtn.btn-outline:hover:not(:disabled) {
|
||||
border-color: var(--ble);
|
||||
background: rgba(95,168,255,.08);
|
||||
color: var(--ble);
|
||||
}
|
||||
#modeEcatBtn.btn-outline:hover:not(:disabled) {
|
||||
border-color: #8b5cf6;
|
||||
background: rgba(139,92,246,.08);
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
/* 升级目标:左右两个等宽扁按钮(GD32 琥珀 · ESP32 青绿) */
|
||||
.target-tab-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.target-tab-btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.target-tab-btn .target-tab-ico {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
opacity: .9;
|
||||
}
|
||||
.target-tab-btn .target-tab-txt {
|
||||
line-height: 1.1;
|
||||
}
|
||||
.target-tab-btn.btn-outline {
|
||||
border-width: 1.5px;
|
||||
background: rgba(255,255,255,.03);
|
||||
}
|
||||
#targetGd32Tab.btn-primary {
|
||||
background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%);
|
||||
color: #2b1d02;
|
||||
@@ -257,6 +320,58 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
#targetEsp32Tab.btn-primary {
|
||||
background: linear-gradient(135deg, var(--esp32) 0%, #14b8a6 100%);
|
||||
color: #04231e;
|
||||
box-shadow: 0 4px 16px rgba(47,216,193,.28), inset 0 1px 0 rgba(255,255,255,.25);
|
||||
}
|
||||
/* 未选中时用各自子系统的淡描边,避免两个灰按钮看不出身份 */
|
||||
#targetGd32Tab.btn-outline {
|
||||
border-color: rgba(252,191,63,.45);
|
||||
color: var(--gd32);
|
||||
}
|
||||
#targetEsp32Tab.btn-outline {
|
||||
border-color: rgba(47,216,193,.45);
|
||||
color: var(--esp32);
|
||||
}
|
||||
#targetGd32Tab.btn-outline:hover:not(:disabled) {
|
||||
border-color: var(--gd32);
|
||||
background: rgba(252,191,63,.08);
|
||||
color: var(--gd32);
|
||||
}
|
||||
#targetEsp32Tab.btn-outline:hover:not(:disabled) {
|
||||
border-color: var(--esp32);
|
||||
background: rgba(47,216,193,.08);
|
||||
color: var(--esp32);
|
||||
}
|
||||
#ppEcatOta.active {
|
||||
color: #2b1d02;
|
||||
background: linear-gradient(135deg, var(--gd32), #f59e0b);
|
||||
box-shadow: 0 2px 12px rgba(252,191,63,.35);
|
||||
}
|
||||
|
||||
/* EtherCAT 连接说明 + 探测:与卡片内其他控件同一套字重/字距 */
|
||||
.ecat-mode-note {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: rgba(252,191,63,.06);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.ecat-mode-note b {
|
||||
color: var(--gd32);
|
||||
font-weight: 600;
|
||||
}
|
||||
#bootEcatProbeBtn {
|
||||
margin-bottom: 10px;
|
||||
border-color: rgba(252,191,63,.4);
|
||||
color: var(--gd32);
|
||||
background: rgba(252,191,63,.05);
|
||||
}
|
||||
#bootEcatProbeBtn:hover:not(:disabled) {
|
||||
border-color: var(--gd32);
|
||||
background: rgba(252,191,63,.12);
|
||||
color: var(--gd32-hi);
|
||||
}
|
||||
/* GD32 OTA 大按钮 = 琥珀(区别于 ESP32 青绿) */
|
||||
#uploadGd32Btn {
|
||||
@@ -264,6 +379,14 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
color: #2b1d02;
|
||||
box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3);
|
||||
}
|
||||
#bootEcatRunBtn {
|
||||
background: linear-gradient(135deg, var(--gd32) 0%, #f59e0b 100%);
|
||||
color: #2b1d02;
|
||||
box-shadow: 0 6px 20px rgba(252,191,63,.3), inset 0 1px 0 rgba(255,255,255,.3);
|
||||
}
|
||||
#bootEcatRunBtn:disabled {
|
||||
opacity: .38;
|
||||
}
|
||||
#uploadEsp32Btn {
|
||||
box-shadow: 0 6px 20px rgba(47,216,193,.32), inset 0 1px 0 rgba(255,255,255,.28);
|
||||
}
|
||||
@@ -275,10 +398,10 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||
font-size: 13px; font-weight: 500;
|
||||
}
|
||||
.conn-indicator .light { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.conn-indicator .light { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; background: var(--text-label); }
|
||||
.conn-indicator.ok .light { background: var(--green); box-shadow: 0 0 8px rgba(61,220,142,.6); }
|
||||
.conn-indicator.err .light { background: var(--red); box-shadow: 0 0 8px rgba(255,107,107,.5); }
|
||||
.conn-indicator.warn .light { background: var(--amber); }
|
||||
.conn-indicator.warn .light { background: var(--amber); box-shadow: 0 0 8px rgba(252,191,63,.45); }
|
||||
|
||||
.helper-note {
|
||||
margin-top: 10px; padding: 10px 13px;
|
||||
|
||||
@@ -671,6 +671,16 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
timeout = 30.0 if path.endswith(("/op/start", "/motor/start",
|
||||
"/flash", "/eeprom/write",
|
||||
"/eeprom/read", "/eeprom/verify")) else 6.0
|
||||
# EtherCAT 直灌 BOOT: run 要走 519 个邮箱帧 + 整区预擦, 几十秒量级。
|
||||
# 默认 6s 会在"正在正常刷写"的过程中假失败 —— 浏览器报错离场, native
|
||||
# 却还在往 flash 里写, 比不刷更糟。同一路由下的 open/push 都是纯内存
|
||||
# 毫秒级, 不连它们一起放大(后端没起时探测会干等几分钟), 故按 body 区分。
|
||||
# probe/reboot 走 boot_dl_probe 的两级邮箱自愈: 最坏 2×2s statecheck +
|
||||
# 重枚举 + 三次 700ms SDO 读 ≈ 7s+。这里必须给足, 因为 _ecat_proxy 超时
|
||||
# 后会拉起后端并**把同一个 POST 重发一次**(见其尾部重试), 带副作用的请求
|
||||
# 假超时 = 让后端把写过的命令再写一遍。
|
||||
if path.endswith("/ota/ecat"):
|
||||
timeout = 300.0 if b'"run"' in raw else 60.0
|
||||
try:
|
||||
status, body = _ecat_proxy(path, raw, timeout)
|
||||
_note_ecat_loop(path, status, body)
|
||||
|
||||
@@ -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;
|
||||
|
||||
+18
-9
@@ -17,7 +17,8 @@
|
||||
恢复可靠性(为什么睡不死):ESP32↔GD32 的 SPI 链路与 LAN9252 完全解耦(独立外设/中断),
|
||||
芯片睡得再深 0xA2 命令照样送达 GD32;NRST 硬复位覆盖 LAN9252 的一切软件可达状态
|
||||
(D3 睡死/HBI 假死都在内);GD32 自身挂了还有断电重启兜底(固件 hardwareReset 自动重走
|
||||
受控复位序列)。页面常驻 2s 自动状态轮询,睡没睡一目了然。
|
||||
受控复位序列)。自动状态轮询默认已关(AUTO_POLL=false),仅手动「读状态/唤醒/休眠」,
|
||||
避免持续 /spi/send 0xA2 在 ESP32 串口刷「WEB SPI」日志;需恢复自动刷新改 AUTO_POLL。
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -151,7 +152,7 @@
|
||||
<div>
|
||||
|
||||
<div class="card card--mem">
|
||||
<div class="card-head"><span class="ico">🔋</span> 芯片电源状态 <span style="margin-left:auto;font-size:.68rem;color:var(--text-label);font-weight:400">2s 自动刷新</span></div>
|
||||
<div class="card-head"><span class="ico">🔋</span> 芯片电源状态 <span style="margin-left:auto;font-size:.68rem;color:var(--text-label);font-weight:400">仅手动读状态</span></div>
|
||||
<div class="card-body">
|
||||
<div class="pm-sleep-banner" id="sleepBanner">🌙 芯片处于低功耗档 —— EtherCAT 通信已中断,点「手动唤醒」恢复</div>
|
||||
<div class="pm-grid">
|
||||
@@ -323,10 +324,11 @@
|
||||
$("sleepBanner").className = "pm-sleep-banner show";
|
||||
}
|
||||
|
||||
/* ---- 自动状态轮询:连接后 2s 一次;读到睡眠态即暂停(1.6.24 实测 HBI 读
|
||||
* 事务本身会唤醒睡眠中的芯片,轮询等于不停叫醒它);连败 3 次标红停轮。
|
||||
* pollGen:丢弃 enterMode/唤醒 之后仍在途的过期 1E 响应,避免竞态覆盖 UI。 ---- */
|
||||
var pollFail = 0, pollTimer = null, pollPaused = false, pollGen = 0;
|
||||
/* ---- 自动状态轮询已关闭(默认只手动):此前 2s 轮询会持续打 /spi/send 0xA2,
|
||||
* 在 ESP32 串口刷「WEB SPI」日志。手动「读状态 / 唤醒 / 休眠」仍可用。
|
||||
* 若需恢复自动刷新:把 AUTO_POLL 改 true。 ---- */
|
||||
var AUTO_POLL = false;
|
||||
var pollFail = 0, pollTimer = null, pollPaused = !AUTO_POLL, pollGen = 0;
|
||||
/** 主站看门狗豁免是否已置位(pmHold 幂等,防 2s 轮询刷屏) */
|
||||
var pmHoldOn = false;
|
||||
function bumpPollGen() { return ++pollGen; }
|
||||
@@ -369,6 +371,10 @@
|
||||
}
|
||||
function resumePolling() {
|
||||
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
||||
if (!AUTO_POLL) {
|
||||
pollPaused = true;
|
||||
return;
|
||||
}
|
||||
pollPaused = false;
|
||||
bumpPollGen();
|
||||
pollTimer = setTimeout(pollOnce, 2000);
|
||||
@@ -393,8 +399,9 @@
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
setConn(true, "已连接");
|
||||
log("设备可达: " + deviceIp(), "ok");
|
||||
pollOnce();
|
||||
log("设备可达: " + deviceIp() +
|
||||
(AUTO_POLL ? "。" : "。自动状态轮询已关闭,请点「读状态」手动查询。"), "ok");
|
||||
if (AUTO_POLL) pollOnce();
|
||||
})
|
||||
.catch(function (e) {
|
||||
setSpiFail("ESP32 探测失败: " + e.message + "(IP " + deviceIp() + ")");
|
||||
@@ -624,7 +631,9 @@
|
||||
pmHold(1);
|
||||
probeEspOnly();
|
||||
} else {
|
||||
log("就绪。状态每 2s 自动刷新;改设备 IP 请到 OTA 中继页(全局共享)。", "warn");
|
||||
log(AUTO_POLL
|
||||
? "就绪。状态每 2s 自动刷新;改设备 IP 请到 OTA 中继页(全局共享)。"
|
||||
: "就绪。自动状态轮询已关闭(避免持续 SPI 诊断流量);需要时点「读状态」。改设备 IP 请到 OTA 中继页。", "warn");
|
||||
probe();
|
||||
}
|
||||
})();
|
||||
|
||||
+543
-193
File diff suppressed because it is too large
Load Diff
+81
-12
@@ -13,7 +13,7 @@
|
||||
<!-- 空 favicon:阻止浏览器自动请求 /favicon.ico(本地静态服务无此文件会 404) -->
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="../css/base.css?v=857f83a5">
|
||||
<link rel="stylesheet" href="../css/layout.css?v=acd17001">
|
||||
<link rel="stylesheet" href="../css/layout.css?v=targettabs20260914g">
|
||||
<link rel="stylesheet" href="../css/components.css?v=a2c3d4e5">
|
||||
<link rel="stylesheet" href="../css/can.css?v=c06f5688">
|
||||
<link rel="stylesheet" href="../css/responsive.css?v=46449483">
|
||||
@@ -59,9 +59,19 @@
|
||||
<div class="card card--ble">
|
||||
<div class="card-head"><span class="ico">🔗</span> 设备连接</div>
|
||||
<div class="card-body">
|
||||
<div style="display:flex;gap:6px;margin-bottom:14px;">
|
||||
<button id="modeWifiBtn" class="btn btn-primary" style="flex:1;font-size:12px;padding:7px 10px;" data-action="set-console-mode" data-arg="wifi">WiFi OTA</button>
|
||||
<button id="modeBleBtn" class="btn btn-outline" style="flex:1;font-size:12px;padding:7px 10px;" data-action="set-console-mode" data-arg="ble">BLE 蓝牙</button>
|
||||
<div class="target-tab-row" id="modeTabRow">
|
||||
<button id="modeWifiBtn" class="btn target-tab-btn btn-primary" data-action="set-console-mode" data-arg="wifi" type="button">
|
||||
<span class="target-tab-ico" aria-hidden="true">🌐</span>
|
||||
<span class="target-tab-txt">WiFi OTA</span>
|
||||
</button>
|
||||
<button id="modeBleBtn" class="btn target-tab-btn btn-outline" data-action="set-console-mode" data-arg="ble" type="button">
|
||||
<span class="target-tab-ico" aria-hidden="true">📡</span>
|
||||
<span class="target-tab-txt">BLE 蓝牙</span>
|
||||
</button>
|
||||
<button id="modeEcatBtn" class="btn target-tab-btn btn-outline" data-action="set-console-mode" data-arg="ecat" type="button">
|
||||
<span class="target-tab-ico" aria-hidden="true">⚡</span>
|
||||
<span class="target-tab-txt">EtherCAT</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="wifiModeCards">
|
||||
<div class="form-row">
|
||||
@@ -135,6 +145,35 @@
|
||||
<div class="ble-launch-hint">点击按钮连接 / 断开蓝牙设备</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- EtherCAT 模式:这条链路没有 IP / 配对概念,"连接对象"就是本机 EtherCAT 主站。
|
||||
总线的打开/关闭由主站侧(ecat_native)负责,本页只在下面几个读数里反映现状。 -->
|
||||
<div id="ecatModeCards" style="display:none;">
|
||||
<div class="ecat-mode-note">
|
||||
主站经引导程序邮箱刷写 GD32 APP,<b>ESP32 离线不影响</b>
|
||||
</div>
|
||||
<div class="form-label">总线对面</div>
|
||||
<div class="conn-indicator" id="bootEcatConn" style="margin-bottom:10px;">
|
||||
<span class="light" id="bootEcatConnLight"></span>
|
||||
<span id="bootEcatConnTxt">未探测</span>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-block" id="bootEcatProbeBtn" data-action="boot-ecat-probe">探测对面是谁</button>
|
||||
<button class="btn btn-outline btn-block" id="bootEcatRebootBtn" data-action="boot-ecat-reboot"
|
||||
style="margin-top:8px;" title="写 APP 的 CoE 0x2502:00=1,让它复位进引导程序">让应用回引导程序</button>
|
||||
<div class="file-info-grid" style="margin-top:10px;">
|
||||
<div class="file-meta-item"><span>对面是谁</span><b id="bootEcatWho">--</b></div>
|
||||
<div class="file-meta-item"><span>从站是否放行</span><b id="bootEcatArm">--</b></div>
|
||||
</div>
|
||||
<div class="file-info-grid" style="margin-top:8px;">
|
||||
<div class="file-meta-item"><span>主站已缓存</span><b id="bootEcatSrv">0 B</b></div>
|
||||
<div class="file-meta-item"><span>从站已写 flash</span><b id="bootEcatRecv">--</b></div>
|
||||
</div>
|
||||
<div class="file-meta-item" style="margin-top:8px;">
|
||||
<span>设备名(0x1008)</span><b id="bootEcatName" style="word-break:break-all;">—</b>
|
||||
</div>
|
||||
<div class="file-meta-item">
|
||||
<span>软件版本(0x100A)</span><b id="bootEcatSw">—</b>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 连接文案只在顶栏 #pillConnTxt 显示,此处不再重复 -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,9 +182,15 @@
|
||||
<div class="card card--gd32">
|
||||
<div class="card-head"><span class="ico">🎯</span> 升级目标、固件文件与执行操作</div>
|
||||
<div class="card-body">
|
||||
<div style="display:flex;gap:6px;margin-bottom:14px;">
|
||||
<button id="targetGd32Tab" class="btn btn-outline" style="flex:1;font-size:12px;padding:7px 10px;" data-action="select-flash-target" data-arg="gd32">GD32</button>
|
||||
<button id="targetEsp32Tab" class="btn btn-primary" style="flex:1;font-size:12px;padding:7px 10px;" data-action="select-flash-target" data-arg="esp32">ESP32</button>
|
||||
<div class="target-tab-row" id="targetTabRow">
|
||||
<button id="targetGd32Tab" class="btn target-tab-btn btn-outline" data-action="select-flash-target" data-arg="gd32" type="button">
|
||||
<span class="target-tab-ico" aria-hidden="true">⚙</span>
|
||||
<span class="target-tab-txt">GD32</span>
|
||||
</button>
|
||||
<button id="targetEsp32Tab" class="btn target-tab-btn btn-primary" data-action="select-flash-target" data-arg="esp32" type="button">
|
||||
<span class="target-tab-ico" aria-hidden="true">📶</span>
|
||||
<span class="target-tab-txt">ESP32</span>
|
||||
</button>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border);margin:0 -16px 14px;padding-top:14px;padding-left:16px;padding-right:16px;">
|
||||
<div id="wifiEsp32UploadPane">
|
||||
@@ -233,18 +278,28 @@
|
||||
<button id="autoFlashBtn" class="btn btn-primary" disabled>一键刷写</button>
|
||||
<button id="abortBtn" class="btn btn-danger" disabled>中止任务</button>
|
||||
</div>
|
||||
<!-- EtherCAT 直灌面板:只在「设备连接 = EtherCAT」时显示(applySharedModeUi 控制)。
|
||||
固件复用同一个 #fileInputGd32 与同一条 processFile 转换/校验,
|
||||
所以这里不出现第二份 HEX 解析器;"能不能灌"只由 BOOT 里的 ecatArmed()
|
||||
裁决,前端不复制判据(见 js/ota/boot_ecat.js 头注)。 -->
|
||||
<div id="bootEcatPane" style="display:none;">
|
||||
<button class="btn btn-block" id="bootEcatRunBtn" data-action="boot-ecat-run" disabled>EtherCAT 刷写 APP</button>
|
||||
<button class="btn btn-danger btn-block" id="bootEcatAbortBtn"
|
||||
data-action="boot-ecat-abort" style="margin-top:8px;">放弃本轮</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Flash Progress + Stage Flow -->
|
||||
<div class="card card--esp32">
|
||||
<!-- 4. Flash Progress + Stage Flow(WiFi/BLE/EtherCAT 共用同一张进度卡) -->
|
||||
<div class="card card--esp32" id="progressCard">
|
||||
<div class="card-head"><span class="ico">⚡</span> 刷写进度与流程阶段</div>
|
||||
<div class="card-body">
|
||||
<!-- Phase step indicators -->
|
||||
<div class="progress-phase-row">
|
||||
<div class="pp-step active" id="ppEsp32"><span class="pp-ico">📦</span><span class="pp-txt">ESP32 OTA</span></div>
|
||||
<div class="pp-step" id="ppGd32Ota"><span class="pp-ico">📤</span><span class="pp-txt">GD32 OTA</span></div>
|
||||
<div class="pp-step" id="ppEcatOta"><span class="pp-ico">⚡</span><span class="pp-txt">EtherCAT</span></div>
|
||||
</div>
|
||||
<!-- Unified progress bar -->
|
||||
<div class="progress-wrap show" id="progressWrap">
|
||||
@@ -282,6 +337,16 @@
|
||||
<div class="stage-node" id="s5"><div class="stage-dot">5</div><div class="stage-label">APP 启动</div><div class="stage-detail" id="sd5"></div></div>
|
||||
<div class="stage-node" id="s6"><div class="stage-dot">6</div><div class="stage-label">OTA完成</div><div class="stage-detail" id="sd6"></div></div>
|
||||
</div>
|
||||
<!-- EtherCAT 刷写 stages -->
|
||||
<div class="stage-flow" id="ecatStages" style="display:none;">
|
||||
<div class="stage-node" id="ec1"><div class="stage-dot">1</div><div class="stage-label">读取镜像</div><div class="stage-detail" id="ecd1"></div></div>
|
||||
<div class="stage-node" id="ec2"><div class="stage-dot">2</div><div class="stage-label">推到主站</div><div class="stage-detail" id="ecd2"></div></div>
|
||||
<div class="stage-node" id="ec3"><div class="stage-dot">3</div><div class="stage-label">交接进 BOOT</div><div class="stage-detail" id="ecd3"></div></div>
|
||||
<div class="stage-node" id="ec4"><div class="stage-dot">4</div><div class="stage-label">确认引导程序</div><div class="stage-detail" id="ecd4"></div></div>
|
||||
<div class="stage-node" id="ec5"><div class="stage-dot">5</div><div class="stage-label">邮箱灌写</div><div class="stage-detail" id="ecd5"></div></div>
|
||||
<div class="stage-node" id="ec6"><div class="stage-dot">6</div><div class="stage-label">从站复位</div><div class="stage-detail" id="ecd6"></div></div>
|
||||
<div class="stage-node" id="ec7"><div class="stage-dot">7</div><div class="stage-label">刷写完成</div><div class="stage-detail" id="ecd7"></div></div>
|
||||
</div>
|
||||
<div class="stage-status-bar" id="stageStatusBar">
|
||||
<span class="ssb-dot" id="stageStatusDot"></span>
|
||||
<span class="ssb-text" id="stageStatusText">待机</span>
|
||||
@@ -344,7 +409,7 @@
|
||||
<div class="card-body">
|
||||
<div class="metric-grid">
|
||||
<div class="metric"><div class="k">ESP32 温度</div><div class="v" id="mTemp">--</div></div>
|
||||
<div class="metric"><div class="k">GD32 温度</div><div class="v" id="mGd32Temp" title="SPI 0xC4;片内温度">--</div></div>
|
||||
<div class="metric"><div class="k">GD32 温度</div><div class="v" id="mGd32Temp" title="SPI 0xC4;片内温度">--</div></div>
|
||||
<div class="metric"><div class="k">GD32 负载</div><div class="v" id="mGd32Load" title="DCU_APP CPU 负载率:中断+非延迟处理(任务派发/OTA中继)占墙钟比,不含常开轮询与空转;EtherCAT 输入 PDO 段5,1s 窗口">--</div></div>
|
||||
<div class="metric"><div class="k">输入 VCC</div><div class="v" id="mVcc" title="PA6 分压反推:VCC—47k—ADC—4.7k—GND">--</div></div>
|
||||
<div class="metric"><div class="k">VDDA</div><div class="v" id="mVdda" title="片内 VREFINT 反推的 MCU 模拟供电">--</div></div>
|
||||
@@ -500,8 +565,12 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="../js/app.js?v=20260905a"></script>
|
||||
<script type="module" src="../js/ota/console-mode.js?v=20260901d"></script>
|
||||
<!-- app 窗口租约:本页会自动拉起 EtherCAT PDO 循环(GD32 负载),
|
||||
必须持有租约,否则服务端在租约归零宽限期后会把循环又停掉。
|
||||
与 tabs.html / motor.html 并存时连接数>1,互不影响。 -->
|
||||
<script src="../js/common/app-lease.js"></script>
|
||||
<script type="module" src="../js/app.js?v=20260915a"></script>
|
||||
<script type="module" src="../js/ota/console-mode.js?v=20260914b"></script>
|
||||
<script type="module" src="../js/ble_ota.js?v=20260901d"></script>
|
||||
<!-- 内联兜底:同步 BLE 按钮 data-ble-state 与 checkbox.checked
|
||||
不依赖 ES 模块(子模块可能被浏览器缓存为旧版,不含 data-ble-state 逻辑)。
|
||||
|
||||
Reference in New Issue
Block a user