Files
ESP32_COMM/tools/index.html
T

278 lines
14 KiB
HTML
Raw Normal View History

<!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=20260530b">
</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 idle" id="pillRelay"><span class="indicator"></span><span id="pillRelayTxt">待机</span></span>
<span class="pill" id="pillUptime">运行 --</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> 设备连接</div>
<div class="card-body">
<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 class="conn-indicator err" id="connStatus">
<span class="light"></span>
<span id="connStatusTxt">等待连接</span>
</div>
</div>
</div>
<!-- 2. ESP32 OTA -->
<div class="card">
<div class="card-head"><span class="ico">📦</span> ESP32 自身升级</div>
<div class="card-body">
<div style="font-size:12px;color:var(--text-dim);margin-bottom:10px;">上传固件并执行 OTA,设备将自动重启</div>
<div class="upload-zone upload-zone-sm" id="uploadZoneEsp32" onclick="if(window.showOpenFilePicker){pickFileViaFSAPI('esp32')}else{document.getElementById('fileInputEsp32').click()}">
<div class="upload-ico">📤</div>
<h4>点击或拖拽 ESP32 固件</h4>
</div>
<input type="file" id="fileInputEsp32" accept=".bin" onchange="handleFileSelect(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="clearFile('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>
</label>
<button class="btn btn-primary btn-block" id="uploadEsp32Btn" onclick="startUpload('esp32')" disabled style="margin-top:10px;">
上传并升级 ESP32
</button>
</div>
</div>
<!-- 3. GD32 OTA -->
<div class="card">
<div class="card-head"><span class="ico">📦</span> GD32 SPI 中继</div>
<div class="card-body">
<div style="font-size:12px;color:var(--text-dim);margin-bottom:10px;">上传固件到缓存,再触发 SPI 中继刷写<br><span style="color:var(--accent);">支持 .bin 和 .hex(自动转换)</span></div>
<div class="upload-zone upload-zone-sm" id="uploadZoneGd32" onclick="if(window.showOpenFilePicker){pickFileViaFSAPI('gd32')}else{document.getElementById('fileInputGd32').click()}">
<div class="upload-ico">📤</div>
<h4>点击或拖拽 GD32 固件</h4>
</div>
<input type="file" id="fileInputGd32" accept=".hex,.bin" onchange="handleFileSelect(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="clearFile('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>
</label>
<button class="btn btn-success btn-block" id="uploadGd32Btn" onclick="startUpload('gd32')" disabled style="margin-top:10px;margin-bottom:8px;">
上传固件到缓存
</button>
<button class="btn btn-outline btn-block" id="relayBtn" onclick="triggerRelay()">
触发 GD32 OTA 中继
</button>
</div>
</div>
<!-- 4. Flash Progress -->
<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="ppGd32Up"><span class="pp-ico">📤</span><span class="pp-txt">GD32 上传</span></div>
<div class="pp-step" id="ppGd32Relay"><span class="pp-ico">🔀</span><span class="pp-txt">GD32 转发</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="margin-top:12px;">
<button class="btn btn-outline btn-block" onclick="restartEsp32()">重启 ESP32</button>
<a class="btn btn-outline btn-block" href="ble_ota.html?v=20260509-0046" target="_blank" style="margin-top:10px;text-decoration:none;">
切换到 BLE 蓝牙刷写 ↗
</a>
</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>
</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">WiFi / IP</div><div class="v" id="mWifi" style="font-size:14px;">--</div></div>
<div class="metric"><div class="k">信号强度</div><div class="v" id="mRSSI">--</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">ESP32 版本</div><div class="v" id="mVersion" 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>
<!-- OTA stage flow -->
<div class="card">
<div class="card-head"><span class="ico">🔀</span> OTA 流程阶段</div>
<div class="card-body">
<div class="stage-flow">
<div class="stage-node" id="s1"><div class="stage-dot">1</div><div class="stage-label">握手认证</div></div>
<div class="stage-node" id="s2"><div class="stage-dot">2</div><div class="stage-label">传输准备</div></div>
<div class="stage-node" id="s3"><div class="stage-dot">3</div><div class="stage-label">数据转发</div></div>
<div class="stage-node" id="s4"><div class="stage-dot">4</div><div class="stage-label">完成跳转</div></div>
</div>
</div>
</div>
<!-- CAN panel -->
<div class="card">
<div class="card-head"><span class="ico">📨</span> CAN 帧发送</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">
<div class="card-head"><span class="ico">⚡</span> SPI 发送</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=20260603b"></script>
</body>
</html>