docs(review): 专家评审 2026-10-19

This commit is contained in:
cbb
2026-10-20 21:13:50 +08:00
parent af1f68222e
commit ff590d8996
6 changed files with 1523 additions and 0 deletions
+7
View File
@@ -1545,6 +1545,13 @@ python "F:\.espressif\v6.0\esp-idf\tools\idf.py" build
</section>
<!-- ================================================================ -->
<div class="warnbox" style="border-left-color:#7c3aed;background:#f5f3ff"><b>2026-10-19 复核更新(专家评审,基线 af1f682 / v0.5.2)</b>:上方 2026-08-16 四项结论原文保留,复核结果如下——
<br>① 零认证:<b>仍存在</b>,且发现 <code>/spi/send</code> 可原样透传任意 SPI 帧类型、<code>timeout_ms</code> 无上限,升级为<b>致命</b>(评审 F-01 / S-12)。
<br>② 凭据硬编码:<b>仍存在</b>,且 STA 口令为真实口令、已进入 git 历史,需轮换(S-08)。
<br>③ 无固件签名:<b>仍存在</b>;另发现 ESP32 回滚保护取消过早、回滚开关所在 <code>config/sdkconfig.defaults</code> 未被构建引用(S-01 / S-02)。
<br>④ AES-ECB:<b>仍存在</b>。
<br>“缓解现状”中“nonce 每会话随机绑定防重放”<b>不成立</b>:nonce 仅 4 字节 tick 变化且明文写入日志(S-06)。新增:BLE GATT 无配对/加密(S-09)。
<br>完整问题清单、既有评审跟踪与修复路线图见 <a href="review/ESP32_COMM_专家评审_2026-10-19.html">docs/review/ESP32_COMM_专家评审_2026-10-19.html</a>;历史评审原文已恢复至 <code>docs/review/archive/</code>。</div>
<h2 id="s20">20. 版本演进摘要(0.3.x → 0.5.2)</h2>
<section class="block">
<table>
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
# docs/review — 评审记录归档
| 文件 | 日期 | 说明 |
|---|---|---|
| [ESP32_COMM_专家评审_2026-10-19.html](ESP32_COMM_专家评审_2026-10-19.html) | 2026-10-19 | 全量专家评审(基线 af1f682 / v0.5.2),含既有评审逐条跟踪 |
| [archive/esp32_architecture_review_2026-06-27.md](archive/esp32_architecture_review_2026-06-27.md) | 2026-06-27 | 架构扫描(原 docs/,af1f682 删除,原样恢复) |
| [archive/CODE_REVIEW_REPORT_2026-06-13.html](archive/CODE_REVIEW_REPORT_2026-06-13.html) | 2026-06-13 | 高风险代码评审 v0.3.12(原 docs/,5b2c5f8 删除,原样恢复) |
| [archive/COMMUNICATION_AUDIT_2026-05-05.md](archive/COMMUNICATION_AUDIT_2026-05-05.md) | 2026-05-05 | 通信梳理(原 docs/,5b2c5f8 删除,原样恢复) |
| ../project_introduction.html §19 | 2026-08-16 / 2026-10-19 | 安全审计原文 + 2026-10-19 复核更新 |
约定:评审记录只增不删;新评审引用并跟踪旧结论。
@@ -0,0 +1,970 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32_COMM 高风险代码评审报告</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--bg-card: #1c2128;
--border: #30363d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--critical: #f85149;
--critical-bg: rgba(248,81,73,0.12);
--critical-border: rgba(248,81,73,0.4);
--high: #d29922;
--high-bg: rgba(210,153,34,0.12);
--high-border: rgba(210,153,34,0.4);
--medium: #58a6ff;
--medium-bg: rgba(88,166,255,0.12);
--medium-border: rgba(88,166,255,0.4);
--accent: #58a6ff;
--success: #3fb950;
--success-bg: rgba(63,185,80,0.12);
--code-bg: #0d1117;
--table-stripe: rgba(110,118,129,0.05);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', Helvetica, Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Header */
.report-header {
text-align: center;
padding: 3rem 0;
border-bottom: 1px solid var(--border);
margin-bottom: 3rem;
}
.report-header h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, #f85149, #d29922, #58a6ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.report-header .subtitle {
color: var(--text-secondary);
font-size: 1rem;
margin-bottom: 1.5rem;
}
.report-meta {
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
font-size: 0.85rem;
color: var(--text-muted);
}
.report-meta span { display: flex; align-items: center; gap: 0.4rem; }
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin-bottom: 3rem;
}
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: transform 0.2s, border-color 0.2s;
}
.stat-card:hover { transform: translateY(-2px); }
.stat-card .number {
font-size: 2.5rem;
font-weight: 800;
line-height: 1;
margin-bottom: 0.3rem;
}
.stat-card .label {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-card.critical { border-color: var(--critical-border); }
.stat-card.critical .number { color: var(--critical); }
.stat-card.high { border-color: var(--high-border); }
.stat-card.high .number { color: var(--high); }
.stat-card.medium { border-color: var(--medium-border); }
.stat-card.medium .number { color: var(--medium); }
.stat-card.total { border-color: var(--accent); }
.stat-card.total .number { color: var(--accent); }
.stat-card.verified { border-color: var(--success); }
.stat-card.verified .number { color: var(--success); }
/* Dimension Table */
.section { margin-bottom: 3rem; }
.section-title {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 1.2rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--border);
display: flex;
align-items: center;
gap: 0.6rem;
}
.section-title .icon { font-size: 1.2rem; }
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
th, td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: var(--bg-tertiary);
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
}
tr:nth-child(even) td { background: var(--table-stripe); }
/* Severity Badges */
.badge {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.6rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.badge.critical { background: var(--critical-bg); color: var(--critical); border: 1px solid var(--critical-border); }
.badge.high { background: var(--high-bg); color: var(--high); border: 1px solid var(--high-border); }
.badge.medium { background: var(--medium-bg); color: var(--medium); border: 1px solid var(--medium-border); }
/* Finding Cards */
.finding-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 1.2rem;
overflow: hidden;
transition: border-color 0.2s;
}
.finding-card:hover { border-color: var(--text-muted); }
.finding-card.critical { border-left: 4px solid var(--critical); }
.finding-card.high { border-left: 4px solid var(--high); }
.finding-header {
padding: 1.2rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
cursor: pointer;
user-select: none;
}
.finding-header:hover { background: var(--bg-tertiary); }
.finding-title-group { flex: 1; }
.finding-number {
font-size: 0.7rem;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.2rem;
}
.finding-title {
font-size: 1.05rem;
font-weight: 600;
line-height: 1.4;
}
.finding-badges {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
align-items: center;
flex-wrap: wrap;
justify-content: flex-end;
}
.finding-body {
padding: 0 1.5rem 1.5rem;
display: none;
}
.finding-card.open .finding-body { display: block; }
.finding-body .detail-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.4rem 1rem;
margin-bottom: 1rem;
font-size: 0.85rem;
}
.detail-label {
color: var(--text-muted);
font-weight: 500;
white-space: nowrap;
}
.detail-value { color: var(--text-primary); }
.code-block {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin: 0.8rem 0;
overflow-x: auto;
font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
font-size: 0.82rem;
line-height: 1.5;
}
.code-block .comment { color: #6a9955; }
.code-block .keyword { color: #c586c0; }
.code-block .type { color: #4ec9b0; }
.code-block .string { color: #ce9178; }
.code-block .highlight { background: rgba(248,81,73,0.15); display: inline; padding: 0.1em 0.2em; border-radius: 3px; }
.impact-box {
background: rgba(248,81,73,0.06);
border: 1px solid rgba(248,81,73,0.2);
border-radius: 8px;
padding: 0.8rem 1rem;
margin: 0.8rem 0;
font-size: 0.88rem;
}
.impact-box .impact-label {
color: var(--critical);
font-weight: 600;
font-size: 0.8rem;
margin-bottom: 0.3rem;
}
.fix-box {
background: rgba(63,185,80,0.06);
border: 1px solid rgba(63,185,80,0.2);
border-radius: 8px;
padding: 0.8rem 1rem;
margin: 0.8rem 0;
font-size: 0.88rem;
}
.fix-box .fix-label {
color: var(--success);
font-weight: 600;
font-size: 0.8rem;
margin-bottom: 0.3rem;
}
/* Architecture Section */
.arch-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
}
.arch-card h4 {
font-size: 1rem;
margin-bottom: 0.6rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.arch-card p { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.7; }
/* Priority Table */
.priority-table { margin-top: 1rem; }
.priority-table .p0 { color: var(--critical); font-weight: 700; }
.priority-table .p1 { color: var(--high); font-weight: 600; }
.priority-table .p2 { color: var(--medium); font-weight: 500; }
.priority-table .p3 { color: var(--text-secondary); }
.progress-bar {
width: 100%;
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
margin-top: 0.3rem;
}
.progress-bar .fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease;
}
.fill.critical-fill { background: var(--critical); }
.fill.high-fill { background: var(--high); }
.fill.medium-fill { background: var(--medium); }
/* Chart */
.chart-container {
display: flex;
align-items: flex-end;
gap: 1.5rem;
height: 200px;
padding: 1rem 0;
justify-content: center;
}
.chart-bar-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
max-width: 100px;
}
.chart-bar-stack {
width: 60px;
display: flex;
flex-direction: column-reverse;
border-radius: 6px 6px 0 0;
overflow: hidden;
}
.chart-bar-stack > div {
width: 100%;
transition: height 0.6s ease;
}
.chart-label {
font-size: 0.72rem;
color: var(--text-muted);
text-align: center;
white-space: nowrap;
}
.chart-value {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-primary);
}
/* Expand/Collapse all */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.toolbar button {
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 0.4rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
transition: all 0.2s;
}
.toolbar button:hover {
background: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--text-muted);
}
/* Footer */
.report-footer {
text-align: center;
padding: 2rem 0;
border-top: 1px solid var(--border);
margin-top: 3rem;
color: var(--text-muted);
font-size: 0.8rem;
}
/* Responsive */
@media (max-width: 768px) {
.container { padding: 1rem; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.finding-header { flex-direction: column; }
.finding-badges { justify-content: flex-start; }
.chart-container { gap: 0.8rem; }
.chart-bar-stack { width: 40px; }
}
/* Print */
@media print {
body { background: white; color: black; }
.finding-body { display: block !important; }
.stat-card, .finding-card, .arch-card { break-inside: avoid; }
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="report-header">
<h1>ESP32_COMM 高风险代码评审报告</h1>
<div class="subtitle">多维度并行审查 + 对抗性验证 · 4维度 × 33项确认发现</div>
<div class="report-meta">
<span>&#128203; 项目: ESP32_COMM (ESP32-C3 OTA Relay)</span>
<span>&#128197; 日期: 2026-06-13</span>
<span>&#128268; 固件版本: v0.3.12</span>
<span>&#128101; 审查Agent: 4 Review + 36 Verify + 1 Synthesize</span>
</div>
</header>
<!-- Stats -->
<div class="stats-grid">
<div class="stat-card total">
<div class="number">33</div>
<div class="label">确认发现</div>
</div>
<div class="stat-card critical">
<div class="number">8</div>
<div class="label">CRITICAL</div>
</div>
<div class="stat-card high">
<div class="number">18</div>
<div class="label">HIGH</div>
</div>
<div class="stat-card medium">
<div class="number">4</div>
<div class="label">MEDIUM</div>
</div>
<div class="stat-card verified">
<div class="number">33/36</div>
<div class="label">验证通过率</div>
</div>
<div class="stat-card total">
<div class="number">2</div>
<div class="label">已驳回(误报)</div>
</div>
</div>
<!-- Dimension Chart -->
<div class="section">
<div class="section-title"><span class="icon">&#128202;</span> 各维度发现分布</div>
<div class="chart-container">
<div class="chart-bar-group">
<div class="chart-value">8</div>
<div class="chart-bar-stack" style="height:160px;">
<div style="height:60px;background:var(--critical);" title="2 CRITICAL"></div>
<div style="height:80px;background:var(--high);" title="4 HIGH"></div>
<div style="height:20px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">内存安全</div>
</div>
<div class="chart-bar-group">
<div class="chart-value">10</div>
<div class="chart-bar-stack" style="height:180px;">
<div style="height:54px;background:var(--critical);" title="3 CRITICAL"></div>
<div style="height:108px;background:var(--high);" title="6 HIGH"></div>
<div style="height:18px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">并发/RTOS</div>
</div>
<div class="chart-bar-group">
<div class="chart-value">9</div>
<div class="chart-bar-stack" style="height:170px;">
<div style="height:72px;background:var(--critical);" title="4 CRITICAL"></div>
<div style="height:72px;background:var(--high);" title="4 HIGH"></div>
<div style="height:18px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">OTA/固件</div>
</div>
<div class="chart-bar-group">
<div class="chart-value">7</div>
<div class="chart-bar-stack" style="height:150px;">
<div style="height:43px;background:var(--critical);" title="2 CRITICAL"></div>
<div style="height:86px;background:var(--high);" title="4 HIGH"></div>
<div style="height:14px;background:var(--medium);" title="1 MEDIUM"></div>
</div>
<div class="chart-label">SPI/协议</div>
</div>
</div>
</div>
<!-- TOP 3 Critical -->
<div class="section">
<div class="section-title"><span class="icon">&#128680;</span> TOP 3 最危险 Bug</div>
<!-- Finding 1 -->
<div class="finding-card critical open" id="f1">
<div class="finding-header" onclick="toggleFinding('f1')">
<div class="finding-title-group">
<div class="finding-number">Finding #1 — CRITICAL</div>
<div class="finding-title">SPI 总线无并发保护 — 多任务竞争导致协议损坏</div>
</div>
<div class="finding-badges">
<span class="badge critical">CRITICAL</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border);">并发竞态</span>
</div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span>
<span class="detail-value">spi_master.h:73-142, ota_relay.cpp, wifi_ota.cpp</span>
<span class="detail-label">维度:</span>
<span class="detail-value">并发/RTOS</span>
<span class="detail-label">验证:</span>
<span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度 — 源码确认</span>
</div>
<p>SpiMaster 是单例但<strong>没有任何互斥锁</strong>。多个 FreeRTOS 任务直接调用 SPI 方法:ota_relay 任务 (优先级 MAX-2)、wifi_ota 任务 (MAX-3)、health_monitor 任务 (MAX-4)。协议操作是多步非原子的(如 otaTransfer = 发送帧 + 轮询响应,CS在两次传输之间释放),高优先级任务可在协议序列中间抢占。</p>
<div class="code-block"><span class="comment">// spi_master.h — 无任何mutex成员</span>
<span class="keyword">class</span> <span class="type">SpiMaster</span> {
<span class="keyword">private</span>:
<span class="type">spi_device_handle_t</span> m_spiDevice = <span class="keyword">NULL</span>; <span class="comment">// ← 唯一成员,无锁</span>
};</div>
<div class="impact-box">
<div class="impact-label">&#9888; 影响</div>
帧交错(A任务的请求配对B任务的响应)、GD32状态机混乱、固件传输数据损坏。生产环境高频CAN通信+OTA并发时必现。
</div>
<div class="fix-box">
<div class="fix-label">&#128295; 修复建议</div>
在 SpiMaster 中添加 <code>SemaphoreHandle_t m_mutex</code>,所有公共方法入口加锁。对于多步协议操作(otaTransfer等),使用统一的锁粒度保护整个请求-响应序列。
</div>
</div>
</div>
<!-- Finding 2 -->
<div class="finding-card critical" id="f2">
<div class="finding-header" onclick="toggleFinding('f2')">
<div class="finding-title-group">
<div class="finding-number">Finding #2 — CRITICAL</div>
<div class="finding-title">硬编码占位密钥 — 认证体系形同虚设</div>
</div>
<div class="finding-badges">
<span class="badge critical">CRITICAL</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border);">安全/OTA</span>
</div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span>
<span class="detail-value">app_config.h:107-115</span>
<span class="detail-label">维度:</span>
<span class="detail-value">OTA/固件完整性</span>
<span class="detail-label">验证:</span>
<span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度 — 源码确认</span>
</div>
<p>MASTER_KEY 使用 NIST AES 测试向量,SCRAMBLE_CONST 使用简单递增序列,GD32_CHIP_UID 使用 0x00-0x0B。注释标注"需填入实际密钥"但从未替换。</p>
<div class="code-block"><span class="comment">// ⚠️ 请填入实际密钥 ← 注释说"需填入"但从未填入</span>
<span class="keyword">inline constexpr</span> <span class="type">uint8_t</span> MASTER_KEY[] = {
<span class="highlight">0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF</span>, <span class="comment">// ← NIST AES测试向量!</span>
<span class="highlight">0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10</span>
};
<span class="keyword">inline constexpr</span> <span class="type">uint8_t</span> SCRAMBLE_CONST[] = {
<span class="highlight">0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88</span>, <span class="comment">// ← 简单递增序列</span>
<span class="highlight">0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00</span>
};
<span class="keyword">inline constexpr</span> <span class="type">uint8_t</span> GD32_CHIP_UID[] = {
<span class="highlight">0x00, 0x01, 0x02, 0x03, 0x04, 0x05</span>, <span class="comment">// ← 0x00-0x0B</span>
<span class="highlight">0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B</span>
};</div>
<div class="impact-box">
<div class="impact-label">&#9888; 影响</div>
攻击者可轻易伪造认证握手,向 GD32 注入恶意固件。GD32_CHIP_UID占位值意味着所有设备共享相同"身份"(如果运行时未正确覆盖)。
</div>
<div class="fix-box">
<div class="fix-label">&#128295; 修复建议</div>
从安全存储(NVS/efuse)读取密钥;移除所有硬编码占位值;添加编译期断言确保非测试密钥;MASTER_KEY和SCRAMBLE_CONST应在首次设备配准时由安全随机数生成。
</div>
</div>
</div>
<!-- Finding 3 -->
<div class="finding-card critical" id="f3">
<div class="finding-header" onclick="toggleFinding('f3')">
<div class="finding-title-group">
<div class="finding-number">Finding #3 — CRITICAL</div>
<div class="finding-title">Flash 写入无对齐检查 — 可能导致固件损坏</div>
</div>
<div class="finding-badges">
<span class="badge critical">CRITICAL</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border);">内存安全/OTA</span>
</div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span>
<span class="detail-value">ota_relay.cpp:308-320, wifi_ota.cpp:711/740, ble_ota.cpp:575</span>
<span class="detail-label">维度:</span>
<span class="detail-value">内存安全 + OTA完整性</span>
<span class="detail-label">验证:</span>
<span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度 — 源码确认</span>
</div>
<div class="code-block"><span class="type">esp_err_t</span> OtaRelay::flashWriteChunk(<span class="type">uint32_t</span> offset, <span class="keyword">const</span> <span class="type">uint8_t</span> *data, <span class="type">size_t</span> len)
{
<span class="comment">// ... 无对齐检查 ...</span>
<span class="keyword">return</span> <span class="highlight">esp_partition_write</span>(instance().m_gd32Partition, offset, data, len); <span class="comment">// ← offset/len可能非4字节对齐</span>
}</div>
<p>两个调用路径产生非对齐值:(1) WiFi OTA: gd32_offset 从 httpd_req_recv 返回值累加,最后一个chunk是固件余数;(2) BLE OTA: data_len = recv_len - 11(7字节头+4字节CRC),不保证4字节对齐。esp_partition_write 要求 offset 和 size 均4字节对齐。</p>
<div class="impact-box">
<div class="impact-label">&#9888; 影响</div>
非对齐写入导致 ESP_ERR_INVALID_ARG 或触发 read-modify-write 异常,GD32固件最后一个chunk损坏或丢失。
</div>
<div class="fix-box">
<div class="fix-label">&#128295; 修复建议</div>
flashWriteChunk 内部做4字节对齐对齐(不足部分 read-modify-write),或在上层确保只传对齐参数。
</div>
</div>
</div>
</div>
<!-- Other CRITICAL Findings -->
<div class="section">
<div class="section-title"><span class="icon">&#128308;</span> 其他 CRITICAL 级发现</div>
<div class="finding-card critical" id="f4">
<div class="finding-header" onclick="toggleFinding('f4')">
<div class="finding-title-group">
<div class="finding-number">Finding #4 — CRITICAL</div>
<div class="finding-title">BLE/WiFi 双通道 OTA 同时触发 — ESP32 OTA状态冲突</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">wifi_ota.h:42-46, ble_ota.h</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<p>BleOta 和 WifiOta 各自维护独立的 m_esp32OtaActive、m_esp32OtaHandle、m_esp32UpdatePartition,但底层 esp_ota_begin/write/end 操作的是同一个OTA分区。两个通道同时触发时:第二个 esp_ota_begin 会失败或覆盖句柄;esp_ota_write 可能交错写入同一分区;导致固件损坏+变砖。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>添加全局 OTA 互斥标志,同一时刻只允许一个OTA源(BLE或WiFi)。</div>
</div>
</div>
<div class="finding-card critical" id="f5">
<div class="finding-header" onclick="toggleFinding('f5')">
<div class="finding-title-group">
<div class="finding-number">Finding #5 — CRITICAL</div>
<div class="finding-title">SPI 协议帧仅用 XOR 伪校验 — 数据损坏不可检测</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">spi_master.cpp:134-141</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<div class="code-block"><span class="type">uint8_t</span> SpiMaster::otaCrc8(<span class="keyword">const</span> <span class="type">uint8_t</span> *data, <span class="type">size_t</span> len) {
<span class="type">uint8_t</span> crc = <span class="highlight">0</span>;
<span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="highlight">0</span>; i &lt; len; ++i)
<span class="highlight">crc ^= data[i]</span>; <span class="comment">// ← 简单XOR,非CRC8</span>
<span class="keyword">return</span> crc;
}</div>
<p>XOR校验无法检测:偶数个相同位置的位翻转、字节交换、全零段丢失。固件数据的静默损坏几乎不可检测。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>替换为 CRC-8/MAXIM 或 CRC-16/CCITT,与GD32团队协商。</div>
</div>
</div>
<div class="finding-card critical" id="f6">
<div class="finding-header" onclick="toggleFinding('f6')">
<div class="finding-title-group">
<div class="finding-number">Finding #6 — CRITICAL</div>
<div class="finding-title">SPI_OTA_SOF 与 SPI_APP_ENTER_BOOT_ACK_OK 值碰撞 (0xA5)</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">spi_master.h:21,26</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<div class="code-block"><span class="keyword">#define</span> <span class="highlight">SPI_OTA_SOF</span> (<span class="highlight">0xA5</span>) <span class="comment">// OTA帧起始标记</span>
<span class="keyword">#define</span> <span class="highlight">SPI_APP_ENTER_BOOT_ACK_OK</span> (<span class="highlight">0xA5</span>) <span class="comment">// APP确认进入Bootloader</span></div>
<p>同一值 0xA5 表示两种完全不同的语义。GD32在OTA窗口回复ACK时,ESP32可能将ACK字节误解析为OTA帧起始,导致协议状态机混乱。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>修改其中一个常量值,消除歧义。例如将 SPI_APP_ENTER_BOOT_ACK_OK 改为 0xB5。</div>
</div>
</div>
<div class="finding-card critical" id="f7">
<div class="finding-header" onclick="toggleFinding('f7')">
<div class="finding-title-group">
<div class="finding-number">Finding #7 — CRITICAL</div>
<div class="finding-title">OTA 会话无全局超时 — 可无限挂起</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">ota_relay.cpp:76-91,562; ota_relay.h:59</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<p>waitUntilNotWait() 每次调用有 max_wait_rounds 上限,但整个 forwardFirmware() 流程没有全局超时。GD32 持续返回 WAIT 时,单个 SEND_SIZE阶段可挂起 ~78秒,完整流程可挂起数分钟。OTA_RESULT_ERR_USER_ABORT 已定义但从未使用。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>在 forwardFirmware() 入口记录开始时间,每个阶段检查全局超时(如5分钟),超时则中止并清理。</div>
</div>
</div>
<div class="finding-card critical" id="f8">
<div class="finding-header" onclick="toggleFinding('f8')">
<div class="finding-title-group">
<div class="finding-number">Finding #8 — CRITICAL</div>
<div class="finding-title">HTTP CORS 允许所有来源 — 任意网页可触发 OTA</div>
</div>
<div class="finding-badges"><span class="badge critical">CRITICAL</span></div>
</div>
<div class="finding-body">
<div class="detail-grid">
<span class="detail-label">文件:</span><span class="detail-value">wifi_ota.cpp:58</span>
<span class="detail-label">验证:</span><span class="detail-value" style="color:var(--success);">&#10004; HIGH置信度</span>
</div>
<div class="code-block">httpd_resp_set_hdr(req, <span class="string">"Access-Control-Allow-Origin"</span>, <span class="highlight">"*"</span>);</div>
<p>任何网页都可以向 ESP32 发起 OTA 请求。结合占位密钥问题,攻击链完整:恶意网页 → 上传伪造固件 → 通过占位密钥认证 → 烧录恶意GD32固件。</p>
<div class="fix-box"><div class="fix-label">&#128295; 修复建议</div>限制 CORS 为本地网络来源,或移除 CORS头并使用同源策略。</div>
</div>
</div>
</div>
<!-- HIGH Findings -->
<div class="section">
<div class="section-title"><span class="icon">&#128992;</span> HIGH 级关键发现</div>
<div class="toolbar">
<span style="color:var(--text-muted);font-size:0.85rem;">共 18 项 HIGH 级发现,点击展开详情</span>
<button onclick="toggleAll('high')">展开/折叠全部</button>
</div>
<table>
<thead>
<tr><th>#</th><th>严重度</th><th>文件</th><th>问题</th><th>影响</th></tr>
</thead>
<tbody>
<tr><td>9</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:121</code></td><td>WiFi事件回调中调用 esp_wifi_connect() 重连</td><td>在系统事件任务中调用可能死锁</td></tr>
<tr><td>10</td><td><span class="badge high">HIGH</span></td><td><code>health_monitor.cpp</code></td><td>healthState 静态变量无锁跨任务读写</td><td>竞态导致健康判断错误</td></tr>
<tr><td>11</td><td><span class="badge high">HIGH</span></td><td><code>ota_relay.h:80-84</code></td><td>5个静态变量(relayState等)无锁跨任务访问</td><td>协议状态不一致</td></tr>
<tr><td>12</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.h:33-47</code></td><td>WifiOta 所有成员 public 且无锁</td><td>HTTP回调与WiFi事件回调竞争</td></tr>
<tr><td>13</td><td><span class="badge high">HIGH</span></td><td><code>ble_ota.h</code></td><td>BleOta 成员无锁,BLE GATT回调与主任务竞争</td><td>BLE OTA 数据损坏</td></tr>
<tr><td>14</td><td><span class="badge high">HIGH</span></td><td><code>debug_log.cpp:296</code></td><td>log() 栈分配 msg[512] + log_buf[600] = 1112字节</td><td>低栈任务中栈溢出</td></tr>
<tr><td>15</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:1008</code></td><td>spi_send_handler 栈分配 1728+ 字节</td><td>HTTP server栈仅8KB,接近溢出</td></tr>
<tr><td>16</td><td><span class="badge high">HIGH</span></td><td><code>ota_relay.cpp:456</code></td><td>sendNonceAndAuthenticate() nonce 仅用 esp_random() 一次</td><td>认证会话密钥可预测</td></tr>
<tr><td>17</td><td><span class="badge high">HIGH</span></td><td><code>ota_relay.h:25</code></td><td>ota_meta_t.checksum[32] 从未填充/验证</td><td>固件完整性无保障</td></tr>
<tr><td>18</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:429-444</code></td><td>ESP32 OTA 完成→重启,无回滚验证</td><td>新固件若无法启动将变砖</td></tr>
<tr><td>19</td><td><span class="badge high">HIGH</span></td><td><code>spi_master.cpp:258</code></td><td>syncTransfer/recv 忽略 timeout_ms 参数</td><td>SPI事务可能无限等待</td></tr>
<tr><td>20</td><td><span class="badge high">HIGH</span></td><td><code>wifi_ota.cpp:599-629</code></td><td>esp_ota_write 失败后 break,未调用 esp_ota_abort</td><td>OTA分区处于不一致状态</td></tr>
<tr><td>21</td><td><span class="badge high">HIGH</span></td><td><code>ble_ota.cpp:439-444</code></td><td>BLE OTA 帧预留4字节CRC但减去计算不匹配</td><td>最后几个字节被误截断</td></tr>
<tr><td>22</td><td><span class="badge high">HIGH</span></td><td><code>spi_master.h</code></td><td>SPI仅4线,无硬件流控/就绪信号</td><td>高速传输时GD32来不及处理</td></tr>
</tbody>
</table>
</div>
<!-- Architecture Concerns -->
<div class="section">
<div class="section-title"><span class="icon">&#127959;</span> 架构级隐患</div>
<div class="arch-card">
<h4>&#9312; 全局状态泛滥,无封装无同步</h4>
<p>项目中有 <strong>5个静态全局变量</strong> (relayState, relaySeq, relayTotalPackets, gd32AppAlive, gd32Version) 和 <strong>大量public成员</strong> (WifiOta/BleOta所有状态),跨越3+个FreeRTOS任务和多个ISR/callback上下文访问,<strong>没有任何一个</strong>被mutex/atomic/critical section保护。这是系统性问题,不是个别bug。</p>
</div>
<div class="arch-card">
<h4>&#9313; 单通道SPI + 多任务无仲裁</h4>
<p>SpiMaster 单例是唯一的SPI通信通道,但没有任何仲裁机制。WiFi/BLE/健康监控/OTA中继四个功能模块各自直接调用,协议级操作(请求-响应对)不是原子的。需要从架构层面引入SPI总线仲裁器。</p>
</div>
<div class="arch-card">
<h4>&#9314; 安全架构不完整</h4>
<p>认证握手存在但密钥硬编码→形同虚设;固件校验字段存在但未填充→形同虚设;CORS开放→任何人可触发OTA。<strong>安全功能"看起来有"但实际上不提供保护</strong>,这种"安全假象"比没有安全功能更危险,因为它给了虚假的信心。</p>
</div>
</div>
<!-- Priority Fix Order -->
<div class="section">
<div class="section-title"><span class="icon">&#128736;</span> 优先修复顺序</div>
<table class="priority-table">
<thead>
<tr><th>优先级</th><th>修复项</th><th>工作量</th><th>紧急度</th><th>进度</th></tr>
</thead>
<tbody>
<tr>
<td class="p0">P0</td>
<td>添加 SPI 总线互斥锁</td>
<td>小</td>
<td>极高 — 生产环境必崩</td>
<td><div class="progress-bar"><div class="fill critical-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p0">P0</td>
<td>替换硬编码密钥为安全存储</td>
<td>中</td>
<td>极高 — 安全漏洞</td>
<td><div class="progress-bar"><div class="fill critical-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p0">P0</td>
<td>Flash 写入对齐检查</td>
<td>小</td>
<td>极高 — 数据损坏</td>
<td><div class="progress-bar"><div class="fill critical-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>添加全局 OTA 超时+中止机制</td>
<td>中</td>
<td>高 — 设备挂死</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>ESP32 OTA 添加回滚验证</td>
<td>中</td>
<td>高 — 变砖风险</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>替换 XOR 为 CRC-8/16 校验</td>
<td>小</td>
<td>高 — 固件损坏</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p1">P1</td>
<td>消除 0xA5 值碰撞</td>
<td>极小</td>
<td>高 — 协议混乱</td>
<td><div class="progress-bar"><div class="fill high-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p2">P2</td>
<td>添加 OTA 单源互斥 (BLE/WiFi)</td>
<td>中</td>
<td>中高</td>
<td><div class="progress-bar"><div class="fill medium-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p2">P2</td>
<td>为所有跨任务状态添加锁/atomic</td>
<td>大</td>
<td>中高</td>
<td><div class="progress-bar"><div class="fill medium-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p2">P2</td>
<td>修复 WiFi 事件回调中的重连调用</td>
<td>小</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill medium-fill" style="width:0%"></div></div></td>
</tr>
<tr>
<td class="p3">P3</td>
<td>修复大栈分配问题</td>
<td>小</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill" style="width:0%;background:var(--text-muted);"></div></div></td>
</tr>
<tr>
<td class="p3">P3</td>
<td>限制 CORS 来源</td>
<td>小</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill" style="width:0%;background:var(--text-muted);"></div></div></td>
</tr>
<tr>
<td class="p3">P3</td>
<td>实现 ota_meta_t 校验填充与验证</td>
<td>中</td>
<td>中</td>
<td><div class="progress-bar"><div class="fill" style="width:0%;background:var(--text-muted);"></div></div></td>
</tr>
</tbody>
</table>
</div>
<!-- Dismissed -->
<div class="section">
<div class="section-title"><span class="icon">&#9989;</span> 已驳回发现(误报)</div>
<table>
<thead>
<tr><th>原始严重度</th><th>问题描述</th><th>驳回原因</th></tr>
</thead>
<tbody>
<tr>
<td><span class="badge critical" style="text-decoration:line-through;opacity:0.5;">CRITICAL</span></td>
<td>SPI DMA缓冲区栈分配导致数据损坏/硬Fault</td>
<td>ESP-IDF 6.0 的 spi_master 驱动内置 <code>setup_dma_priv_buffer()</code> 函数自动检测并对齐DMA缓冲区,栈分配仅导致性能损失,不引发数据损坏</td>
</tr>
<tr>
<td><span class="badge high" style="text-decoration:line-through;opacity:0.5;">HIGH</span></td>
<td>TOCTOU: relayState 检查后 reset() 无锁</td>
<td>虽然模式存在,但当前代码流程中检查和重置在同一线程同步执行,实际竞态窗口极小,风险被高估</td>
</tr>
</tbody>
</table>
</div>
<!-- Footer -->
<footer class="report-footer">
<p>ESP32_COMM 高风险代码评审报告 · 由 4维并行审查 + 36项对抗性验证 生成 · 2026-06-13</p>
<p style="margin-top:0.5rem;">审查维度: 内存安全 · 并发/RTOS · OTA/固件完整性 · SPI通信/协议</p>
</footer>
</div>
<script>
function toggleFinding(id) {
const card = document.getElementById(id);
if (card) card.classList.toggle('open');
}
let allHighOpen = false;
function toggleAll() {
allHighOpen = !allHighOpen;
document.querySelectorAll('.finding-card').forEach(card => {
if (allHighOpen) card.classList.add('open');
else card.classList.remove('open');
});
}
</script>
</body>
</html>
@@ -0,0 +1,74 @@
# ESP32_COMM 通信梳理(2026-05-05)
## 结论
当前“ESP32 ↔ 上位机(tools WebUI)”的主通信方式是 **HTTP**,不是 UDP。
### WebUI 实际在用的接口
- `GET /system/status`:每 1 秒轮询一次,用于在线状态、WiFi、heap、温度、OTA 状态展示
- `GET /logs`:每 1 秒轮询一次,用于拉取并清空日志环形缓冲区
- `POST /can/send`:发送 CAN 请求到 GD32
- `POST /ota/upload` / `POST /ota/relay` / `POST /ota/update`:OTA 相关
## 日志链路
所有 `DEBUG_*` 日志目前会并行写入三处:
1. **UART0**
- 本地串口调试
2. **UDP 单播** → `DEBUG_LOG_IP:DEBUG_LOG_PORT`
- 可供独立 UDP 调试接收端使用
3. **内部环形缓冲区**
- 被 `GET /logs` 读取
- 这是当前 WebUI 日志面板真正使用的通道
## 为什么 `{"t":"hb"}` 是多余的
之前在 `wifi_ota_task()` 里,每 3 秒会发送一次:
```json
{"t":"hb"}
```
它是通过 `debug_log_send()` 发出的,所以会同时进入:
- UART
- UDP
- `/logs` 对应的环形缓冲区
这会导致 WebUI 把它显示成一条普通日志:
- level: `I`
- tag: `ESP32`
- message: `{"t":"hb"}`
但 WebUI 本来已经每秒轮询 `/system/status`,完全可以判断设备在线,不需要再靠这种“伪日志心跳”。
## 已处理
已删除 `wifi_ota_task()` 中每 3 秒发送一次的 `{"t":"hb"}`。
## 现在是否还有重复/不必要通信
### 对 WebUI 来说
有两条并行链路,但用途不同,不算重复:
- `/system/status`:结构化状态
- `/logs`:文本日志
这两条都保留是合理的。
### 对底层日志输出来说
日志同时发 UART + UDP + ring buffer,属于“一份日志三路镜像”。
这对调试友好,但从“最简设计”角度看,**UDP 在当前 WebUI 场景里不是必需的**。
也就是说:
- **必须保留:** ring buffer + `/logs`
- **建议保留:** UART0
- **可选保留:** UDP 6666
如果后续你想继续精简,可以做“方案2”:把 UDP 也做成可关闭,甚至默认关闭。
@@ -0,0 +1,301 @@
# ESP32 软件架构扫描报告(2026-06-27)
## 背景
上一轮评审(2026-06-25,见 `docs/esp32_architecture_review_2026-06-25.md`)已完成 8 项优化:根目录清理、OtaCoordinator 状态查询单次取值、9 个 GD32 存储 wrapper inline 化、HTTP URI 路由表化、OtaRelay commonReject helper、SpiMaster *Unlocked 折叠、HealthMonitor RAII LockGuard、编译验证。
本轮扫描基于上述优化完成后的代码,新发现约 74 项可优化点,分三类:
- **新发现**(约 65 项):上轮完全未涉及
- **遗留**(3 项):上轮评审已提到但当时标"不在本次范围"
- **残留**(3 项):上轮已做但未完全覆盖
按风险/收益分 P0 / P1 / P2 三档。
---
## P0 优先(功能正确性 / 安全性 bug)
### P0-1 metaVerifyChecksum 全零跳过校验是隐性后门
- **文件**:[main/storage/gd32_flash_store.cpp:299-306](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L299-L306)
- **问题**:checksum 全零时直接返回 `ESP_OK`,注释"旧数据兼容"。meta 被意外清零时校验会"通过",掩盖固件损坏
- **建议**:返回 `ESP_ERR_INVALID_STATE`,由调用方决定是否容忍
### P0-2 RSSI 状态机 CRITICAL→NORMAL 无法降级
- **文件**:[main/monitor/health_monitor.cpp:333-357](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L333-L357)
- **问题**:"恢复到 NORMAL"只在 `cur == SYS_WARNING` 时触发,CRITICAL 状态下 RSSI 恢复正常不会降级
- **建议**:明确状态转换表,补 CRITICAL → NORMAL 直接降级路径
### P0-3 OTA_MAX_FIRMWARE_SIZE 注释与 0x36000 不一致
- **文件**:[main/include/app_config.h:62](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L62)
- **问题**:`216*1024 = 221184`,注释"对齐 GD32 APP_MAX_SIZE (0x36000)",但 `0x36000 = 221296`,相差 112 字节,并非对齐
- **建议**:确认正确值,用十六进制表达 `0x36000` 或修正注释
### P0-4 凭证硬编码入仓库
- **文件**:[main/include/app_config.h:31,48,112-115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L31)
- **问题**:WiFi 密码、AP 密码、MASTER_KEY 明文入仓库,README 也标注"⚠️ 占位值,生产环境需更换"
- **建议**:迁移到 NVS/Kconfig + env 文件,CI 加凭证扫描规则
### P0-5 BleEsp32FinishCtx 三处 free 易漏
- **文件**:[main/server/ble_ota.cpp:854,158,190,210](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L854)
- **问题**:ctx 在 3 个不同分支释放,`if (ctx) free(ctx)` 防御式写法暗示历史 bug;第 190 行后 `restartEsp32()` 不返回,理论上 free 不执行
- **建议**:RAII 封装 `BleFinishCtxDeleter`,任务入口 `BleFinishCtxDeleter guard{ctx};`,所有 return 路径自动释放
- **备注**:上轮评审 G 项已提到("不是精简,是健壮性,留后续"),本轮方案改为 RAII deleter(上轮原方案是改静态单例)
### P0-6 GD32 固件大小校验逻辑双通道重复
- **文件**:[main/server/ble_ota.cpp:1115-1121](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1115-L1121) vs [main/server/http_handlers.cpp:302-308](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L302-L308)
- **问题**:两处完全相同的校验:`uint32_t rounded = ((total + PKT - 1) / PKT) * PKT; if (total == 0 || total > MAX || rounded > MAX) {...}`,但 BLE 用 `BLE_OTA_FINISH_ERR_GD32_SIZE` 上报,HTTP 用 `HTTPD_500` 返回,错误码语义不对齐
- **建议**:抽取 `OtaCoordinator::validateGd32Size(total_size) -> esp_err_t`,两路调用方各自翻译为通道特定错误回报
### P0-7 doRelayPackets 180 行 + goto + 重试阈值混乱
- **文件**:[main/relay/ota_relay.cpp:936-1115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L936-L1115)
- **问题**:约 180 行,`for` → 内嵌 `while(1)` → 内嵌 `for` 重试 → 2 处 `goto resend_packet`(行 1057、1091);重试计数器阈值不一致:行 1015 `>=10U`、行 1032 `>=3U`、行 1050 `>=3U`、行 1075 `>=10U`、行 1084 `>=10U`,同一 `retry_count` 既限 3 又限 10
- **建议**:拆分为 `preparePacket(seq)` + `sendPacketWithRetry(seq, tx_buf)` + 外层 `doRelayPackets`;重试阈值提取常量 `kMaxResendOnNack`/`kMaxResendOnStructError`/`kMaxResendOnPollExhaust`
### P0-8 ble_ota_process_data_batch 245 行
- **文件**:[main/server/ble_ota.cpp:975-1219](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L975-L1219)
- **问题**:单函数处理 ESP32 与 GD32 两条独立数据流,ESP32 分支内嵌 6 个子逻辑,GD32 分支内嵌 9 个子逻辑,循环内多处 `offset += packet_len; if (offset + 7 > recv_len) break;` 重复推进
- **建议**:拆分为 `process_esp32_packet(...)` 与 `process_gd32_packet(...)`,主循环只负责解包 + 分发
---
## P1 优先(数据竞争 / 资源泄漏 / 双通道一致性)
### P1-A OtaRelay 状态成员封装破坏(一组原子改动)
**头文件**:[main/relay/ota_relay.h:228-237](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.h#L228-L237)
**问题**:把 `relayState/relaySeq/relayTotalPackets/gd32AppAlive/gd32Version/taskHandle/s_relayListener/s_stateMutex` 全部声明为 `public static`,但 cpp 内部多处直接访问绕过带锁的 setter/getter,与外部带锁读取形成数据竞争:
| 位置 | 直接访问 |
|---|---|
| [ota_relay.cpp:331-332](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L331-L332) | `handleSizeNeg` 直接写 `relayTotalPackets/relaySeq` 无锁 |
| [ota_relay.cpp:1102,1104-1105](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1102-L1105) | `doRelayPackets` 直接写读 `relaySeq` 无锁 |
| [ota_relay.cpp:1189,1197-1198,1209](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1189-L1209) | `doFinishAndVerify` 直接写读 `gd32AppAlive/gd32Version` 无锁 |
| [ota_relay.cpp:470,478](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L470-L478) | `forwardFirmware` 直接读 `relayState` 无锁 |
| [ota_relay.cpp:1272,1288](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1272) | `taskEntry` 直接访问 `s_relayListener` 绕过 getter |
**建议**:所有静态状态成员改 `private`,统一通过 setter/getter;relay task 内部若已持锁,在 `StateLockGuard` 块内访问。`s_stateMutex` 改 private,`StateLockGuard` 作为友元或保留 public 嵌套类。
### P1-9 getSnapshot 双重加锁且可能不一致
- **文件**:[main/ota/ota_coordinator.cpp:571-591](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L571-L591)
- **问题**:行 576-581 持锁读取 `m_sysState` 填入 `snap.sys_state` 后释放;行 582 调 `getSystemStateString()` 又内部调 `getSystemState()` 二次加锁。两次取值之间状态可能变化,导致 `snap.sys_state`(第一次取的值)与 `snap.sys_state_str`(第二次取的值)描述不同时刻状态,前端展示自相矛盾
- **建议**:让 `getSystemStateString` 接受 `ota_system_state_t` 参数(重载 `static const char* stateToString(ota_system_state_t)`),`getSnapshot` 持锁时一次性取值并就地转字符串
- **备注**:上轮 A.2 只改了 `isIdle/isGd32Busy/isEsp32Busy`,`getSnapshot` 未覆盖
### P1-10 getSystemStateString 等仍独立加锁
- **文件**:[main/ota/ota_coordinator.cpp:509-549](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L509-L549)
- **问题**:`isIdle`、`isGd32Busy`、`isEsp32Busy`、`getSystemStateString` 各自调 `getSystemState()`(加锁取值再解锁)。`if (!isIdle() && !isGd32Busy())` 这类连续调用会加锁 2~3 次
- **建议**:提供"取一次状态后多次判断"的接口,或让调用方先 `getSystemState()` 取一次再传给各判断函数
- **备注**:上轮 A.2 残留
### P1-11 esp32_ota_core finalize 3 处清理序列重复
- **文件**:[main/ota/esp32_ota_core.cpp:281-288,312-318,332-339](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L281-L339)
- **问题**:3 处几乎相同:`esp_ota_abort(m_handle); m_ended = false; releaseMutex(); resetState(); return xxx;`(SHA 计算失败 / SHA 不匹配 / esp_ota_end 失败)
- **建议**:提取私有方法 `finalizeCleanupAndReturn(esp_err_t code)`,或用 `goto cleanup`
### P1-12 esp32_ota_core begin 3 处清理序列重复
- **文件**:[main/ota/esp32_ota_core.cpp:126-130,135-141,151-158](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L126-L158)
- **问题**:3 处清理序列:分区找不到 / 固件太大 / `esp_ota_begin` 失败,模式高度相似,差异仅在是否清零 `m_handle` 和返回码
- **建议**:提取 `beginCleanupAndReturn(esp_err_t code)`,或统一出错点设置返回码后 `goto begin_fail`
### P1-13 doFinishAndVerify 100 行双职责
- **文件**:[main/relay/ota_relay.cpp:1118-1216](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1118-L1216)
- **问题**:单函数承担两个协议阶段:(1) 发送 FINISH_OTA 并等 ACK(1120-1176);(2) APP 存活轮询验证(1184-1213)。函数名含 "Verify",但两个阶段职责不同、超时参数不同、重试策略不同,行 1191 `for (attempt < 30)` 与 FINISH_OTA 的 `for (retry < 10)` 嵌套语义混淆
- **建议**:拆分为 `doSendFinishOta()` + `doWaitForAppAlive()`,`doFinishAndVerify` 改为顺序调用两者并做 session 清理
### P1-14 APP 存活探测模式三处重复
- **文件**:[main/relay/ota_relay.cpp:1191-1207,1240-1261](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1191-L1207) + [main/ota/ota_coordinator.cpp:596-615](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L596-L615)
- **问题**:同一"调 `SpiMaster::appPing` + 写 `gd32AppAlive`/`gd32Version` + 日志版本号"模式在 3 处重复:(1) `doFinishAndVerify` 轮询(30 次重试);(2) `taskEntry` 启动探测(1 次);(3) `OtaCoordinator::probeGd32AppOnce`(1 次)。日志格式、版本号字段打印、成功/失败处理高度相似但不完全相同
- **建议**:提取 `static void OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t* out_uptime)`,三处调用方分别指定重试次数和超时
### P1-15 setWifiConnected 未加锁
- **文件**:[main/monitor/health_monitor.cpp:320-323,248](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L320-L323)
- **问题**:直接写 `volatile bool m_wifiConnected`,未走 `m_mutex`。虽然 `volatile` 保证可见性,但与 `m_healthState` 等其他状态的访问模式不一致。`checkBeforeOta` 行 248 读 `m_wifiConnected` 也未加锁,存在读写竞态
- **建议**:统一通过 LockGuard 保护,或显式说明为何此字段例外(如使用 `std::atomic<bool>`)
- **备注**:上轮 F 给 HealthMonitor 加了 LockGuard,但 `m_wifiConnected` 漏了
### P1-16 温度 esp_timer_handle_t 是局部变量,资源无法回收
- **文件**:[main/monitor/health_monitor.cpp:182-194](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L182-L194)
- **问题**:`esp_timer_handle_t temp_timer` 是局部变量未存为成员。任务若被删除,定时器持续运行,回调仍会访问 `instance()` 的成员
- **建议**:将 `temp_timer` 存为成员,提供 `deinit()` 在任务删除时 `esp_timer_stop` + `esp_timer_delete`
### P1-17 metaUpdate 读-改-写非原子
- **文件**:[main/storage/gd32_flash_store.cpp:188-198](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L188-L198)
- **问题**:`metaLoad` → 修改 → `metaSave` 中间无锁,若两个任务同时调用 `metaUpdate`,会丢失更新
- **建议**:在 Gd32FlashStore 内增加 NVS 互斥锁,或将"由调用方串行化"约束以运行时断言强制
### P1-18 metaLoad NVS open 失败一律当 OK
- **文件**:[main/storage/gd32_flash_store.cpp:142-145](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L142-L145)
- **问题**:`nvs_open` 失败时直接 `return ESP_OK;`(注释说"命名空间不存在时返回全零结构"),但 NVS open 失败原因可能不止"不存在"(如 NVS 损坏),一律当 OK 会掩盖真实错误
- **建议**:区分 `ESP_ERR_NVS_NOT_FOUND`(OK + 全零)与其他错误(返回 err)
### P1-19 DebugLog init 不检查 uartInit 返回值
- **文件**:[main/monitor/debug_log.cpp:252-272](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L252-L272)
- **问题**:`uartInit();`(行 258)返回值被丢弃,UART 初始化失败仍设 `m_initialized = true` 并打印"调试日志已初始化"
- **建议**:检查返回值,失败时降级(仅 ring buffer)并在日志中标注 UART 不可用
### P1-20 ringDrainJson 容量估算与边界检查
- **文件**:[main/monitor/debug_log.cpp:116-151](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L116-L151)
- **问题**:`capacity = count * (LOG_RING_SIZE * 2) + 16`,count=64、LOG_RING_SIZE=256 → 最大 32784 字节,单次 malloc 在 ESP32-C3(~160KB heap)上风险较大;`for` 循环 `pos < capacity - 4` 的边界检查在 `first` 分支后逗号写入(行 133)未做边界检查,可能溢出
- **建议**:分块 realloc,或对单条日志超长时截断并标记
### P1-21 关键任务创建失败不停止初始化
- **文件**:[main/core/main.cpp:69-73,101-105,117-121,133-137](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L69-L137)
- **问题**:任务创建失败仅 `DEBUG_ERR`,不停止后续初始化,可能导致部分功能缺失但系统"看起来正常启动"
- **建议**:关键任务(health_mon、ota_relay)失败时 `ESP_ERROR_CHECK(ESP_FAIL)` 或进入 safe state
### P1-22 s_ota_recv_buf 全局静态被双 handler 共享
- **文件**:[main/server/http_handlers.cpp:36,249,337](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L36)
- **问题**:`static uint8_t s_ota_recv_buf[OTA_BLOCK_SIZE];` 在 ESP32 与 GD32 两条路径间共享。httpd 默认单线程不会并发,但若未来 `max_open_sockets > 1` 启用并发请求,两个 `ota_upload_handler` 同时跑会互相覆盖 buffer
- **建议**:改为 handler 内栈分配(若 `OTA_BLOCK_SIZE < 4KB` 可行),或改为 per-request 分配
### P1-23 一次性任务创建失败上报不一致
- **文件**:[main/server/ble_ota.cpp:816-821,875-878,896-899](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L816-L899)
- **问题**:
- ble_gd32_relay_wait_task 创建失败:`free(ctx); sendFinishEvent(ERR_GD32_RELAY, ...);` — 完整上报
- ble_esp32_finish_and_reboot_task 创建失败:`free(finish_ctx); m_esp32RebootPending = false;` — **未上报 FINISH 错误事件**,前端会一直等
- ble_esp32_restart_task 创建失败:`m_esp32RebootPending = false;` — **未上报任何错误**
- **建议**:统一"任务创建失败 → 释放 ctx → 复位 pending 标志 → 上报 FINISH 错误事件"三步
### P1-24 reorder slot malloc 失败静默丢弃
- **文件**:[main/server/ble_ota.cpp:513-518](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L513-L518)
- **问题**:malloc 失败时返回 `ESP_OK` 让上层认为已处理,但实际数据丢失。若客户端窗口已用完(无重传预算),固件写入会永久卡在缺口。注释说"靠客户端重传"但客户端无法感知设备端 malloc 失败
- **建议**:malloc 失败时至少发一次 `sendWindowAck` 报告当前 `expected`,让客户端立即重传缺口;或返回错误触发 abort
### P1-25 protocol.h 死代码
- **文件**:[main/transport/protocol.h:16-18,25-29](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/protocol.h#L16-L29) + [main/transport/protocol.cpp:96-118](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/protocol.cpp#L96-L118)
- **问题**:
1. `protocol.h` 仍定义 `FRAME_SOF 0xEB`、`FRAME_MAX_PAYLOAD 64`、`FRAME_OVERHEAD 6`,但 `protocol.cpp` 注释(第 8-10 行)明确说"帧构建、AES-CBC 加解密、CRC16 等中间件代码已移除",这三个常量无任何引用
2. `session_ctx_t.iv[16]` 在 `sessionKeyInit` 中被 `memset(iv, 0, 16)` 后再无任何写入;`session_ctx_t.used` 初始化为 0 后再无任何代码置 1。整个 `session_ctx_t` 实际只用到 `key[16]`
- **建议**:删除 `FRAME_SOF`/`FRAME_MAX_PAYLOAD`/`FRAME_OVERHEAD`;精简 `session_ctx_t` 为 `{ uint8_t key[16]; }` 或整体移除
### P1-26 GD32 OTA 写入序列两路径重复
- **文件**:[main/server/ble_ota.cpp:1113-1208](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1113-L1208)(BLE)vs [main/server/http_handlers.cpp:314-413](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L314-L413)(HTTP)
- **问题**:两路径都执行相同序列:`beginGd32Storage` → `saveGd32Meta` → 循环 `writeGd32Chunk` + `updateGd32Meta` → `completeGd32Storage` + `finalizeGd32Checksum`。但 HTTP 路径多了 `eraseGd32Partition` 和 `clearGd32Meta`(行 327、335),BLE 路径没有。这暗示 BLE 路径可能依赖前次 HTTP 上传后的已擦除状态,或在 `beginGd32Storage` 内隐式擦除——这种隐式假设是 bug 温床
- **建议**:抽取 `OtaCoordinator::storeGd32FirmwareFromStream(...)` 接收"读块回调",BLE 与 HTTP 各自实现回调,统一擦除/写入/meta/完成序列
---
## P2 优先(样板抽取 / 配置化 / 风格统一)
### P2-A 跨模块样板抽取(高 ROI)
| # | 主题 | 涉及位置 |
|---|---|---|
| P2-27 | Hex 编解码工具统一 | [http_handlers.cpp:460-463,558-562,629-633](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L460-L633)、[ble_ota.cpp:174-178,642-649,861-863](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L174-L863) 共 6+ 处。**关键 bug**:SHA-256 在 BLE 路径大小写混用(`shaBytesToHexUpper` 大写 vs 行 861-863 `%02x` 小写),HTTP 路径用大写 `%02X`,前端比对 SHA 时大小写敏感会出错 |
| P2-28 | SHA-256 → hex 循环 | [esp32_ota_core.cpp:230-234,254-257,290-293,323-325](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/esp32_ota_core.cpp#L230-L325) 4 处 + [gd32_flash_store.cpp:283-286,316-320](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L283-L320) 2 处。提取 `static void sha256ToHex(const uint8_t in[32], char out[65])` |
| P2-29 | HTTP JSON body 接收+解析 | [http_handlers.cpp:71-95,436-452,522-535,595-608](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L71-L608) 4 处。缓冲区上限不一(256/512)。提取 `http_recv_json(httpd_req_t*, char*, size_t, cJSON**)` |
| P2-30 | HTTP 错误响应样板 | [http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp) 17 处 `httpd_resp_send_err + return ESP_FAIL`。提取 `http_send_json_error/http_send_json_ok` |
| P2-31 | 忙状态判断 if 链 | [ota_coordinator.cpp:136-140](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L136-L140)(beginGd32Storage) vs [ota_coordinator.cpp:346-350](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L346-L350)(beginEsp32Ota)。提取 `bool isAnyChannelBusy() const` |
| P2-32 | 三个校验函数重复 metaLoad 样板 | [gd32_flash_store.cpp:265-275,292-298,331-351](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L265-L351) |
### P2-B 函数过长 / 高圈复杂度
| # | 函数 | 行数 | 文件 |
|---|---|---|---|
| P2-33 | ble_ota_process_data_batch | 245 行 | [ble_ota.cpp:975-1219](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L975-L1219)(见 P0-8) |
| P2-34 | ota_upload_handler | 240 行 | [http_handlers.cpp:180-420](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L180-L420)。ESP32/GD32 双路径混杂,共享 `pre_read_buf`/`pre_read_len`。拆为 `handle_esp32_upload`/`handle_gd32_upload` |
| P2-35 | ble_ota_ctrl_access | 190 行 | [ble_ota.cpp:717-907](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L717-L907)。3 命令分支长度极不均(0xF7 90 行 / 0xF8 54 行 / 0xFA 19 行),`session_id` 提取重复 |
| P2-36 | doRelayPackets | 180 行 + goto | [ota_relay.cpp:936-1115](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L936-L1115)(见 P0-7) |
| P2-37 | doFinishAndVerify | 100 行双职责 | [ota_relay.cpp:1118-1216](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1118-L1216)(见 P1-13) |
| P2-38 | app_main | 124 行 | [main.cpp:30-154](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L30-L154)。拆为 `initEarlyServices` + `createOtaTasks` + `markAppValid` |
### P2-C 配置化(magic number → app_config.h)
| # | 主题 | 位置 |
|---|---|---|
| P2-39 | 任务栈/优先级散落硬编码 | [main.cpp:63,65,95,97,111,113,127,129](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L63)、[ble_ota.cpp:812,871,892,1723](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L812)、[wifi_ota.cpp:623](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L623)、[http_handlers.cpp:905-913](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L905-L913)(task_metrics 必须手工同步,已出现一处不对齐风险) |
| P2-40 | HTTP server config | [wifi_ota.cpp:419-424](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L419-L424)(`max_uri_handlers=32` 实际只用 18) |
| P2-41 | BLE 连接参数 | [ble_ota.cpp:1395-1401](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L1395-L1401)(itvl/latency/supervision) |
| P2-42 | AP 静态 IP | [wifi_ota.cpp:234-236,240,258-259](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/wifi_ota.cpp#L234-L259) |
| P2-43 | BLE_OTA_ACK_WINDOW 位置 | [ble_ota.cpp:58](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L58) 用 `#define` 应移入 app_config.h 作 `inline constexpr` |
| P2-44 | debug_log magic number | [debug_log.cpp:199,317,352,365,369](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L199)(1024/512/600/32) |
| P2-45 | 健康巡检周期 | [health_monitor.cpp:190,208](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L190)(1000000/5000) |
| P2-46 | 温度采样间隔 | [health_monitor.cpp:190](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L190) |
### P2-D 死代码 / 未使用配置
| # | 项 | 位置 |
|---|---|---|
| P2-47 | `DEBUG_LOG_ENABLED` 全代码库零引用 | [app_config.h:83](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L83) |
| P2-48 | `GD32_CHIP_UID[]` 占位 | [app_config.h:120-128](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L120-L128) |
| P2-49 | `BLE_OTA_PACKET_OVERHEAD` 零引用 | [app_config.h:104](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L104) |
| P2-50 | `WIFI_RSSI_WEAK` 零引用 | [health_monitor.h:33](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L33) |
| P2-51 | `DEBUG_LOG_NONE/ALL` 零引用 | [debug_log.h:35,40](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L35) |
| P2-52 | `WIFI_STA_USE_STATIC_IP` 临时调试开关 | [app_config.h:36](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L36) |
| P2-53 | `ota_snapshot.h` 仅聚合无价值 | [ota_snapshot.h](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_snapshot.h) 全文 |
### P2-E 其他低风险风格类
| # | 项 | 位置 |
|---|---|---|
| P2-54 | SpiMaster 公共接口混合多个抽象层 | [spi_master.h:60-100](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.h#L60-L100)(上轮 D 残留) |
| P2-55 | DebugLog 未用 RAII(与 HealthMonitor 风格不一) | [debug_log.cpp:53-60,78-108,162-174](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L53-L174) |
| P2-56 | DebugLog logLevel 暴露在 public | [debug_log.h:86](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L86) |
| P2-57 | DebugLog 三路 sink 抽象为列表 | [debug_log.cpp:300-333](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L300-L333)(上轮 E 项遗留) |
| P2-58 | DebugLog send() 返回值无人检查,可改 void | [debug_log.cpp:329-332](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L329-L332) |
| P2-59 | ensureUdpSocket 失败后每条日志都重试 | [debug_log.cpp:278-292](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L278-L292) |
| P2-60 | DebugLog log() 三次缓冲区拼接 | [debug_log.cpp:342-373](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L342-L373) |
| P2-61 | DebugLog 头注释与实现不一致(UDP 无开关) | [debug_log.h:6-7](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L6-L7) vs [debug_log.cpp:311-324](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L311-L324) |
| P2-62 | DebugLog 重复 include | [debug_log.h:23-24](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.h#L23-L24) vs [debug_log.cpp:20,23](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/debug_log.cpp#L20) |
| P2-63 | health_monitor WIFI_RSSI_NORMAL 别名位置错误 | [health_monitor.h:32-33](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L32-L33) |
| P2-64 | health_state_t 与 health_status_t 两套枚举语义重叠 | [health_monitor.h:16-29](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.h#L16-L29) |
| P2-65 | health_monitor 阈值检查样板可抽 applyThresholdAlarm | [health_monitor.cpp:129-156,333-357](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L129-L357) |
| P2-66 | health_monitor checkBeforeOta 与 checkBeforeOtaRelay 分级样板重复 | [health_monitor.cpp:232-311](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L232-L311) |
| P2-67 | health_monitor taskEntry 职责混杂 | [health_monitor.cpp:165-210](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L165-L210) |
| P2-68 | health_monitor logEvent 与相邻 DEBUG 重复日志 | [health_monitor.cpp:220-223](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L220-L223) |
| P2-69 | health_monitor tempMonitorCallback 注释硬编码温度值 | [health_monitor.cpp:293](file:///g:/Robot_v2/DCU/ESP32_COMM/main/monitor/health_monitor.cpp#L293) |
| P2-70 | gd32_flash_store NVS 字段名硬编码 | [gd32_flash_store.cpp:136-185](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L136-L185) |
| P2-71 | gd32_flash_store tag "OTA" 与模块名不一致 | [gd32_flash_store.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp) 全文 |
| P2-72 | gd32_flash_store SHA 长度 32 裸数字 | [gd32_flash_store.h:26](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.h#L26) 等 |
| P2-73 | gd32_flash_store flashWriteChunk 错误清理重复 | [gd32_flash_store.cpp:74-108](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L74-L108) |
| P2-74 | gd32_flash_store computeFlashSha256 错误清理重复 | [gd32_flash_store.cpp:224-262](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L224-L262) |
| P2-75 | gd32_flash_store 全零 checksum 检查可简化为 memcmp | [gd32_flash_store.cpp:299-302](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L299-L302) |
| P2-76 | gd32_flash_store flashWriteChunk 非对齐警告条件恒真 | [gd32_flash_store.cpp:65-72](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L65-L72) |
| P2-77 | gd32_flash_store m_partition 懒初始化无线程安全 | [gd32_flash_store.cpp:30-40](file:///g:/Robot_v2/DCU/ESP32_COMM/main/storage/gd32_flash_store.cpp#L30-L40) |
| P2-78 | main.cpp 4 处 xTaskCreatePinnedToCore 样板 | [main.cpp:60-137](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L60-L137) |
| P2-79 | main.cpp 启动横幅分隔线硬编码 4 次 | [main.cpp:50,56,149,153](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L50-L153) |
| P2-80 | main.cpp xRet 变量重复使用 | [main.cpp:59,69,92,101,108,117,124,133](file:///g:/Robot_v2/DCU/ESP32_COMM/main/core/main.cpp#L59-L133) |
| P2-81 | http_handlers.cpp 单文件 TAG 混用 OTA/WIFI/SPI | [http_handlers.cpp](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp) 全文 |
| P2-82 | canFdSend 返回 -1 无法区分失败模式 | [canfd_bridge.cpp:59-68](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/canfd_bridge.cpp#L59-L68) |
| P2-83 | otaCrc8 实为 XOR 校验和,命名误导 | [spi_master.cpp:180-187](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L180-L187) |
| P2-84 | otaParseFrame/otaFindFrame 验证逻辑重复 | [spi_master.cpp:225-279](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L225-L279) |
| P2-85 | CAN 帧 vs SPI OTA 帧两层 framing 未抽象 | [canfd_bridge.cpp:12-51](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/canfd_bridge.cpp#L12-L51) + [spi_master.cpp:198-279](file:///g:/Robot_v2/DCU/ESP32_COMM/main/transport/spi_master.cpp#L198-L279) |
| P2-86 | BLE OTA WiFi 共存标志位冗余 | [ble_ota.cpp:226-250](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/ble_ota.cpp#L226-L250) |
| P2-87 | http_handlers task_metrics 硬编码任务栈大小 | [http_handlers.cpp:905-913](file:///g:/Robot_v2/DCU/ESP32_COMM/main/server/http_handlers.cpp#L905-L913) |
| P2-88 | notifyRelayStarted/Complete 纯转发中间层 | [ota_coordinator.cpp:264-307](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L264-L307) |
| P2-89 | notifyEsp32FinalizeComplete 与 notifyEsp32Rebooting 几乎相同 | [ota_coordinator.cpp:409-428](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L409-L428) |
| P2-90 | doFinishAndVerify 成功路径与 reset() session 清理重复 | [ota_relay.cpp:1178-1182 vs 487-504](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1178-L1182) |
| P2-91 | metaLoad 单次中继被调 3 次 | [ota_coordinator.cpp:213-221](file:///g:/Robot_v2/DCU/ESP32_COMM/main/ota/ota_coordinator.cpp#L213-L221) + [ota_relay.cpp:311-316,438-440](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L311-L440) |
| P2-92 | forwardFirmware 中 expected_ack=0xF0F0F0F0 魔法数字 | [ota_relay.cpp:1024](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L1024) |
| P2-93 | doRelayPackets:918 WARN 级别输出纯配置信息(调试残留) | [ota_relay.cpp:918](file:///g:/Robot_v2/DCU/ESP32_COMM/main/relay/ota_relay.cpp#L918) |
| P2-94 | app_config 配置项类型不一致(uint8_t 应为 bool) | [app_config.h:25,36,83](file:///g:/Robot_v2/DCU/ESP32_COMM/main/include/app_config.h#L25) |
---
## 不建议改的地方(保留上轮结论)
- OTA 事件状态机的 5 步驱动顺序、健康检查时机、`OtaCoordinator` 的 watchdog 自愈、`forceCleanup` 仅在 `isIdle()` 启动。
- `Esp32OtaCore::s_mutex + s_occupied` 双层 mutex 语义("持锁查占用" + "持续占用期")。
- 单例模式已统一(`static XXX inst; return inst;` + `= delete` 拷贝构造/赋值)。
- 无 TODO/FIXME/XXX/HACK 标记,无注释掉的死代码。
---
## 预估收益
- 修复 4 项功能性 bug(校验后门 / RSSI 状态机降级 / 固件大小注释 / 凭证安全)。
- 消除 6 处数据竞争(OtaRelay 状态访问 + setWifiConnected)。
- 6 个超长函数拆分(245/240/190/180/124/100 行),可维护性显著提升。
- `main/` 行数预计再减约 400~500 行(去重 + 死代码)。
- 双通道(BLE/HTTP)一致性通过 `validateGd32Size` / `storeGd32FirmwareFromStream` 抽取彻底对齐。
- protocol.h / app_config.h 死代码清理约减 50+ 行。
---
## 推荐落地顺序
1. **第 1 批(P0)**:P0-1 ~ P0-8。先修功能正确性 / 安全性 bug。P0-7/P0-8 函数拆分风险高,需配合 GD32 全链路 OTA 回归。
2. **第 2 批(P1.A)**:OtaRelay 状态成员封装一组(P1-A + P1-15),消除数据竞争,原子改动。
3. **第 3 批(P1.B)**
4. **第 4 批(P2.A/B)**:样板抽取(P2-27 ~ P2-32)+ 函数拆分(P2-33 ~ P2-38)。
5. **第 5 批(P2.C/D/E)**:配置化(P2-39 ~ P2-46)+ 死代码删除(P2-47 ~ P2-53)+ 风格统一(P2-54 ~ P2-94),可随业务变更顺手做。