- 实现局域网设备发现面板,支持探测ESP32中继板并自动切换端点 - 添加POST /api/lan/probe接口用于设备探测,返回三态判决结果 - 实现快扫和全段扫描两种模式,支持种子地址优先探测 - 添加端点唯一写手机制,防止多处同时修改设备IP - 优化链路胶囊UI显示WiFi/EtherCAT/BLE连接状态和设备名称 - 修复EtherCAT链路OTA刷写期间PDO循环暂停和AL状态处理问题 - 调整PDO周期设置范围为0.125-4ms,支持亚毫秒显示为微秒 - 添加ESP32 WiFi使能开关控件样式和数据来源角标显示
435 lines
18 KiB
JavaScript
435 lines
18 KiB
JavaScript
/**
|
||
* 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,
|
||
updateLinkPills, setWifiPillOverride
|
||
} from './common/connection.js';
|
||
import {
|
||
scheduleStatusPoll, startPolling, pausePollingForUpload,
|
||
resumePollingAfterUpload, pollStatus, pollLogs, setStatusPollIntervalMs
|
||
} from './common/polling.js?v=20260916e';
|
||
|
||
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, clearLiveMetricsOnDisconnect
|
||
} from './ota/stages.js?v=20260916d';
|
||
import { startEcatLoadPoller } from './ota/ecat_load.js';
|
||
import { deviceLink } from './common/device-link.js';
|
||
import { isValidIpv4 } from './common/base-url.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';
|
||
import {
|
||
bootEcatProbe, bootEcatFlash, bootEcatAbort, bootEcatPick, bootEcatReboot, refreshBootEcatUi
|
||
} from './ota/boot_ecat.js?v=20261013b';
|
||
|
||
// ==================== 工具函数 ====================
|
||
function formatPercent(value) {
|
||
if (!Number.isFinite(value)) return '--';
|
||
return value.toFixed(1).replace(/\.0$/, '') + '%';
|
||
}
|
||
/** 设备 IP:优先本页 #espIp 输入框,否则 ConnectionHub。
|
||
* 只在值变化时 setIp——Owner 会把 set-ip 当成立即重探,读路径每次写会 1Hz 风暴。
|
||
* 输入框不是合法 IPv4 时**不参与拼 URL**:否则本页打到脏地址、Owner 与其余
|
||
* 标签还打在旧端点上,会出现"灯是绿的、上传是失败的"这种分裂状态。 */
|
||
function getEspIp() {
|
||
var el = document.getElementById('espIp');
|
||
var ip = el && el.value ? el.value.trim() : '';
|
||
if (ip && !isValidIpv4(ip)) return deviceLink.getIp();
|
||
if (!ip) return deviceLink.getIp();
|
||
if (ip !== deviceLink.getIp()) {
|
||
try { deviceLink.setIp(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;
|
||
/* EtherCAT 直灌那条通道共用同一块 flash 与同一份已选镜像,可用性在这里一次算完,
|
||
* 不在 file-handle / progress / console-mode 各抄一遍条件。 */
|
||
refreshBootEcatUi();
|
||
}
|
||
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() {
|
||
var elIp = document.getElementById('pillIP');
|
||
if (elIp) elIp.textContent = '--';
|
||
var elRssi = document.getElementById('pillRSSI');
|
||
if (elRssi) elRssi.textContent = '--';
|
||
// 断线约 1s(本轮轮询失败)即清实时指标,防止残留旧温度/电压误导
|
||
clearLiveMetricsOnDisconnect();
|
||
// ★ 状态机守卫:会话活跃时不重置进度/阶段,避免覆盖进行中的 OTA 进度
|
||
if (otaSessionActive()) return;
|
||
// FAILED 态:保留失败进度显示,只清连接胶囊(让用户看到失败原因)
|
||
if (otaSessionState === OtaSessionState.FAILED) return;
|
||
resetProgress();
|
||
resetStageFlow();
|
||
}
|
||
function applyRebootingRuntimeState() {
|
||
var elapsed = Math.round((Date.now() - window.rebootStartTime) / 1000);
|
||
setWifiPillOverride('重启中 ' + 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;
|
||
}
|
||
setWifiPillOverride('上传中');
|
||
var bc = document.getElementById('bConn');
|
||
if (bc) bc.textContent = '上传中';
|
||
if (bc) bc.className = 'v warn';
|
||
}
|
||
/** 设备忙:覆盖 WiFi 胶囊文案 + 系统状态徽章。 */
|
||
function applyDeviceBusyRuntimeState(pillText, statusText, badgeText) {
|
||
setWifiPillOverride(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.clearLiveMetricsOnDisconnect = clearLiveMetricsOnDisconnect;
|
||
|
||
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;
|
||
|
||
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;
|
||
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.updateLinkPills = updateLinkPills;
|
||
window.setWifiPillOverride = setWifiPillOverride;
|
||
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 'boot-ecat-probe': if (!window.bootEcatBusy) 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;
|
||
}
|
||
});
|
||
|
||
// ==================== ESP32 WiFi 使能(PC9 → ESP32 EN) ====================
|
||
// 走 EtherCAT SDO 0x2503(/ecat/api/esp32/en),与 WiFi 解耦。
|
||
// action:0=拉高(开) 1=拉低保持(关)
|
||
function sendEsp32En(action) {
|
||
return fetch('/ecat/api/esp32/en', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: action }),
|
||
signal: AbortSignal.timeout(8000)
|
||
}).then(function (r) {
|
||
return r.json().then(function (j) {
|
||
if (!r.ok || j.ok === false) throw new Error((j.log && j.log[0]) || j.err || ('HTTP ' + r.status));
|
||
return typeof j.status === 'number' ? j.status : -1;
|
||
});
|
||
});
|
||
}
|
||
|
||
function setWifiEnChecked(on) {
|
||
var sw = document.getElementById('esp32WifiSw');
|
||
if (sw) sw.checked = !!on;
|
||
}
|
||
|
||
function applyEsp32WifiSwitch(on) {
|
||
sendEsp32En(on ? 0 : 1).then(function () {
|
||
setWifiEnChecked(on);
|
||
showToast(on ? 'ESP32 WiFi 已开启' : 'ESP32 WiFi 已关断', on ? 'success' : 'warning');
|
||
}).catch(function (e) {
|
||
setWifiEnChecked(!on);
|
||
showToast('EtherCAT 下发失败: ' + e.message, 'error');
|
||
});
|
||
}
|
||
|
||
(function bindWifiEnSwitch() {
|
||
var sw = document.getElementById('esp32WifiSw');
|
||
if (!sw) return;
|
||
sw.addEventListener('change', function () {
|
||
applyEsp32WifiSwitch(sw.checked);
|
||
});
|
||
})();
|
||
|
||
// 共享固件文件选择(原内联 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();
|
||
|
||
/* DeviceLink 快照判 ESP32 离线时也清指标:
|
||
* 本页 pollStatus 可能 AbortError 静默返回,或 iframe 里旧模块未清——
|
||
* 外壳 Owner 是更权威的「设备在线」来源,离线布尔一翻就清。 */
|
||
deviceLink.subscribe(function (s) {
|
||
try {
|
||
if (!s || !s.ts || !s.links) return;
|
||
if ((Date.now() - s.ts) >= 5000) return;
|
||
/* 顶栏三链路:快照一到就刷 WiFi/ECAT 灯(BLE 另走 __bleConnState) */
|
||
if (window.updateLinkPills) window.updateLinkPills(null, null);
|
||
if (!s.links.esp32) return;
|
||
if (s.links.esp32.online) return;
|
||
clearLiveMetricsOnDisconnect();
|
||
} catch (_) {}
|
||
});
|
||
|
||
// GD32 CPU 负载率(EtherCAT 通道,独立于 /system/status 轮询;页面上无该指标则自动空转)
|
||
startEcatLoadPoller();
|
||
|
||
/* ── ECAT 胶囊:0x1008 设备名只在「断开→重连」时读一次,平时不轮询 ──
|
||
* 路径:/ecat/api/ota/ecat probe 的 name(对象字典 0x1008,如 GD32E508 DCU)
|
||
* 注意:status.slave.name 是 SOEM SII 名(DCU Project),不是这里要的。 */
|
||
function refreshEcatPillName() {
|
||
/* 只负责取从站名字(0x1008)并公布到 window.__ecatProbeName;
|
||
灯色与文案由 connection.js 的 updateLinkPills 单点出。这里以前连 className 一起写,
|
||
和快照那条路节奏不同,实测出现过"绿点 + 文字离线"的自相矛盾。 */
|
||
fetch('/ecat/api/ota/ecat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ op: 'probe' }),
|
||
cache: 'no-store',
|
||
signal: AbortSignal.timeout(5000),
|
||
}).then(function (r) { return r.json(); }).then(function (st) {
|
||
/* probe 对 APP 返回 ok:false,name 仍是 0x1008 读数 */
|
||
window.__ecatProbeName = (st && st.name) ? String(st.name).trim() : '';
|
||
updateLinkPills();
|
||
}).catch(function () {
|
||
window.__ecatProbeName = '';
|
||
updateLinkPills();
|
||
});
|
||
}
|
||
|
||
(function bindEcatNameOnReconnect() {
|
||
var lastOnline = null;
|
||
deviceLink.subscribe(function (s) {
|
||
var on = !!(s && s.links && s.links.ecat && s.links.ecat.busOpen);
|
||
if (!on) {
|
||
/* 总线没开:名字作废,灯色/文案交给 updateLinkPills 按快照出,不在这里另写一套 */
|
||
if (lastOnline !== false) {
|
||
lastOnline = false;
|
||
window.__ecatProbeName = '';
|
||
updateLinkPills();
|
||
}
|
||
return;
|
||
}
|
||
/* 首次在线 或 断开后重连 → 读一次 0x1008;保持在线则不再读 */
|
||
if (lastOnline !== true) {
|
||
lastOnline = true;
|
||
refreshEcatPillName();
|
||
}
|
||
});
|
||
})();
|
||
|
||
// IP 输入变化时清零失败计数并立即刷新状态
|
||
var ipInput = document.getElementById('espIp');
|
||
if (ipInput) {
|
||
try {
|
||
var savedIp = deviceLink.getIp();
|
||
if (savedIp && savedIp.trim()) ipInput.value = savedIp.trim();
|
||
} catch (_) {}
|
||
ipInput.addEventListener('change', function () {
|
||
var r = null;
|
||
try { r = deviceLink.setIp(ipInput.value.trim()); } catch (_) { r = { ok: false, reason: '写入异常' }; }
|
||
if (!r || !r.ok) {
|
||
/* 拒绝要说得出原因,也不把输入框停在脏值上:静默收下脏 IP 会让六个标签
|
||
打到旧端点、输入框显示新值,"看起来连着"就是假绿灯。 */
|
||
var why = (r && r.reason) || '未接受';
|
||
addLog('E', '系统', '端点未改:' + why);
|
||
showToast('端点未改:' + why, 'error', { force: true });
|
||
var eff = deviceLink.getIp();
|
||
if (eff && ipInput.value.trim() !== eff) ipInput.value = eff;
|
||
return;
|
||
}
|
||
window.consecutivePollFailures = 0;
|
||
addLog('I', '系统', 'IP 已修改 → ' + r.ip + ',重新启动轮询...');
|
||
if (!window.statusPollInFlight) {
|
||
scheduleStatusPoll(0);
|
||
}
|
||
});
|
||
/* 端点被别处改了(发现面板选中、另一页手输)要回流到本输入框,
|
||
否则六个标签共用新端点、这一页的框还显示旧值。用户正在敲时不覆盖。 */
|
||
deviceLink.subscribe(function (s) {
|
||
if (!s || !s.ip) return;
|
||
if (document.activeElement === ipInput) return;
|
||
if (ipInput.value.trim() !== s.ip) ipInput.value = s.ip;
|
||
});
|
||
}
|
||
|
||
// 初始化按钮状态与提示
|
||
setTimeout(function () {
|
||
updateUploadButtonLabel();
|
||
addLog('I', '系统', '控制台初始化完成,等待固件文件...');
|
||
}, 500); |