Files
ESP32_COMM/tools/index.html
T
小龙虾 51c89b3065 feat(ota): 实现GD32 OTA包间背压机制,优化传输节奏控制
移除固定的包间延迟配置 `OTA_PACKET_DELAY_MS`,采用基于SPI Link v2
CREDIT背压的动态节奏控制,在SpiMaster中通过remoteCredit==0时wait/拒发
实现流量控制,提高OTA传输稳定性。

refactor(stack): 统一任务栈大小配置并添加CPU统计功能

- 在app_config.h中集中定义所有任务栈大小常量,基于实测水位优化分配
- 新增monitor/cpu_stats.cpp组件,提供1Hz FreeRTOS CPU占用采样
- 更新main.cpp中的任务创建使用新的栈大小常量
- 调整health_monitor和ota_relay等任务的栈分配

perf(ota): 改进ESP32和GD32 OTA分区预擦策略

- ESP32 OTA从边写边擦改为整分区预擦模式,避免写入时的扇区擦除阻塞
- GD32 OTA在prepare阶段进行整分区预擦,提升后续写入吞吐量
- 优化OTA协调器状态管理,修复FINALIZING状态残留问题

fix(log): 改进UDP日志发送的堆内存管理

- 当堆内存不足时记录UDP发送跳过的告警信息到ring buffer
- 降低UDP发送跳过的内存阈值从10KB到6KB
- 降低日志排水最小内存阈值从8KB到4KB

docs(readme): 更新OTA配置参数文档说明

移除已废弃的OTA_PACKET_DELAY_MS参数,更新包间节奏控制说明为
基于CREDIT背压机制的描述。
2026-07-16 01:24:12 +08:00

439 lines
24 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.
<!--
index.html - ESP32 OTA 中继控制台主页面
支持 WiFi OTA(ESP32 本机 + GD32 中继)和 BLE OTA 双通道固件升级。
配套脚本:app.js (WiFi OTA), ble_ota.js (BLE OTA), style.css (UI 样式)
-->
<!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="css/base.css?v=20260715a">
<link rel="stylesheet" href="css/layout.css?v=20260715a">
<link rel="stylesheet" href="css/components.css?v=20260715a">
<link rel="stylesheet" href="css/can.css?v=20260715a">
<link rel="stylesheet" href="css/responsive.css?v=20260715a">
</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 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 与触发命令由系统按目标自动设置。</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" id="mHealth">--</div>
<div class="k">健康状态</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">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>
<div class="card" style="display:none">
<div class="card-head">
<span class="ico">🧾</span> 系统状态 (/system/status)
<button class="btn btn-ghost btn-sm card-head-btn" onclick="refreshSystemStatusNow(event)">刷新</button>
</div>
<div class="card-body">
<div class="status-kv-grid">
<div class="status-kv"><span class="k">OTA 状态</span><b id="sOtaState">--</b></div>
<div class="status-kv"><span class="k">Relay 状态</span><b id="sRelayState">--</b></div>
<div class="status-kv"><span class="k">缓存进度</span><b id="sOtaStored">--</b></div>
<div class="status-kv"><span class="k">刷新时间</span><b id="sStatusUpdated">--</b></div>
</div>
</div>
</div>
<div class="card collapsed">
<div class="card-head" onclick="toggleCollapse(this)">
<span class="ico">🧠</span> 堆与任务栈
<button class="btn btn-ghost btn-sm card-head-btn" onclick="refreshSystemStatusNow(event)">刷新</button>
<span class="collapse-arrow">▼</span>
</div>
<div class="card-body">
<div class="status-section-title">CPU 占用</div>
<div class="heap-group cpu-group">
<div class="heap-group-title">ESP32 Runtime <span style="font-weight:400;color:var(--text-dim);font-size:10px;">(FreeRTOS)</span></div>
<div class="heap-bar-row">
<span class="heap-bar-text" id="cpuUsageText">等待采样...</span>
<span class="heap-bar-track"><span class="heap-bar-fill cpu-fill" id="cpuUsageFill"></span></span>
<span class="heap-bar-pct" id="cpuUsagePct">--</span>
</div>
<div class="heap-detail-row">
<span>空闲: <b id="cpuIdlePct">--</b></span>
<span>窗口: <b id="cpuSampleMs">--</b></span>
<span>任务数: <b id="cpuTaskCount">--</b></span>
</div>
</div>
<div class="status-task-list cpu-task-list" id="cpuTaskList">
<div class="status-empty">等待下一次状态刷新...</div>
</div>
<div class="status-section-title">堆概况</div>
<div class="heap-group">
<div class="heap-group-title">内部 DRAM <span style="font-weight:400;color:var(--text-dim);font-size:10px;">(数据堆)</span></div>
<div class="heap-bar-row">
<span class="heap-bar-text" id="memInternalUsage">--</span>
<span class="heap-bar-track"><span class="heap-bar-fill" id="memInternalFill"></span></span>
<span class="heap-bar-pct" id="memInternalPct">--</span>
</div>
<div class="heap-detail-row">
<span>空闲: <b id="mInternalHeap">--</b></span>
<span>最低空闲: <b id="mInternalMinHeap">--</b></span>
<span>最大块: <b id="mInternalLargestBlock">--</b></span>
</div>
</div>
<div class="status-section-title">任务栈水位</div>
<div class="status-task-list" id="taskStackList">
<div class="status-empty">等待状态刷新...</div>
</div>
<div class="status-section-title">原始 JSON</div>
<pre class="status-json" id="statusJson">--</pre>
</div>
</div>
<!-- CAN workspace entry -->
<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-launch-card">
<div class="can-launch-copy">
<div class="can-launch-top">
<div>
<div class="can-launch-kicker">CAN Workspace</div>
<h3>独立 CAN 通信工作台</h3>
</div>
<span class="can-launch-badge">DBC / HEX</span>
</div>
<p class="can-launch-desc">导入 DBC 后自动读取报文和信号,按物理值实时组帧,并复用现有 <code>/can/send</code> 接口直接发送。</p>
<div class="can-launch-points">
<span class="can-launch-point">自动解析 DBC</span>
<span class="can-launch-point">信号实时组帧</span>
<span class="can-launch-point">保留原始 HEX 发送</span>
</div>
</div>
<div class="can-launch-actions">
<a class="btn btn-primary can-launch-primary" href="can.html">进入 CAN 页面</a>
<button class="btn btn-outline can-launch-secondary" type="button" onclick="window.open('can.html','_blank','noopener')">新窗口打开</button>
<a class="btn btn-outline can-launch-secondary" href="can_monitor.html">CAN 接收监控</a>
</div>
<div class="can-launch-foot">
<span class="can-launch-foot-label">说明</span>
<span>手工 CAN 表单已迁到独立页面,避免和 OTA 控制流混在一起;SPI 调试仍保留在当前页。</span>
</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 type="module" src="js/app.js?v=20260716c"></script>
<script type="module" src="js/ota/console-mode.js?v=20260716c"></script>
<script type="module" src="js/ble_ota.js?v=20260716c"></script>
</body>
</html>