init: DCU ESP32 调试控制台前端与本地页面服务
纯前端上位机界面 + esp32_server.py 静态服务/桌面窗口外壳, 不含固件、 不直接访问硬件; 业务 API 由 ESP32_COMM 固件 80 端口跨域提供。 - tabs.html 单窗口多标签外壳, iframe 承载各功能页 - index.html OTA 中继(ESP32 WiFi / GD32 relay / BLE), js/ota + js/ble - can.html / can_monitor.html CAN 通信与接收监控, js/can - motor.html 电机 CAN FD 控制台, js/motor - continuity_test.html LAN9252 通断测试, spi_test.html SPI 测速(临时, 自包含) - 三入口: esp32_app.vbs 桌面窗口 / esp32_web.vbs 浏览器 / serve.bat 控制台调试 - 补 README(页面与目录约定、?v= 缓存版本等改动须知)、.gitignore、 .gitattributes(* -text 保持 Windows 换行原样)
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* 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 {
|
||||
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();
|
||||
|
||||
// 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);
|
||||
Reference in New Issue
Block a user