Files
DCU_DesktopController/js/app.js
T
cbb 9cbde690c6 feat(ota): 首页运行指标卡新增 GD32 CPU 负载率(EtherCAT 通道)
DCU_APP 固件 >=1.5.61 把"中断+非延迟处理段"占用率(1.5.63 收窄定义)
写进 EtherCAT 输入 PDO 段 5 诊断区(1s 滚动窗口, 0.1% 分辨率), native
经 /api/app/load 提供纯缓存读; 前端 js/ota/ecat_load.js 经同源代理
/ecat/api/app/* 轮询并渲染到 #mGd32Load。

- 与 /system/status 轮询完全独立: ESP32 离线互不影响, 失败不计入
  ESP32 连接失败计数
- 失败静默退避 1s→5s→30s, 不用 EtherCAT 的用户不按 1Hz 触发 esp32_server
  的自动拉起逻辑
- 无效显示 --: valid:false(总线未开/循环未跑/固件过旧)或 age_ms>3s
  (PDO 循环已停, 值不再滚动); >=85% 琥珀色提示余量吃紧
- 页面上无 #mGd32Load 时自动空转(其他页共用 app.js 不受影响)
2026-09-05 19:58:36 +08:00

297 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* app.js - ESP32 OTA 中继控制台入口(WiFi OTA 通道)
*
* 薄入口:import 所有 ota/* + common/* 模块,将 HTML onclick 引用的函数
* 显式挂载到 window 对象,保留原有内联事件处理器兼容。
*
* 模块通过 window.* 共享状态,模块间依赖通过 import 解耦。
* 此入口只负责挂载 + 顶层副作用(拖拽区绑定、SPI 输入监听、启动轮询)。
*/
import { formatBytes, formatTime, formatUptime, escapeHtml } from './common/utils.js';
import { showToast } from './common/toast.js';
import { addLog, clearLogs, setLogFilter, toggleAutoScroll } from './common/log.js';
import {
updateConnectionUi, updateHealthUi, updateHeapAndTaskStatusUi,
testConnection, refreshSystemStatusNow, diagnoseNetwork, systemStatusUrl
} from './common/connection.js';
import {
scheduleStatusPoll, startPolling, pausePollingForUpload,
resumePollingAfterUpload, pollStatus, pollLogs, setStatusPollIntervalMs
} from './common/polling.js';
import {
OtaSessionState, otaSessionState, otaSessionTarget, otaFailReason,
otaSessionActive, otaSessionTerminated, otaSessionTransition,
otaSessionReset, clearOtaSessionResiduals,
stopUploadWaitHeartbeat, startUploadWaitHeartbeat
} from './ota/session.js';
import {
toggleCollapse, switchStageFlow, updateEsp32StagesFromPhase,
updateEsp32StagesError, cleanupAfterOtaFailure, setActivePhase,
markPhaseDone, setProgress, resetProgress, esp32PhaseLabels
} from './ota/progress.js';
import {
updateStagesFromRelayState, updateSystemStatusUi
} from './ota/stages.js';
import { startEcatLoadPoller } from './ota/ecat_load.js';
import {
handleFileSelect, pickFileViaFSAPI, setupDropZone,
parseIntelHex, isIntelHexText, convertHexFileToBin,
processFile, clearFile, resumePersistedFirmwareWithGesture
} from './ota/file-handle.js';
import { startUpload, verifyEsp32Firmware, restartEsp32 } from './ota/upload-esp32.js';
import {
triggerGd32Relay, markGd32OtaUiComplete, stopRelayProgressWatch,
startRelayProgressWatch, emitRelayProgress, ensureRelayProgressComplete
} from './ota/upload-gd32.js';
// ==================== 工具函数 ====================
function formatPercent(value) {
if (!Number.isFinite(value)) return '--';
return value.toFixed(1).replace(/\.0$/, '') + '%';
}
function getEspIp() {
var el = document.getElementById('espIp');
var ip = el && el.value ? el.value.trim() : '';
if (!ip) {
try { ip = (localStorage.getItem('dcu_esp_ip') || '').trim(); } catch (_) {}
}
if (!ip) ip = '192.168.31.127';
try { localStorage.setItem('dcu_esp_ip', ip); } catch (_) {}
return ip;
}
function getBaseUrl() {
return 'http://' + getEspIp();
}
function updateUploadButtonLabel() {
var esp32Btn = document.getElementById('uploadEsp32Btn');
var gd32Btn = document.getElementById('uploadGd32Btn');
/* 仅 XHR 上传期禁用(isUploading)。校验/中继/重启期允许按钮恢复可点,
* startUpload 用 otaSessionActive() 拒绝并发会话。 */
var xhrBusy = !!window.isUploading;
if (esp32Btn) esp32Btn.disabled = !window.esp32File || xhrBusy;
if (gd32Btn) gd32Btn.disabled = !window.gd32File || xhrBusy;
}
function resetStageFlow() {
['s1', 's2', 's3', 's4', 's5', 's6'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.className = 'stage-node';
});
['sd1', 'sd2', 'sd3', 'sd4', 'sd5', 'sd6'].forEach(function (id) {
var el = document.getElementById(id);
if (el) el.textContent = '';
});
var statusBar = document.getElementById('stageStatusBar');
if (statusBar) statusBar.className = 'stage-status-bar';
var statusText = document.getElementById('stageStatusText');
if (statusText) statusText.textContent = '待机';
}
function applyDisconnectedRuntimeState() {
document.getElementById('pillIP').textContent = '--';
document.getElementById('pillRSSI').textContent = '--';
// ★ 状态机守卫:会话活跃时不重置进度/阶段,避免覆盖进行中的 OTA 进度
if (otaSessionActive()) return;
// FAILED 态:保留失败进度显示,只清连接胶囊(让用户看到失败原因)
if (otaSessionState === OtaSessionState.FAILED) return;
resetProgress();
resetStageFlow();
}
function applyRebootingRuntimeState() {
var pc = document.getElementById('pillConn');
var pct = document.getElementById('pillConnTxt');
if (pc) pc.className = 'pill processing';
if (pct) {
var elapsed = Math.round((Date.now() - window.rebootStartTime) / 1000);
pct.textContent = '重启中 ' + elapsed + 's';
}
var bc = document.getElementById('bConn');
if (bc) bc.textContent = '重启中';
if (bc) bc.className = 'v warn';
}
function applyUploadingRuntimeState() {
// ★ 状态机守卫:会话已失败/完成时,绝不重新应用"上传中"运行时态,
// 防止网络抖动后的轮询错误分支把 UI 拉回"上传中"。
if (otaSessionState === OtaSessionState.FAILED ||
otaSessionState === OtaSessionState.DONE) {
return;
}
var pc = document.getElementById('pillConn');
var pct = document.getElementById('pillConnTxt');
if (pc) pc.className = 'pill processing';
if (pct) pct.textContent = '上传中';
var bc = document.getElementById('bConn');
if (bc) bc.textContent = '上传中';
if (bc) bc.className = 'v warn';
}
/** 设备忙:只更新顶栏连接胶囊 + 系统状态徽章,不写下方重复文案。 */
function applyDeviceBusyRuntimeState(pillText, statusText, badgeText) {
var pc = document.getElementById('pillConn');
var pct = document.getElementById('pillConnTxt');
if (pc) pc.className = 'pill processing';
if (pct) pct.textContent = pillText || '设备忙';
var bc = document.getElementById('bConn');
if (bc) bc.textContent = badgeText || '设备忙';
if (bc) bc.className = 'v warn';
}
// ==================== 挂载 ota/* 函数到 window(供其他模块通过 window.* 调用) ====================
window.OtaSessionState = OtaSessionState;
window.otaSessionActive = otaSessionActive;
window.otaSessionTerminated = otaSessionTerminated;
window.otaSessionTransition = otaSessionTransition;
window.otaSessionReset = otaSessionReset;
window.clearOtaSessionResiduals = clearOtaSessionResiduals;
window.stopUploadWaitHeartbeat = stopUploadWaitHeartbeat;
window.startUploadWaitHeartbeat = startUploadWaitHeartbeat;
window.toggleCollapse = toggleCollapse;
window.switchStageFlow = switchStageFlow;
window.updateEsp32StagesFromPhase = updateEsp32StagesFromPhase;
window.updateEsp32StagesError = updateEsp32StagesError;
window.cleanupAfterOtaFailure = cleanupAfterOtaFailure;
window.setActivePhase = setActivePhase;
window.markPhaseDone = markPhaseDone;
window.setProgress = setProgress;
window.resetProgress = resetProgress;
window.updateStagesFromRelayState = updateStagesFromRelayState;
window.updateSystemStatusUi = updateSystemStatusUi;
window.handleFileSelect = handleFileSelect;
window.pickFileViaFSAPI = pickFileViaFSAPI;
window.resumePersistedFirmwareWithGesture = resumePersistedFirmwareWithGesture;
window.parseIntelHex = parseIntelHex;
window.isIntelHexText = isIntelHexText;
window.convertHexFileToBin = convertHexFileToBin;
window.processFile = processFile;
window.clearFile = clearFile;
window.startUpload = startUpload;
window.verifyEsp32Firmware = verifyEsp32Firmware;
window.restartEsp32 = restartEsp32;
window.triggerGd32Relay = triggerGd32Relay;
window.markGd32OtaUiComplete = markGd32OtaUiComplete;
window.stopRelayProgressWatch = stopRelayProgressWatch;
window.startRelayProgressWatch = startRelayProgressWatch;
window.emitRelayProgress = emitRelayProgress;
window.ensureRelayProgressComplete = ensureRelayProgressComplete;
// ==================== 挂载 common/* 函数到 window(供 polling.js 等通过 window.* 调用) ====================
window.formatBytes = formatBytes;
window.formatTime = formatTime;
window.formatUptime = formatUptime;
window.escapeHtml = escapeHtml;
window.formatPercent = formatPercent;
window.showToast = showToast;
window.addLog = addLog;
window.clearLogs = clearLogs;
window.setLogFilter = setLogFilter;
window.toggleAutoScroll = toggleAutoScroll;
window.updateConnectionUi = updateConnectionUi;
window.updateHealthUi = updateHealthUi;
window.updateHeapAndTaskStatusUi = updateHeapAndTaskStatusUi;
window.testConnection = testConnection;
window.refreshSystemStatusNow = refreshSystemStatusNow;
window.systemStatusUrl = systemStatusUrl;
window.scheduleStatusPoll = scheduleStatusPoll;
window.setStatusPollIntervalMs = setStatusPollIntervalMs;
window.startPolling = startPolling;
window.pausePollingForUpload = pausePollingForUpload;
window.resumePollingAfterUpload = resumePollingAfterUpload;
window.pollStatus = pollStatus;
window.pollLogs = pollLogs;
window.diagnoseNetwork = diagnoseNetwork;
// ==================== 挂载本入口的辅助函数到 window ====================
window.getEspIp = getEspIp;
window.getBaseUrl = getBaseUrl;
window.updateUploadButtonLabel = updateUploadButtonLabel;
window.resetStageFlow = resetStageFlow;
window.applyDisconnectedRuntimeState = applyDisconnectedRuntimeState;
window.applyRebootingRuntimeState = applyRebootingRuntimeState;
window.applyUploadingRuntimeState = applyUploadingRuntimeState;
window.applyDeviceBusyRuntimeState = applyDeviceBusyRuntimeState;
// ==================== data-action 事件委托(替代 HTML 内联 onclick) ====================
// 处理函数分散在 app.js / ota/console-mode.js / ble_ota.js 三个入口,均挂载在
// window 上;委托在点击时才解引用,天然规避模块加载顺序问题。
document.addEventListener('click', function (ev) {
var target = ev.target.closest('[data-action]');
if (!target) return;
var arg = target.getAttribute('data-arg');
switch (target.getAttribute('data-action')) {
case 'set-console-mode': if (window.setConsoleMode) window.setConsoleMode(arg); break;
case 'test-connection': testConnection(); break;
case 'select-flash-target': if (window.selectFlashTarget) window.selectFlashTarget(arg); break;
case 'pick-shared-firmware': if (window.pickSharedFirmware) window.pickSharedFirmware(arg); break;
case 'clear-shared-firmware': if (window.clearSharedFirmware) window.clearSharedFirmware(arg); break;
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 'restart-esp32': restartEsp32(); break;
case 'refresh-system-status': refreshSystemStatusNow(ev); break;
}
});
// 共享固件文件选择(原内联 onchange="handleSharedFileSelect(event, chip)")
['esp32', 'gd32'].forEach(function (chip) {
var input = document.getElementById(chip === 'esp32' ? 'fileInputEsp32' : 'fileInputGd32');
if (input) {
input.addEventListener('change', function (ev) {
if (window.handleSharedFileSelect) window.handleSharedFileSelect(ev, chip);
});
}
});
// BLE 上传区拖拽(原内联 ondragover/ondragleave/ondrop)
var bleZone = document.getElementById('uploadZone');
if (bleZone) {
bleZone.addEventListener('dragover', function (ev) {
ev.preventDefault();
bleZone.classList.add('dragover');
});
bleZone.addEventListener('dragleave', function () {
bleZone.classList.remove('dragover');
});
bleZone.addEventListener('drop', function (ev) {
ev.preventDefault();
bleZone.classList.remove('dragover');
var f = ev.dataTransfer.files[0];
if (f && window.bleOta) window.bleOta.notifyFileChange(f, null);
});
}
// ==================== 顶层副作用(原 app.js 末尾的初始化) ====================
setupDropZone('uploadZoneEsp32', 'esp32');
setupDropZone('uploadZoneGd32', 'gd32');
// 启动轮询(必须在所有模块加载完成、window.OtaSessionState 已挂载后调用,
// 否则 polling.js 的 scheduleStatusPoll 会因 window.OtaSessionState 未定义而抛 TypeError)
startPolling();
// GD32 CPU 负载率(EtherCAT 通道,独立于 /system/status 轮询;页面上无该指标则自动空转)
startEcatLoadPoller();
// IP 输入变化时清零失败计数并立即刷新状态
var ipInput = document.getElementById('espIp');
if (ipInput) {
try {
var savedIp = localStorage.getItem('dcu_esp_ip');
if (savedIp && savedIp.trim()) ipInput.value = savedIp.trim();
} catch (_) {}
ipInput.addEventListener('change', function () {
try { localStorage.setItem('dcu_esp_ip', ipInput.value.trim() || '192.168.31.127'); } catch (_) {}
window.consecutivePollFailures = 0;
addLog('I', '系统', 'IP 已修改,重新启动轮询...');
if (!window.statusPollInFlight) {
scheduleStatusPoll(0);
}
});
}
// 初始化按钮状态与提示
setTimeout(function () {
updateUploadButtonLabel();
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
}, 500);