保留 ConnectionHub(deviceLink 导入/ECAT 自启/pmHold 幂等)与主线 WIP(断线清指标、AUTO_POLL=false、boot_ecat 等)。
367 lines
17 KiB
JavaScript
367 lines
17 KiB
JavaScript
/**
|
||
* console-mode.js - 共享模式/目标切换胶水代码(从 index.html 内联脚本抽取)
|
||
*
|
||
* 职责:
|
||
* - 覆盖 app.js 中的 switchStageFlow,支持 BLE 阶段流程
|
||
* - 提供 setConsoleMode / selectFlashTarget 等供 HTML onclick 调用
|
||
* - 维护共享固件文件状态 (sharedFirmwareFiles) 并在 WiFi/BLE 间同步
|
||
* - 页面刷新后恢复上次升级目标与固件选择
|
||
*
|
||
* 依赖:app.js (通过 window.* 或 bare 引用) 提供 switchStageFlow、
|
||
* handleFileSelect、clearFile、pickFileViaFSAPI、addLog、showToast、setActivePhase 等。
|
||
*/
|
||
import {
|
||
restorePersistedFirmwareSelection,
|
||
persistFlashTarget,
|
||
loadPersistedFlashTarget
|
||
} from './file-handle.js';
|
||
|
||
window.connectionMode = window.connectionMode || 'wifi';
|
||
/* 刷新后优先用上次目标,避免默认回到 ESP32 丢掉 GD32 选择上下文 */
|
||
window.flashTarget = loadPersistedFlashTarget() || window.flashTarget || 'esp32';
|
||
window.sharedFirmwareFiles = window.sharedFirmwareFiles || {
|
||
esp32: { file: null, handle: null },
|
||
gd32: { file: null, handle: null }
|
||
};
|
||
(function () {
|
||
var originalSwitchStageFlow = window.switchStageFlow;
|
||
window.switchStageFlow = function (target) {
|
||
if (target === 'ble') {
|
||
window.currentOtaTarget = 'ble';
|
||
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 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) {
|
||
// ★ 刷写进行中禁止切换连接模式:setConsoleMode → syncBleFileFromSelectedTarget →
|
||
// notifyFileChange → updateFileInfo → finalizeFileInfo → setProgress(0) 会覆盖
|
||
// 进行中的 WiFi/BLE 上传进度(与 selectFlashTarget 同样的守卫策略)。
|
||
var bleBusy = window.bleOta && window.bleOta.isUploading && window.bleOta.isUploading();
|
||
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 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');
|
||
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') {
|
||
setActivePhase(window.flashTarget === 'gd32' ? 'gd32Ota' : 'esp32Ota');
|
||
}
|
||
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'));
|
||
}
|
||
};
|
||
|
||
window.selectFlashTarget = function (target) {
|
||
// 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 || 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;
|
||
}
|
||
window.flashTarget = target === 'gd32' ? 'gd32' : 'esp32';
|
||
persistFlashTarget(window.flashTarget);
|
||
applySharedModeUi();
|
||
/* 与 WiFi 一致:两路文件独立;切换只换「当前目标」面板 + 绑定该路文件到会话 */
|
||
if (window.connectionMode === 'ble') {
|
||
window.switchStageFlow('ble');
|
||
if (typeof setActivePhase === 'function') {
|
||
setActivePhase(window.flashTarget === 'gd32' ? 'gd32Ota' : 'esp32Ota');
|
||
}
|
||
if (window.bleOta && typeof window.bleOta.refreshTargetUi === 'function') {
|
||
window.bleOta.refreshTargetUi();
|
||
}
|
||
/* 把当前目标对应的 esp32/gd32 固件绑到 BLE 会话(无文件则清空,避免误刷另一芯片) */
|
||
syncBleFileFromSelectedTarget();
|
||
} else {
|
||
window.switchStageFlow(window.flashTarget);
|
||
}
|
||
};
|
||
|
||
window.handleSharedFileSelect = function (event, chip) {
|
||
window.flashTarget = chip === 'gd32' ? 'gd32' : 'esp32';
|
||
persistFlashTarget(window.flashTarget);
|
||
applySharedModeUi();
|
||
/* 等 processFile(含 HEX→BIN)完成后再写入 shared 并 sync */
|
||
var done = (typeof window.handleFileSelect === 'function')
|
||
? Promise.resolve(window.handleFileSelect(event, chip))
|
||
: Promise.resolve();
|
||
done.then(function () {
|
||
var prepared = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||
if (prepared) {
|
||
window.sharedFirmwareFiles[chip] = { file: prepared, handle: null };
|
||
}
|
||
syncBleFileFromSelectedTarget();
|
||
}).catch(function (err) {
|
||
if (typeof window.addLog === 'function') {
|
||
window.addLog('W', '文件', chip.toUpperCase() + ' 选择失败: ' + (err && err.message ? err.message : err));
|
||
}
|
||
});
|
||
};
|
||
|
||
window.pickSharedFirmware = function (chip) {
|
||
window.flashTarget = chip === 'gd32' ? 'gd32' : 'esp32';
|
||
persistFlashTarget(window.flashTarget);
|
||
applySharedModeUi();
|
||
if (window.showOpenFilePicker && typeof window.pickFileViaFSAPI === 'function') {
|
||
var nativePicker = window.showOpenFilePicker;
|
||
var capturedHandle = null;
|
||
window.showOpenFilePicker = function () {
|
||
return nativePicker.apply(this, arguments).then(function (handles) {
|
||
capturedHandle = handles && handles.length ? handles[0] : null;
|
||
return handles;
|
||
});
|
||
};
|
||
Promise.resolve(window.pickFileViaFSAPI(chip)).then(function () {
|
||
/* 用 processFile 后的结果(GD32 HEX→BIN 后的 prepared),不要再塞回原始 .hex */
|
||
var prepared = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||
var handle = (chip === 'gd32' ? window.gd32FileHandle : window.esp32FileHandle) || capturedHandle;
|
||
if (prepared) {
|
||
window.sharedFirmwareFiles[chip] = { file: prepared, handle: handle || null };
|
||
} else if (capturedHandle) {
|
||
window.sharedFirmwareFiles[chip] = {
|
||
file: window.sharedFirmwareFiles[chip] && window.sharedFirmwareFiles[chip].file,
|
||
handle: capturedHandle
|
||
};
|
||
}
|
||
syncBleFileFromSelectedTarget();
|
||
}).catch(function (err) {
|
||
if (err && err.name !== 'AbortError' && typeof window.addLog === 'function') {
|
||
window.addLog('W', '文件', 'FSAPI 选择失败: ' + (err.message || err));
|
||
}
|
||
}).then(function () {
|
||
/* 无论成败都还原原生 picker,避免二次选择失效 */
|
||
window.showOpenFilePicker = nativePicker;
|
||
});
|
||
} else {
|
||
var input = document.getElementById(chip === 'gd32' ? 'fileInputGd32' : 'fileInputEsp32');
|
||
if (input) input.click();
|
||
}
|
||
};
|
||
|
||
window.clearSharedFirmware = function (chip) {
|
||
window.sharedFirmwareFiles[chip] = { file: null, handle: null };
|
||
if (typeof window.clearFile === 'function') {
|
||
window.clearFile(chip);
|
||
}
|
||
if (window.connectionMode === 'ble' && window.flashTarget === chip && window.bleOta && typeof window.bleOta.clearFile === 'function') {
|
||
window.bleOta.clearFile();
|
||
}
|
||
};
|
||
|
||
window.setBleFirmwareInputFile = function (file) {
|
||
var input = document.getElementById('firmwareFile');
|
||
if (!input || !file || typeof window.DataTransfer !== 'function') {
|
||
return;
|
||
}
|
||
try {
|
||
var dt = new DataTransfer();
|
||
dt.items.add(file);
|
||
input.files = dt.files;
|
||
} catch (err) {
|
||
// Some browsers restrict assignment to file inputs; BLE state is still synced via notifyFileChange.
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 按 flashTarget 把「该芯片一路」固件绑到 BLE 会话(复刻 WiFi:两路独立、切换不串文件)。
|
||
* 优先 window.esp32File/gd32File(含 HEX→BIN 结果),其次 sharedFirmwareFiles / input。
|
||
* 当前目标无文件时清空 BLE 会话,禁止继续显示/上传另一芯片固件。
|
||
*/
|
||
window.syncBleFileFromSelectedTarget = function () {
|
||
if (window.connectionMode !== 'ble' || !window.bleOta) {
|
||
return;
|
||
}
|
||
if (window.bleOta.isUploading && window.bleOta.isUploading()) {
|
||
return;
|
||
}
|
||
var chip = window.flashTarget === 'gd32' ? 'gd32' : 'esp32';
|
||
var file = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||
var handle = chip === 'gd32' ? window.gd32FileHandle : window.esp32FileHandle;
|
||
var stored = window.sharedFirmwareFiles && window.sharedFirmwareFiles[chip]
|
||
? window.sharedFirmwareFiles[chip]
|
||
: null;
|
||
if (!handle && stored && stored.handle) handle = stored.handle;
|
||
if (!file && stored && stored.file) file = stored.file;
|
||
|
||
function bindBle(f, h) {
|
||
setBleFirmwareInputFile(f);
|
||
if (typeof window.bleOta.notifyFileChange === 'function') {
|
||
window.bleOta.notifyFileChange(f, h || null);
|
||
}
|
||
/* 会话 SHA 与对应芯片槽对齐(不跨芯片) */
|
||
var slot = chip === 'gd32' ? window._gd32FileSha256 : window._esp32FileSha256;
|
||
if (window.bleOta.state) {
|
||
if (slot) window.bleOta.state.fileSha256 = slot;
|
||
}
|
||
}
|
||
function clearBleSession() {
|
||
if (typeof window.bleOta.clearFile === 'function') {
|
||
window.bleOta.clearFile();
|
||
} else if (typeof window.bleOta.notifyFileChange === 'function') {
|
||
window.bleOta.notifyFileChange(null, null);
|
||
}
|
||
}
|
||
|
||
if (file) {
|
||
bindBle(file, handle);
|
||
return;
|
||
}
|
||
if (handle && typeof handle.getFile === 'function') {
|
||
handle.getFile().then(function (fresh) {
|
||
if (chip === 'gd32' && fresh.name.toLowerCase().endsWith('.hex') &&
|
||
typeof window.processFile === 'function') {
|
||
return Promise.resolve(window.processFile(fresh, 'gd32', {
|
||
silent: true,
|
||
skipHandlePersist: true
|
||
})).then(function () {
|
||
if (window.gd32File) bindBle(window.gd32File, handle);
|
||
else bindBle(fresh, handle);
|
||
});
|
||
}
|
||
if (chip === 'esp32') window.esp32File = fresh;
|
||
else window.gd32File = fresh;
|
||
if (stored) stored.file = chip === 'gd32' ? window.gd32File : window.esp32File;
|
||
bindBle(chip === 'gd32' ? window.gd32File : window.esp32File, handle);
|
||
}).catch(function () {
|
||
if (stored && stored.file) bindBle(stored.file, handle);
|
||
else clearBleSession();
|
||
});
|
||
return;
|
||
}
|
||
var suffix = chip === 'gd32' ? 'Gd32' : 'Esp32';
|
||
var input = document.getElementById('fileInput' + suffix);
|
||
var inputFile = input && input.files && input.files[0] ? input.files[0] : null;
|
||
if (inputFile) {
|
||
if (window.sharedFirmwareFiles) {
|
||
window.sharedFirmwareFiles[chip] = { file: inputFile, handle: null };
|
||
}
|
||
bindBle(inputFile, null);
|
||
return;
|
||
}
|
||
clearBleSession();
|
||
};
|
||
|
||
window.applySharedModeUi = function () {
|
||
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 = '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 = {
|
||
/* EtherCAT 复用 GD32 上传区(同一 fileInputGd32 / processFile),选文件 UI 与 WiFi/BLE 一致 */
|
||
wifiEsp32UploadPane: !isEcat && !isGd32,
|
||
wifiGd32UploadPane: isGd32 || isEcat,
|
||
bleUploadPane: false,
|
||
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 (isEcat) {
|
||
hint.innerHTML = 'EtherCAT 模式:选择 GD32 固件 → 探测对面是谁 → 刷写 APP。';
|
||
} else if (isBle) {
|
||
hint.innerHTML = isGd32
|
||
? 'BLE 模式 · 目标 GD32:连接设备 → 选择 GD32 固件 → 一键刷写。'
|
||
: 'BLE 模式 · 目标 ESP32:连接设备 → 选择 ESP32 固件 → 一键刷写。';
|
||
} else if (isGd32) {
|
||
hint.innerHTML = 'WiFi 模式:上传 GD32 固件后自动执行 SPI OTA。';
|
||
} else {
|
||
hint.innerHTML = 'WiFi 模式:上传 ESP32 固件后自动执行 OTA 并重启。';
|
||
}
|
||
}
|
||
if (typeof window.refreshBootEcatUi === 'function') window.refreshBootEcatUi();
|
||
};
|
||
|
||
applySharedModeUi();
|
||
/* 刷新后恢复上次固件与目标(FSAPI handle + localStorage 目标) */
|
||
restorePersistedFirmwareSelection().then(function () {
|
||
if (typeof window.switchStageFlow === 'function' && window.connectionMode !== 'ble') {
|
||
window.switchStageFlow(window.flashTarget || 'esp32');
|
||
}
|
||
}).catch(function () {});
|
||
})();
|
||
/* HTML onclick 用的 API 已在 IIFE 内挂到 window.*,无需再二次赋值 */
|