添加了zlib压缩功能以减少固件传输时间,特别是在弱网环境下 优化了SPI通信参数和错误处理机制 BREAKING CHANGE: 固件版本从0.3.0升级到0.3.1,增加了压缩传输协议 - 在WiFi OTA中集成zlib压缩/解压缩功能 - 添加压缩选项UI界面和前端压缩逻辑 - 修复SPI引脚配置错误(MOSI/MISO接反) - 优化SPI通信参数设置增加CS时序控制 - 改进OTA重试机制和超时处理 - 扩展ota_relay组件栈大小从8K到12K - 添加解压缩回调函数和上下文管理 - 更新固件版本号到0.3.1 - 添加更多调试日志和错误检查
279 lines
14 KiB
HTML
279 lines
14 KiB
HTML
<!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 style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim);margin-top:8px;cursor:pointer;">
|
|
<input type="checkbox" id="compressEsp32"> zlib 压缩(弱网加速,CPU 开销大)
|
|
</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 style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim);margin-bottom:4px;cursor:pointer;">
|
|
<input type="checkbox" id="compressGd32"> zlib 压缩(弱网加速,CPU 开销大)
|
|
</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" 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">固件版本</div><div class="v" id="mVersion" style="font-size:16px;">--</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=20260530f"></script>
|
|
</body>
|
|
</html>
|