feat(ota): 添加OTA回滚保护和固件完整性校验功能
- 启用CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE和CONFIG_APP_ROLLBACK_ENABLE配置选项 - 在main.cpp中添加esp_ota_mark_app_valid_cancel_rollback()调用,防止意外回滚 - 实现SHA-256固件完整性校验,包括计算、存储和验证功能 - 添加ota_mutex.h头文件用于ESP32 OTA全局互斥锁管理 feat(health_monitor): 实现线程安全的健康状态管理 - 将HealthMonitor类的健康状态改为私有成员并通过mutex保护 - 添加getState()和setState()方法提供线程安全的状态访问 - 修复多任务并发访问健康状态时的竞争条件问题 feat(ota_relay): 增强中继模块的线程安全性和可靠性 - 为OtaRelay类添加静态mutex保护状态变量访问 - 实现StateLockGuard RAII锁管理器确保异常安全 - 添加全局超时机制防止OTA过程无限等待 - 改进flash写入逻辑支持非4字节对齐数据处理 refactor(app_config): 更新版本信息和配置常量 - 更新BUILD_TIME至最新编译时间 - 更新GIT_HASH至最新提交 - 添加BLE_OTA_CTRL_VERIFY_SHA256命令常量定义
This commit is contained in:
+504
-185
File diff suppressed because it is too large
Load Diff
+471
-366
File diff suppressed because it is too large
Load Diff
+32
-9
@@ -4,7 +4,7 @@
|
||||
<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">
|
||||
<link rel="stylesheet" href="style.css?v=20260614c">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
@@ -68,8 +68,11 @@
|
||||
<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>
|
||||
</label>
|
||||
<button class="btn btn-primary btn-block" id="uploadEsp32Btn" onclick="startUpload('esp32')" disabled style="margin-top:10px;">
|
||||
上传并升级 ESP32
|
||||
</button>
|
||||
@@ -96,7 +99,6 @@
|
||||
<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>
|
||||
@@ -177,11 +179,32 @@
|
||||
<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>
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
</div>
|
||||
@@ -271,7 +294,7 @@
|
||||
<span id="toastMsg">操作成功</span>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=20260603b"></script>
|
||||
<script src="app.js?v=20260614f"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
+134
-12
@@ -321,7 +321,7 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
|
||||
/* === PRIMARY STATUS BADGES (large) === */
|
||||
.badge-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
|
||||
display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;
|
||||
}
|
||||
.badge-stat {
|
||||
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||
@@ -373,9 +373,106 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
.stage-label {
|
||||
font-size: 11px; color: var(--text-label); text-align: center; line-height: 1.3;
|
||||
}
|
||||
.stage-detail {
|
||||
font-size: 10px; color: var(--accent); text-align: center; line-height: 1.2;
|
||||
margin-top: 2px; min-height: 14px;
|
||||
font-family: var(--font-mono);
|
||||
transition: color .3s;
|
||||
}
|
||||
.stage-node.live .stage-label,
|
||||
.stage-node.done .stage-label,
|
||||
.stage-node.err .stage-label { color: var(--text); }
|
||||
.stage-node.live .stage-detail { color: var(--accent); }
|
||||
.stage-node.done .stage-detail { color: var(--green); }
|
||||
.stage-node.err .stage-detail { color: var(--red); }
|
||||
|
||||
/* Stage status bar */
|
||||
.stage-status-bar {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-top: 14px; padding: 10px 16px;
|
||||
border-radius: 20px;
|
||||
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
transition: all .3s;
|
||||
}
|
||||
.stage-status-bar .ssb-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: var(--text-dim);
|
||||
transition: all .3s;
|
||||
}
|
||||
.stage-status-bar .ssb-text {
|
||||
color: var(--text-dim); font-size: 13px; font-weight: 500;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
/* idle — muted, with subtle pulse */
|
||||
.stage-status-bar .ssb-dot { animation: dotPulse 2.5s ease-in-out infinite; }
|
||||
@keyframes dotPulse {
|
||||
0%, 100% { opacity: .45; }
|
||||
50% { opacity: .8; }
|
||||
}
|
||||
/* active — accent colored */
|
||||
.stage-status-bar.active {
|
||||
border-color: rgba(34,211,238,.25);
|
||||
background: linear-gradient(135deg, rgba(34,211,238,.06), rgba(34,211,238,.02));
|
||||
}
|
||||
.stage-status-bar.active .ssb-dot {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px var(--accent-dim);
|
||||
animation: dotPulseLive 1.2s ease-in-out infinite;
|
||||
}
|
||||
.stage-status-bar.active .ssb-text { color: var(--accent); font-weight: 600; }
|
||||
@keyframes dotPulseLive {
|
||||
0%, 100% { opacity: .7; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.25); }
|
||||
}
|
||||
/* ok — green */
|
||||
.stage-status-bar.ok {
|
||||
border-color: rgba(34,197,94,.25);
|
||||
background: linear-gradient(135deg, rgba(34,197,94,.06), rgba(34,197,94,.02));
|
||||
}
|
||||
.stage-status-bar.ok .ssb-dot {
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 6px rgba(34,197,94,.3);
|
||||
animation: none;
|
||||
}
|
||||
.stage-status-bar.ok .ssb-text { color: var(--green); font-weight: 600; }
|
||||
/* err — red */
|
||||
.stage-status-bar.err {
|
||||
border-color: rgba(239,68,68,.25);
|
||||
background: linear-gradient(135deg, rgba(239,68,68,.06), rgba(239,68,68,.02));
|
||||
}
|
||||
.stage-status-bar.err .ssb-dot {
|
||||
background: var(--red);
|
||||
box-shadow: 0 0 6px rgba(239,68,68,.3);
|
||||
animation: none;
|
||||
}
|
||||
.stage-status-bar.err .ssb-text { color: var(--red); font-weight: 600; }
|
||||
|
||||
/* Stage verification info panel */
|
||||
.stage-verify-info {
|
||||
margin-top: 12px; padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-elevated); border: 1px solid var(--border);
|
||||
}
|
||||
.svi-row {
|
||||
display: flex; align-items: flex-start; gap: 12px;
|
||||
padding: 6px 0; font-size: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.03);
|
||||
}
|
||||
.svi-row:last-child { border-bottom: none; }
|
||||
.svi-key {
|
||||
color: var(--text-label); font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: .06em; font-size: 10px;
|
||||
min-width: 100px; flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.svi-val {
|
||||
color: var(--text-dim); font-family: var(--font-mono); font-size: 11px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.svi-val.ok { color: var(--green); font-weight: 600; }
|
||||
.svi-val.err { color: var(--red); font-weight: 600; }
|
||||
|
||||
/* === LOG PANEL === */
|
||||
.log-panel { display: flex; flex-direction: column; min-height: 380px; }
|
||||
@@ -417,6 +514,9 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
.log-line .lv.D { color: var(--text-label); }
|
||||
.log-line .lg { color: #a78bfa; flex-shrink: 0; }
|
||||
.log-line .lm { color: var(--text); word-break: break-all; }
|
||||
/* BLE log compat */
|
||||
.log-line .t { color: var(--text-label); flex-shrink: 0; }
|
||||
.log-line .tag { color: #a78bfa; flex-shrink: 0; }
|
||||
|
||||
.log-foot {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
@@ -434,19 +534,41 @@ select option { background: var(--bg-panel); color: var(--text); }
|
||||
|
||||
/* === TOAST === */
|
||||
.toast {
|
||||
position: fixed; bottom: 28px; right: 28px; z-index: 2000;
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 14px 20px;
|
||||
position: fixed; top: 45%; left: 50%; transform: translateX(-50%) translateY(30px);
|
||||
z-index: 9999;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
transform: translateY(120px); opacity: 0;
|
||||
transition: transform .25s ease, opacity .25s ease;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,.55);
|
||||
padding: 14px 24px; border-radius: 10px;
|
||||
font-size: 14px; font-weight: 600; color: #fff; pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: all .35s cubic-bezier(.4,0,.2,1);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.35);
|
||||
}
|
||||
.toast.show { transform: translateY(0); opacity: 1; }
|
||||
.toast.ok { border-left: 3px solid var(--green); }
|
||||
.toast.err { border-left: 3px solid var(--red); }
|
||||
.toast.warn { border-left: 3px solid var(--amber); }
|
||||
.toast .toast-ico { font-size: 20px; }
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto; }
|
||||
.toast.ok { background: #16a34a; }
|
||||
.toast.err { background: #dc2626; }
|
||||
.toast.warn { background: #d97706; }
|
||||
.toast .toast-ico { font-size: 18px; }
|
||||
|
||||
/* === GRID ROWS === */
|
||||
.row-2, .row-3, .row-4 { display: grid; gap: 10px; }
|
||||
.row-2 { grid-template-columns: 1fr 1fr; }
|
||||
.row-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.row-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
|
||||
/* === SUB ACTIONS === */
|
||||
.sub-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; }
|
||||
|
||||
/* === FOOTER NOTE === */
|
||||
.footer-note { margin-top: 12px; font-size: 12px; line-height: 1.7; color: var(--text-dim); }
|
||||
|
||||
/* === CODE === */
|
||||
code { color: #d3f9ff; font-family: var(--font-mono); }
|
||||
|
||||
/* === FILE INFO BORDER === */
|
||||
.file-info { border: 1px solid var(--border); }
|
||||
|
||||
/* === PILL LIVE === */
|
||||
.pill.live .indicator { background: var(--amber); animation: blink 1s infinite; }
|
||||
|
||||
/* === RESPONSIVE === */
|
||||
@media (max-width: 1100px) {
|
||||
|
||||
Reference in New Issue
Block a user