Files
ESP32_COMM/tools/index.html
T
小龙虾 7feef70750 feat(ota): 统一BLE/WiFi OTA协调器接口并优化并发控制
- 添加 beginEsp32OtaWithRetry 统一重试逻辑,防止锁残留导致的并发问题
- 实现 finalizeForReboot 和 restartEsp32 统一封装收尾重启流程
- 修复BLE/WiFi双通道并发OTA的安全隐患,增强系统状态检查

refactor(ota): 优化SHA计算内存管理

- 使用静态缓冲区替代动态分配,减少内存碎片
- 重构OTA协调器状态机,增强错误处理机制

perf(monitor): 增强堆内存监控功能

- 添加最大空闲块大小监控,提供更详细的内存状态信息
- 更新健康监测算法,提高内存异常检测准确性
2026-06-19 03:02:33 +08:00

627 lines
31 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 OTA 中继控制台</title>
<link rel="stylesheet" href="style.css?v=20260617a">
</head>
<body>
<div class="container">
<!-- ====== HEADER: compact status bar ====== -->
<header class="header-bar">
<div class="header-brand">
<span class="dot-mark"></span>
<strong>ESP32 OTA 中继控制台</strong>
</div>
<div class="header-status">
<span class="pill offline" id="pillConn"><span class="indicator"></span><span id="pillConnTxt">未连接</span></span>
<span class="pill" id="pillVer">v--</span>
<span class="pill" id="pillIP">--</span>
<span class="pill" id="pillRSSI">--</span>
<span class="pill" id="pillUptime">运行 --</span>
</div>
<div class="header-sub">
<span class="pill">设备名: <b class="pill-val" id="deviceName">-</b></span>
<span class="pill">设备ID: <b class="pill-val" id="deviceId">-</b></span>
</div>
</header>
<!-- ====== MAIN LAYOUT ====== -->
<div class="main-layout">
<!-- ====== LEFT: workflow ====== -->
<div>
<!-- 1. Connection -->
<div class="card">
<div class="card-head"><span class="ico">🔗</span> 设备连接
<span id="bleHeadActions" style="display:none;margin-left:auto;">
<button id="connectBtn" class="btn btn-sm btn-primary">连接设备</button>
<button id="disconnectBtn" class="btn btn-sm btn-outline" disabled>断开连接</button>
</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;" onclick="setConsoleMode('wifi')">WiFi OTA</button>
<button id="modeBleBtn" class="btn btn-outline" style="flex:1;font-size:12px;padding:7px 10px;" onclick="setConsoleMode('ble')">BLE 蓝牙</button>
</div>
<div id="wifiModeCards">
<div class="form-row">
<label class="form-label" for="espIp">ESP32 IP 地址</label>
<div class="input-group">
<input class="input" type="text" id="espIp" value="192.168.31.127" placeholder="输入 IP 地址">
<button class="btn btn-outline" onclick="testConnection()">测试</button>
</div>
</div>
</div>
<div id="bleModeCards" style="display:none;">
<div class="row-2" id="bleParamRow" style="margin-top:0;">
<div class="form-row" style="margin:0;">
<label class="form-label" for="payloadSize">每包 Payload</label>
<input id="payloadSize" class="input" type="number" min="32" max="498" value="480">
</div>
<div class="form-row" style="margin:0;">
<label class="form-label" for="ctrlHex">触发命令(HEX)</label>
<input id="ctrlHex" class="input" type="text" value="F7" maxlength="8">
</div>
</div>
</div>
<div class="conn-indicator err" id="connStatus" style="margin-top:12px;">
<span class="light"></span>
<span id="connStatusTxt">等待连接</span>
</div>
</div>
</div>
<!-- ====== Shared Target + Firmware + Actions ====== -->
<div class="card">
<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;" onclick="selectFlashTarget('gd32')">GD32</button>
<button id="targetEsp32Tab" class="btn btn-primary" style="flex:1;font-size:12px;padding:7px 10px;" onclick="selectFlashTarget('esp32')">ESP32</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">
<div class="upload-zone upload-zone-sm" id="uploadZoneEsp32" onclick="pickSharedFirmware('esp32')">
<div class="upload-ico">📤</div>
<h4>点击或拖拽 ESP32 固件</h4>
</div>
<input type="file" id="fileInputEsp32" accept=".bin" onchange="handleSharedFileSelect(event, 'esp32')" style="display:none;">
<div class="file-info" id="fileInfoEsp32">
<div class="file-info-row">
<span style="font-weight:600;">📄 <span id="fileNameEsp32">-</span></span>
<button class="btn btn-ghost btn-sm" onclick="clearSharedFirmware('esp32')">清除</button>
</div>
<div class="file-info-grid">
<div class="file-meta-item"><span>类型</span><b id="fileTypeEsp32">-</b></div>
<div class="file-meta-item"><span>大小</span><b id="fileSizeEsp32">-</b></div>
</div>
<div class="file-meta-item" style="margin-top:6px;">
<span>SHA-256</span>
<b id="fileSha256Esp32" style="font-size:10px;word-break:break-all;font-family:monospace;">-</b>
</div>
</div>
</div>
<div id="wifiGd32UploadPane" style="display:none;">
<div class="upload-zone upload-zone-sm" id="uploadZoneGd32" onclick="pickSharedFirmware('gd32')">
<div class="upload-ico">📤</div>
<h4>点击或拖拽 GD32 固件</h4>
<p>支持 .bin 和 .hex(自动转换)</p>
</div>
<input type="file" id="fileInputGd32" accept=".hex,.bin" onchange="handleSharedFileSelect(event, 'gd32')" style="display:none;">
<div class="file-info" id="fileInfoGd32">
<div class="file-info-row">
<span style="font-weight:600;">📄 <span id="fileNameGd32">-</span></span>
<button class="btn btn-ghost btn-sm" onclick="clearSharedFirmware('gd32')">清除</button>
</div>
<div class="file-info-grid">
<div class="file-meta-item"><span>类型</span><b id="fileTypeGd32">-</b></div>
<div class="file-meta-item"><span>大小</span><b id="fileSizeGd32">-</b></div>
</div>
</div>
</div>
<div id="bleUploadPane" style="display:none;">
<div class="upload-zone upload-zone-sm" id="uploadZone" style="margin-bottom:12px;"
onclick="document.getElementById('firmwareFile').click()"
ondragover="event.preventDefault();this.classList.add('dragover')"
ondragleave="this.classList.remove('dragover')"
ondrop="event.preventDefault();this.classList.remove('dragover');var f=event.dataTransfer.files[0];if(f&&window.bleOta)window.bleOta.notifyFileChange(f,null);">
<div class="upload-ico">📤</div>
<h4>点击或拖拽文件到此处</h4>
<p>支持 .bin 和 .hex 格式</p>
</div>
<input id="firmwareFile" type="file" accept=".bin,.hex,application/octet-stream" style="display:none;">
<div class="file-info" id="fileInfo">
<div class="file-info-row">
<span style="font-weight:600;">📄 <span id="fileName">-</span></span>
<button id="clearFileBtn" class="btn btn-outline btn-sm" onclick="if(window.bleOta)window.bleOta.clearFile()">清除</button>
</div>
<div class="file-info-grid">
<div class="file-meta-item"><span>类型</span><b id="fileType">-</b></div>
<div class="file-meta-item"><span>大小</span><b id="fileSize">0 B</b></div>
<div class="file-meta-item"><span>总包数</span><b id="packetCount">0</b></div>
<div class="file-meta-item"><span>已写包</span><b id="sentCount">0</b></div>
</div>
<div class="file-meta-item" style="margin-top:8px;">
<span>SHA-256</span>
<b id="fileSha256" style="font-size:10px;word-break:break-all;font-family:var(--font-mono);">-</b>
</div>
</div>
<div class="footer-note">建议使用 Chrome / Edge。若首包失败,可将 Payload 从 480 降为 240 再试。</div>
</div>
<div style="height:12px"></div>
<div id="wifiEsp32ActionPane">
<button class="btn btn-primary btn-block" id="uploadEsp32Btn" onclick="startUpload('esp32')" disabled>
上传并升级 ESP32
</button>
</div>
<div id="wifiGd32ActionPane" style="display:none;">
<button class="btn btn-success btn-block" id="uploadGd32Btn" onclick="startUpload('gd32')" disabled>
GD32 OTA
</button>
</div>
<div class="row-2" id="bleActionPane" style="display:none;">
<button id="autoFlashBtn" class="btn btn-primary" disabled>一键刷写</button>
<button id="abortBtn" class="btn btn-danger" disabled>中止任务</button>
</div>
</div>
</div>
</div>
<!-- 4. Flash Progress + Stage Flow -->
<div class="card">
<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>
<!-- Unified progress bar -->
<div class="progress-wrap show" id="progressWrap">
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
<div class="progress-text">
<span id="progressTxt">等待操作...</span>
<span id="progressPct">0%</span>
</div>
</div>
<div style="height:14px"></div>
<!-- ESP32 OTA stages -->
<div class="stage-flow" id="esp32Stages" style="display:none;">
<div class="stage-node" id="es1"><div class="stage-dot">1</div><div class="stage-label">固件上传</div><div class="stage-detail" id="esd1"></div></div>
<div class="stage-node" id="es2"><div class="stage-dot">2</div><div class="stage-label">OTA 写入</div><div class="stage-detail" id="esd2"></div></div>
<div class="stage-node" id="es3"><div class="stage-dot">3</div><div class="stage-label">SHA-256 校验</div><div class="stage-detail" id="esd3"></div></div>
<div class="stage-node" id="es4"><div class="stage-dot">4</div><div class="stage-label">应用固件</div><div class="stage-detail" id="esd4"></div></div>
<div class="stage-node" id="es5"><div class="stage-dot">5</div><div class="stage-label">重启中</div><div class="stage-detail" id="esd5"></div></div>
<div class="stage-node" id="es6"><div class="stage-dot">6</div><div class="stage-label">启动完成</div><div class="stage-detail" id="esd6"></div></div>
</div>
<!-- BLE OTA stages -->
<div class="stage-flow" id="bleStages" style="display:none;">
<div class="stage-node" id="bs1"><div class="stage-dot">1</div><div class="stage-label">BLE 连接</div><div class="stage-detail" id="bsd1"></div></div>
<div class="stage-node" id="bs2"><div class="stage-dot">2</div><div class="stage-label">GATT 准备</div><div class="stage-detail" id="bsd2"></div></div>
<div class="stage-node" id="bs3"><div class="stage-dot">3</div><div class="stage-label">文件准备</div><div class="stage-detail" id="bsd3"></div></div>
<div class="stage-node" id="bs4"><div class="stage-dot">4</div><div class="stage-label">数据写入</div><div class="stage-detail" id="bsd4"></div></div>
<div class="stage-node" id="bs5"><div class="stage-dot">5</div><div class="stage-label" id="bsl5">校验比对</div><div class="stage-detail" id="bsd5"></div></div>
<div class="stage-node" id="bs6"><div class="stage-dot">6</div><div class="stage-label" id="bsl6">完成触发</div><div class="stage-detail" id="bsd6"></div></div>
</div>
<!-- GD32 OTA stages -->
<div class="stage-flow" id="gd32Stages">
<div class="stage-node" id="s1"><div class="stage-dot">1</div><div class="stage-label">认证握手</div><div class="stage-detail" id="sd1"></div></div>
<div class="stage-node" id="s2"><div class="stage-dot">2</div><div class="stage-label">传输协商</div><div class="stage-detail" id="sd2"></div></div>
<div class="stage-node" id="s3"><div class="stage-dot">3</div><div class="stage-label">数据转发</div><div class="stage-detail" id="sd3"></div></div>
<div class="stage-node" id="s4"><div class="stage-dot">4</div><div class="stage-label">传输完成</div><div class="stage-detail" id="sd4"></div></div>
<div class="stage-node" id="s5"><div class="stage-dot">5</div><div class="stage-label">OTA 刷写</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">校验验证</div><div class="stage-detail" id="sd6"></div></div>
</div>
<div class="stage-status-bar" id="stageStatusBar">
<span class="ssb-dot" id="stageStatusDot"></span>
<span class="ssb-text" id="stageStatusText">待机</span>
</div>
<div class="stage-verify-info" id="stageVerifyInfo" style="display:none;">
<div class="svi-row"><span class="svi-key">前端 SHA-256</span><span class="svi-val" id="sviFrontSha">-</span></div>
<div class="svi-row"><span class="svi-key">设备 SHA-256</span><span class="svi-val" id="sviDeviceSha">-</span></div>
<div class="svi-row"><span class="svi-key">校验结果</span><span class="svi-val" id="sviResult">-</span></div>
</div>
<div style="margin-top:12px;">
<button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button>
</div>
</div>
</div>
</div>
<!-- ====== RIGHT: status + tools ====== -->
<div>
<!-- Primary status -->
<div class="card">
<div class="card-head"><span class="ico">📡</span> 设备状态</div>
<div class="card-body">
<div class="badge-grid">
<div class="badge-stat">
<div class="v err" id="bConn">离线</div>
<div class="k">连接状态</div>
</div>
<div class="badge-stat">
<div class="v warn" id="bOTAProgress">0%</div>
<div class="k">OTA 进度</div>
</div>
<div class="badge-stat">
<div class="v err" id="connBadge">offline</div>
<div class="k">BLE 连接</div>
</div>
<div class="badge-stat">
<div class="v" id="gattState" style="font-size:16px;">未就绪</div>
<div class="k">GATT 状态</div>
</div>
</div>
</div>
</div>
<!-- Secondary metrics -->
<div class="card">
<div class="card-head"><span class="ico">📊</span> 运行指标</div>
<div class="card-body">
<div class="metric-grid">
<div class="metric"><div class="k">温度</div><div class="v" id="mTemp">--</div></div>
<div class="metric"><div class="k">空闲堆</div><div class="v" id="mHeap" style="font-size:16px;">--</div></div>
<div class="metric"><div class="k">最低空闲堆</div><div class="v" id="mMinHeap" style="font-size:16px;">--</div></div>
<div class="metric"><div class="k">GD32 APP</div><div class="v" id="mGd32App" style="font-size:14px;">--</div></div>
<div class="metric"><div class="k">GD32 BOOT</div><div class="v" id="mGd32Boot" style="font-size:14px;">--</div></div>
<div class="metric"><div class="k">OTA 缓存</div><div class="v" id="mOtaCache" style="font-size:13px;">--</div></div>
</div>
</div>
</div>
<!-- CAN panel -->
<div class="card collapsed">
<div class="card-head" onclick="toggleCollapse(this)">
<span class="ico">📨</span> CAN 帧发送
<span class="collapse-arrow">▼</span>
</div>
<div class="card-body">
<div class="can-grid">
<div class="form-row" style="margin:0"><label class="form-label">通道</label>
<select class="input" id="canChannel"><option value="0">CH0</option><option value="1">CH1</option><option value="2">CH2</option></select>
</div>
<div class="form-row" style="margin:0"><label class="form-label">扩展帧 ID</label>
<input class="input" type="text" id="canId" value="0x18DA00F1">
</div>
<div class="form-row" style="margin:0"><label class="form-label">DLC</label>
<input class="input" type="number" id="canDlc" value="8" min="1" max="64">
</div>
<div class="form-row" style="margin:0"><label class="form-label">数据 (HEX)</label>
<input class="input" type="text" id="canData" value="0102030405060708" placeholder="01020304...">
</div>
</div>
<div class="helper-note" id="canHint">输入十六进制数据后会自动检查字节数是否与 DLC 一致。</div>
<div class="btn-row">
<button class="btn btn-primary" onclick="sendCanFrame()">发送</button>
<button class="btn btn-outline" onclick="clearCanFields()">清空</button>
</div>
<div id="canResp" style="display:none;margin-top:12px;padding:12px;background:var(--bg-elevated);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:12px;">
<div style="color:var(--text-label);margin-bottom:4px;">响应</div>
<div id="canRespText" style="color:var(--accent);"></div>
</div>
</div>
</div>
<!-- SPI panel -->
<div class="card collapsed">
<div class="card-head" onclick="toggleCollapse(this)">
<span class="ico">⚡</span> SPI 发送
<span class="collapse-arrow">▼</span>
</div>
<div class="card-body">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
<input class="input" type="text" id="spiType" value="01" maxlength="2" placeholder="type" title="帧类型 (hex)" style="width:52px;flex:none;">
<input class="input" type="text" id="spiData" value="" placeholder="payload hex..." style="flex:1;min-width:100px;">
<input class="input" type="number" id="spiTimeout" value="1000" min="100" max="10000" title="超时 ms" style="width:68px;flex:none;">
<span style="font-size:11px;color:var(--text-dim);">ms</span>
<button class="btn btn-primary" onclick="sendSpiFrame()" style="padding:4px 14px;">发送</button>
<button class="btn btn-outline" onclick="clearSpiFields()" style="padding:4px 10px;">清空</button>
</div>
<div class="helper-note" id="spiHint">type(hex) + payload → 自动构建 SOF+LEN+CRC 帧</div>
<div id="spiResp" style="display:none;margin-top:8px;padding:8px 10px;background:var(--bg-elevated);border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:12px;">
<span id="spiRespText" style="color:var(--accent);"></span>
</div>
</div>
</div>
<!-- Log panel -->
<div class="card log-panel">
<div class="card-head" style="justify-content:space-between;">
<span><span class="ico">🖥️</span> 调试日志</span>
<span style="font-size:11px;font-weight:400;color:var(--text-label);text-transform:none;letter-spacing:0;">UDP :6666</span>
</div>
<div class="card-body">
<div class="log-toolbar">
<div class="log-filters">
<button class="log-filter active" onclick="setLogFilter('all', this)">全部</button>
<button class="log-filter" onclick="setLogFilter('E', this)">错误</button>
<button class="log-filter" onclick="setLogFilter('W', this)">警告</button>
<button class="log-filter" onclick="setLogFilter('I', this)">信息</button>
</div>
<button class="btn btn-ghost btn-sm" onclick="toggleAutoScroll()" id="autoScrollBtn" title="切换自动滚动">📌 自动滚动</button>
</div>
<div class="log-scroll" id="logScroll">
<div class="log-line"><span class="lt">[--:--:--]</span><span class="lv I">I</span><span class="lg">[系统]</span><span class="lm">等待上位机连接...</span></div>
</div>
<div class="log-foot">
<span>共 <span id="logCount">0</span> 条</span>
<button class="btn btn-ghost btn-sm" onclick="clearLogs()">清除日志</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast">
<span class="toast-ico" id="toastIco">✓</span>
<span id="toastMsg">操作成功</span>
</div>
<script src="app.js?v=20260619c"></script>
<script>
window.connectionMode = window.connectionMode || 'wifi';
window.flashTarget = 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');
if (esp32El) esp32El.style.display = 'none';
if (gd32El) gd32El.style.display = 'none';
if (bleEl) bleEl.style.display = '';
return;
}
if (typeof originalSwitchStageFlow === 'function') {
originalSwitchStageFlow(target);
}
var bleEl = document.getElementById('bleStages');
if (bleEl) bleEl.style.display = 'none';
};
window.setConsoleMode = function (mode) {
var isBle = mode === 'ble';
window.connectionMode = isBle ? 'ble' : 'wifi';
var wifiCards = document.getElementById('wifiModeCards');
var bleCards = document.getElementById('bleModeCards');
var wifiBtn = document.getElementById('modeWifiBtn');
var bleBtn = document.getElementById('modeBleBtn');
if (wifiCards) wifiCards.style.display = isBle ? 'none' : '';
if (bleCards) bleCards.style.display = isBle ? '' : 'none';
var headBtns = document.getElementById('bleHeadActions');
if (headBtns) headBtns.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';
applySharedModeUi();
syncBleFileFromSelectedTarget();
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 {
window.switchStageFlow(window.flashTarget || (window.activePhase === 'esp32Ota' ? 'esp32' : 'gd32'));
}
};
window.selectFlashTarget = function (target) {
window.flashTarget = target === 'gd32' ? 'gd32' : 'esp32';
applySharedModeUi();
syncBleFileFromSelectedTarget();
if (window.connectionMode === 'ble') {
window.switchStageFlow('ble');
if (typeof setActivePhase === 'function') {
setActivePhase(target === 'gd32' ? 'gd32Ota' : 'esp32Ota');
}
} else {
window.switchStageFlow(window.flashTarget);
}
if (window.bleOta && typeof window.bleOta.refreshTargetUi === 'function') {
window.bleOta.refreshTargetUi();
}
};
window.handleSharedFileSelect = function (event, chip) {
var file = event && event.target && event.target.files && event.target.files[0] ? event.target.files[0] : null;
if (file) {
window.sharedFirmwareFiles[chip] = { file: file, handle: null };
}
if (typeof window.handleFileSelect === 'function') {
window.handleFileSelect(event, chip);
}
window.flashTarget = chip === 'gd32' ? 'gd32' : 'esp32';
applySharedModeUi();
setTimeout(syncBleFileFromSelectedTarget, 0);
};
window.pickSharedFirmware = function (chip) {
window.flashTarget = chip === 'gd32' ? 'gd32' : 'esp32';
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 () {
window.showOpenFilePicker = nativePicker;
if (!capturedHandle || typeof capturedHandle.getFile !== 'function') {
syncBleFileFromSelectedTarget();
return;
}
capturedHandle.getFile().then(function (file) {
window.sharedFirmwareFiles[chip] = { file: file, handle: capturedHandle };
syncBleFileFromSelectedTarget();
}).catch(function () {
syncBleFileFromSelectedTarget();
});
}).catch(function (err) {
window.showOpenFilePicker = nativePicker;
if (err && err.name !== 'AbortError' && typeof window.addLog === 'function') {
window.addLog('W', '文件', 'FSAPI 选择失败: ' + err.message);
}
});
} 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.
}
};
window.syncBleFileFromSelectedTarget = function () {
if (window.connectionMode !== 'ble' || !window.bleOta || typeof window.bleOta.notifyFileChange !== 'function') {
return;
}
var isGd32 = window.flashTarget === 'gd32';
var chip = isGd32 ? 'gd32' : 'esp32';
var suffix = isGd32 ? 'Gd32' : 'Esp32';
var stored = window.sharedFirmwareFiles[chip];
if (stored && stored.handle && typeof stored.handle.getFile === 'function') {
stored.handle.getFile().then(function (file) {
stored.file = file;
setBleFirmwareInputFile(file);
window.bleOta.notifyFileChange(file, stored.handle);
}).catch(function () {
if (stored.file) {
setBleFirmwareInputFile(stored.file);
window.bleOta.notifyFileChange(stored.file, stored.handle);
}
});
return;
}
if (stored && stored.file) {
setBleFirmwareInputFile(stored.file);
window.bleOta.notifyFileChange(stored.file, stored.handle || null);
return;
}
var input = document.getElementById('fileInput' + suffix);
var info = document.getElementById('fileInfo' + suffix);
var hasInfo = info && info.classList.contains('show');
var file = input && input.files && input.files[0] ? input.files[0] : null;
if (file) {
window.sharedFirmwareFiles[chip] = { file: file, handle: null };
setBleFirmwareInputFile(file);
window.bleOta.notifyFileChange(file, null);
return;
}
if (!hasInfo) {
return;
}
var nameEl = document.getElementById('fileName' + suffix);
var typeEl = document.getElementById('fileType' + suffix);
var sizeEl = document.getElementById('fileSize' + suffix);
var bleInfo = document.getElementById('fileInfo');
var bleName = document.getElementById('fileName');
var bleType = document.getElementById('fileType');
var bleSize = document.getElementById('fileSize');
if (bleInfo) bleInfo.classList.add('show');
if (bleName && nameEl) bleName.textContent = nameEl.textContent;
if (bleType && typeEl) bleType.textContent = typeEl.textContent;
if (bleSize && sizeEl) bleSize.textContent = sizeEl.textContent;
var packetCount = document.getElementById('packetCount');
var sentCount = document.getElementById('sentCount');
if (packetCount) packetCount.textContent = '-';
if (sentCount) sentCount.textContent = '0';
};
window.attachSharedFirmwareDropBridge = function () {
['esp32', 'gd32'].forEach(function (chip) {
var zone = document.getElementById(chip === 'gd32' ? 'uploadZoneGd32' : 'uploadZoneEsp32');
if (!zone) return;
zone.addEventListener('drop', function (event) {
var file = event && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0] ? event.dataTransfer.files[0] : null;
if (file) {
window.sharedFirmwareFiles[chip] = { file: file, handle: null };
}
window.flashTarget = chip;
applySharedModeUi();
setTimeout(syncBleFileFromSelectedTarget, 120);
}, true);
});
};
window.applySharedModeUi = function () {
var isBle = window.connectionMode === 'ble';
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';
var ids = {
wifiEsp32UploadPane: !isGd32,
wifiGd32UploadPane: isGd32,
bleUploadPane: false,
wifiEsp32ActionPane: !isBle && !isGd32,
wifiGd32ActionPane: !isBle && isGd32,
bleActionPane: isBle
};
Object.keys(ids).forEach(function (id) {
var el = document.getElementById(id);
if (el) el.style.display = ids[id] ? '' : 'none';
});
var hint = document.getElementById('readyHint');
if (hint) {
if (isBle) {
hint.innerHTML = 'BLE 模式:连接设备 → 选择固件 → 一键刷写。';
} else if (isGd32) {
hint.innerHTML = 'WiFi 模式:上传 GD32 固件后自动执行 SPI OTA。';
} else {
hint.innerHTML = 'WiFi 模式:上传 ESP32 固件后自动执行 OTA 并重启。';
}
}
};
applySharedModeUi();
})();
</script>
<script src="ble_ota.js?v=20260617b"></script>
</body>
</html>