Files
ESP32_COMM/docs/wifi_cold_boot_fix_plan.html
T
小龙虾 5b2c5f8ff9 docs(README): 合并WiFi OTA和BLE OTA
- 调整ESP32_COMM/finalize函数执行顺序:先进行SHA-256校验比对,
  再调用esp_ota_end和设置启动分区
- 更新BLE传输收尾描述,明确设备端读回OTA分区进行SHA比对的流程,
  以及FINISH事件的上报机制
2026-06-16 23:17:06 +08:00

1150 lines
32 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.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32 WiFi 冷启动连接失败排查与修复计划</title>
<style>
:root {
--ink: #161616;
--muted: #5f6267;
--line: #d8d0c2;
--paper: #f7f4ee;
--panel: #fffdf8;
--panel-2: #f0eee6;
--accent: #b63f2f;
--accent-2: #1f7a69;
--accent-3: #7b5a1e;
--ok: #24735c;
--warn: #a05a16;
--danger: #a73327;
--code-bg: #25211b;
--code-ink: #f5ead9;
--shadow: 0 14px 32px rgba(40, 34, 26, 0.11);
--radius: 8px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
color: var(--ink);
background:
linear-gradient(90deg, rgba(22, 22, 22, 0.035) 1px, transparent 1px),
linear-gradient(180deg, rgba(22, 22, 22, 0.03) 1px, transparent 1px),
var(--paper);
background-size: 36px 36px;
font-family: "Microsoft YaHei UI", "Microsoft YaHei", "Source Han Sans SC", sans-serif;
line-height: 1.65;
}
a {
color: inherit;
}
.page {
width: min(1180px, calc(100% - 40px));
margin: 0 auto;
padding: 30px 0 56px;
}
header {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 22px;
align-items: stretch;
min-height: 360px;
border-bottom: 2px solid var(--ink);
padding-bottom: 22px;
}
.hero {
position: relative;
min-height: 360px;
padding: 32px;
color: #fff9ec;
background:
linear-gradient(135deg, rgba(18, 15, 12, 0.86), rgba(69, 45, 31, 0.74)),
repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 14px),
radial-gradient(circle at 20% 30%, #b63f2f 0 12%, transparent 13%),
#2f2a22;
overflow: hidden;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.hero::before,
.hero::after {
content: "";
position: absolute;
inset: auto;
border: 1px solid rgba(255, 249, 236, 0.34);
transform: rotate(-8deg);
pointer-events: none;
}
.hero::before {
width: 380px;
height: 180px;
right: -90px;
top: 36px;
}
.hero::after {
width: 520px;
height: 120px;
left: -120px;
bottom: 28px;
}
.hero-content {
position: relative;
z-index: 1;
max-width: 760px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 22px;
color: #f2c28e;
font-size: 13px;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 30px;
height: 2px;
background: #f2c28e;
}
h1 {
margin: 0;
font-family: "Cambria", "STSong", serif;
font-size: clamp(34px, 5vw, 70px);
line-height: 1;
letter-spacing: 0;
max-width: 820px;
}
.hero p {
max-width: 710px;
margin: 22px 0 0;
font-size: 17px;
color: #f5ead9;
}
.hero-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 30px;
}
.stamp {
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 6px 10px;
border: 1px solid rgba(255, 249, 236, 0.38);
background: rgba(255, 249, 236, 0.1);
border-radius: 4px;
color: #fff9ec;
font-size: 13px;
font-weight: 700;
}
.summary-panel {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 16px;
padding: 22px;
background: var(--panel);
border: 2px solid var(--ink);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.summary-panel h2 {
margin: 0 0 10px;
font-size: 18px;
}
.summary-panel p {
margin: 0;
color: var(--muted);
font-size: 14px;
}
.metric-grid {
display: grid;
gap: 10px;
}
.metric {
padding: 13px;
border: 1px solid var(--line);
background: var(--panel-2);
border-radius: 6px;
}
.metric strong {
display: block;
font-size: 24px;
line-height: 1.1;
}
.metric span {
display: block;
margin-top: 4px;
color: var(--muted);
font-size: 12px;
}
nav {
position: sticky;
top: 0;
z-index: 10;
display: flex;
gap: 8px;
overflow-x: auto;
margin: 18px 0 30px;
padding: 10px;
background: rgba(247, 244, 238, 0.94);
backdrop-filter: blur(12px);
border: 1px solid var(--line);
border-radius: var(--radius);
}
nav a {
flex: 0 0 auto;
padding: 8px 11px;
border: 1px solid transparent;
border-radius: 5px;
color: var(--muted);
font-size: 13px;
font-weight: 700;
text-decoration: none;
}
nav a:hover {
color: var(--ink);
border-color: var(--ink);
background: var(--panel);
}
section {
margin-top: 34px;
}
.section-head {
display: flex;
justify-content: space-between;
gap: 18px;
align-items: end;
margin-bottom: 14px;
border-bottom: 2px solid var(--ink);
padding-bottom: 10px;
}
.section-head h2 {
margin: 0;
font-family: "Cambria", "STSong", serif;
font-size: 30px;
line-height: 1.12;
}
.section-head p {
max-width: 520px;
margin: 0;
color: var(--muted);
font-size: 14px;
text-align: right;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.panel {
padding: 18px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: 0 8px 18px rgba(40, 34, 26, 0.06);
}
.panel h3 {
margin: 0 0 10px;
font-size: 17px;
}
.panel p,
.panel li {
color: var(--muted);
font-size: 14px;
}
.panel ul {
margin: 10px 0 0;
padding-left: 20px;
}
.root-cause {
width: 100%;
border-collapse: collapse;
overflow: hidden;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
th,
td {
padding: 12px 13px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
font-size: 14px;
}
th {
color: #fff9ec;
background: #2b2822;
font-size: 12px;
text-transform: uppercase;
}
tr:last-child td {
border-bottom: 0;
}
td code,
li code,
p code {
padding: 2px 5px;
color: var(--code-ink);
background: var(--code-bg);
border-radius: 4px;
font-family: "Cascadia Mono", "Consolas", monospace;
font-size: 0.92em;
}
.priority {
display: inline-flex;
min-width: 38px;
justify-content: center;
padding: 2px 8px;
border-radius: 4px;
color: #fff;
font-weight: 800;
font-size: 12px;
}
.p0 { background: var(--danger); }
.p1 { background: var(--warn); }
.p2 { background: var(--accent-2); }
.timeline {
display: grid;
gap: 12px;
}
.phase {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.phase-label {
display: flex;
flex-direction: column;
justify-content: center;
padding: 16px;
color: #fff9ec;
background: #2b2822;
}
.phase-label strong {
font-size: 24px;
line-height: 1;
}
.phase-label span {
margin-top: 6px;
color: #d5c7b2;
font-size: 12px;
}
.phase-body {
padding: 16px;
}
.phase-body h3 {
margin: 0 0 6px;
font-size: 17px;
}
.phase-body p {
margin: 0;
color: var(--muted);
font-size: 14px;
}
.phase-body .tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.tag {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 3px 8px;
background: #eee3d0;
border: 1px solid #d3c4ad;
border-radius: 4px;
color: #4d4131;
font-size: 12px;
font-weight: 700;
}
.tasks {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.task {
position: relative;
padding: 16px 16px 16px 52px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
}
.task input {
position: absolute;
left: 16px;
top: 19px;
width: 20px;
height: 20px;
accent-color: var(--accent-2);
}
.task h3 {
margin: 0 0 8px;
font-size: 16px;
}
.task p {
margin: 0 0 10px;
color: var(--muted);
font-size: 14px;
}
.task small {
display: block;
color: var(--accent-3);
font-family: "Cascadia Mono", "Consolas", monospace;
font-size: 12px;
overflow-wrap: anywhere;
}
.task:has(input:checked) {
background: #edf5ef;
border-color: #9fbcac;
}
.task:has(input:checked) h3,
.task:has(input:checked) p {
text-decoration: line-through;
text-decoration-thickness: 1px;
text-decoration-color: rgba(36, 115, 92, 0.45);
}
.checklist {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.checkitem {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: 10px;
align-items: start;
padding: 14px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
}
.checkitem .mark {
display: grid;
place-items: center;
width: 26px;
height: 26px;
color: #fff;
background: var(--accent);
border-radius: 4px;
font-weight: 800;
}
.checkitem strong {
display: block;
margin-bottom: 4px;
}
.checkitem span {
color: var(--muted);
font-size: 14px;
}
.matrix {
width: 100%;
border-collapse: collapse;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
.matrix td:nth-child(1) {
width: 30%;
font-weight: 800;
}
.matrix td:nth-child(3) {
color: var(--ok);
font-weight: 700;
}
.risk-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.risk {
padding: 14px;
background: var(--panel);
border: 1px solid var(--line);
border-top: 4px solid var(--accent);
border-radius: var(--radius);
}
.risk h3 {
margin: 0 0 8px;
font-size: 15px;
}
.risk p {
margin: 0;
color: var(--muted);
font-size: 13px;
}
.code-block {
overflow-x: auto;
margin: 0;
padding: 16px;
color: var(--code-ink);
background: var(--code-bg);
border-radius: var(--radius);
font-family: "Cascadia Mono", "Consolas", monospace;
font-size: 13px;
line-height: 1.55;
}
.footer-note {
margin-top: 36px;
padding: 16px;
color: var(--muted);
background: rgba(255, 253, 248, 0.72);
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: 13px;
}
.toolbar {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 16px;
}
button {
min-height: 34px;
padding: 7px 11px;
color: #fff9ec;
background: #2b2822;
border: 0;
border-radius: 5px;
font-weight: 800;
cursor: pointer;
}
button:hover {
background: var(--accent);
}
@media (max-width: 880px) {
.page {
width: min(100% - 24px, 1180px);
padding-top: 18px;
}
header,
.grid-2,
.tasks,
.checklist,
.risk-grid {
grid-template-columns: 1fr;
}
header {
min-height: 0;
}
.hero {
min-height: 320px;
padding: 24px;
}
.section-head {
display: block;
}
.section-head p {
margin-top: 8px;
text-align: left;
}
.phase {
grid-template-columns: 1fr;
}
.phase-label {
flex-direction: row;
justify-content: flex-start;
gap: 12px;
}
.root-cause,
.matrix {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
@media print {
body {
background: #fff;
}
nav,
button,
.toolbar {
display: none;
}
.page {
width: 100%;
padding: 0;
}
header,
.panel,
.task,
.phase,
.risk,
.summary-panel {
box-shadow: none;
}
section {
break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="hero">
<div class="hero-content">
<div class="eyebrow">ESP32-C3 OTA Relay / WiFi Incident Plan</div>
<h1>WiFi 冷启动连接失败排查与系统化修复计划</h1>
<p>
面向长时间断电后无法连接 STA 的问题,按“复现证据 → 最小诊断 → P0 修复 → 运行期恢复 → 验证矩阵”的顺序推进,避免一次性大改掩盖真实根因。
</p>
<div class="hero-meta">
<span class="stamp">目标:冷启动 STA 稳定联网</span>
<span class="stamp">范围:WiFi / NVS / PHY / Watchdog / SPI mutex</span>
<span class="stamp">项目:G:\Robot_v2\DCU\ESP32_COMM</span>
</div>
</div>
</div>
<aside class="summary-panel" aria-label="计划摘要">
<div>
<h2>执行摘要</h2>
<p>
当前判断是多因素叠加:冷启动擦 PHY 校准、连接窗口偏短、重试耗尽后恢复不足、运行中丢 IP 或断线缺少闭环处理。
</p>
</div>
<div class="metric-grid">
<div class="metric">
<strong>8</strong>
<span>修复与验证任务</span>
</div>
<div class="metric">
<strong>P0</strong>
<span>优先处理 PHY 与重试耗尽</span>
</div>
<div class="metric">
<strong>5 min</strong>
<span>单次冷启动日志采集窗口</span>
</div>
</div>
</aside>
</header>
<nav aria-label="页面导航">
<a href="#root-cause">根因</a>
<a href="#evidence">证据</a>
<a href="#phases">阶段</a>
<a href="#tasks">任务</a>
<a href="#verification">验证</a>
<a href="#risks">风险</a>
<a href="#commands">命令</a>
</nav>
<section id="root-cause">
<div class="section-head">
<h2>根因优先级</h2>
<p>先处理能直接解释“长时间断电后首连失败”和“失败后不恢复”的路径。</p>
</div>
<table class="root-cause">
<thead>
<tr>
<th>优先级</th>
<th>根因</th>
<th>位置</th>
<th>影响</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="priority p0">P0</span></td>
<td>冷启动擦 PHY 校准 + 仅 500ms 延迟</td>
<td><code>wifi_ota.cpp L1524-L1531</code>, <code>L340-L342</code></td>
<td>全量 PHY 校准可能需要更长时间,反复擦除会让首连更不稳定。</td>
</tr>
<tr>
<td><span class="priority p0">P0</span></td>
<td>重试耗尽标志运行中不恢复</td>
<td><code>wifi_ota.cpp L196-L208</code></td>
<td><code>m_staRetryExhausted=true</code> 后可能长期阻止后续重连。</td>
</tr>
<tr>
<td><span class="priority p1">P1</span></td>
<td><code>listen_interval=3</code> 偏短</td>
<td><code>wifi_ota.cpp L324</code></td>
<td>长时间空闲或 AP 策略严格时,STA 更容易被驱逐。</td>
</tr>
<tr>
<td><span class="priority p1">P1</span></td>
<td>无 <code>IP_EVENT_STA_LOST_IP</code> 处理</td>
<td><code>wifi_ota.cpp L228-L242</code></td>
<td>设备可能已经无 IP,但内部仍认为已连接。</td>
</tr>
<tr>
<td><span class="priority p1">P1</span></td>
<td>主循环无 WiFi 断线看门狗</td>
<td><code>wifi_ota.cpp L1679-L1707</code></td>
<td>运行期断线后缺少强制恢复策略。</td>
</tr>
<tr>
<td><span class="priority p2">P2</span></td>
<td>断线原因码未解码</td>
<td><code>wifi_ota.cpp L201</code></td>
<td>日志只有数字,难以区分认证、AP 不存在、握手超时、AP 驱逐。</td>
</tr>
<tr>
<td><span class="priority p2">P2</span></td>
<td>SpiMaster mutex 空指针风险</td>
<td><code>spi_master.cpp L288-L290</code></td>
<td>先 lock 空 mutex 再创建 mutex,属于明确初始化顺序问题。</td>
</tr>
<tr>
<td><span class="priority p2">P2</span></td>
<td>NVS 初始化错误处理不完整</td>
<td><code>wifi_ota.cpp L1492-L1508</code></td>
<td>只处理两个错误码,其他 NVS 异常缺少统一恢复路径。</td>
</tr>
</tbody>
</table>
</section>
<section id="evidence">
<div class="section-head">
<h2>复现证据</h2>
<p>本阶段只补充诊断能力和采集日志,不改变核心连接策略。</p>
</div>
<div class="grid-2">
<div class="panel">
<h3>串口日志必抓字段</h3>
<ul>
<li><code>复位原因</code>: POWER_ON / BROWNOUT / SOFTWARE / 其他</li>
<li><code>冷启动</code> 是否成立,是否擦除 WiFi NVS 或 PHY</li>
<li><code>STA 已启动</code>、<code>STA 已关联到 AP</code>、<code>STA 获取 IP</code></li>
<li><code>STA 断开</code> 的 reason code 与可读原因</li>
<li><code>STA 重试已耗尽</code> 和是否切换 AP</li>
</ul>
</div>
<div class="panel">
<h3>路由器侧同步检查</h3>
<ul>
<li>ESP32 MAC 是否出现在关联列表</li>
<li>是否产生 DHCP 租约,租约 IP 是否变化</li>
<li>AP 日志中是否有 deauth / disassoc / handshake timeout</li>
<li>路由器冷启动时 WiFi 和 DHCP 服务恢复耗时</li>
</ul>
</div>
</div>
</section>
<section id="phases">
<div class="section-head">
<h2>实施阶段</h2>
<p>从可观测性开始,再进入 P0/P1 修复,最后做覆盖测试。</p>
</div>
<div class="timeline">
<article class="phase">
<div class="phase-label"><strong>01</strong><span>Baseline</span></div>
<div class="phase-body">
<h3>复现与日志基线</h3>
<p>冷启动抓 5 分钟串口日志,同时记录路由器关联和 DHCP 日志,确认失败发生在关联前、关联后取 IP、还是运行中断线。</p>
<div class="tags">
<span class="tag">serial_monitor.py</span>
<span class="tag">reset reason</span>
<span class="tag">router log</span>
</div>
</div>
</article>
<article class="phase">
<div class="phase-label"><strong>02</strong><span>Observe</span></div>
<div class="phase-body">
<h3>增加最小诊断能力</h3>
<p>解码断线 reason,补充 retry、exhausted、heap、uptime 和 WiFi API 返回值日志。</p>
<div class="tags">
<span class="tag">wifiReasonToStr</span>
<span class="tag">heap</span>
<span class="tag">uptime</span>
</div>
</div>
</article>
<article class="phase">
<div class="phase-label"><strong>03</strong><span>P0 Fix</span></div>
<div class="phase-body">
<h3>冷启动与重试耗尽修复</h3>
<p>冷启动 PHY 延迟提升到 1500ms;移除无条件 PHY 擦除;重试耗尽增加 30s 冷却恢复机制。</p>
<div class="tags">
<span class="tag">PHY delay</span>
<span class="tag">conditional erase</span>
<span class="tag">retry cooldown</span>
</div>
</div>
</article>
<article class="phase">
<div class="phase-label"><strong>04</strong><span>P1 Fix</span></div>
<div class="phase-body">
<h3>运行期断线恢复</h3>
<p>处理 LOST_IP,增大 listen_interval,配置 inactive time,并加入断线 60s 后的 WiFi 看门狗恢复。</p>
<div class="tags">
<span class="tag">LOST_IP</span>
<span class="tag">listen_interval=10</span>
<span class="tag">watchdog</span>
</div>
</div>
</article>
<article class="phase">
<div class="phase-label"><strong>05</strong><span>Verify</span></div>
<div class="phase-body">
<h3>构建与实机验证</h3>
<p>ESP-IDF v6.0 全量构建,执行冷启动、路由器晚启动、DHCP 变化、路由器重启、弱信号与 BLE OTA 后恢复测试。</p>
<div class="tags">
<span class="tag">idf.py build</span>
<span class="tag">cold boot x5</span>
<span class="tag">router restart x3</span>
</div>
</div>
</article>
</div>
</section>
<section id="tasks">
<div class="section-head">
<h2>任务清单</h2>
<p>勾选状态保存在当前浏览器 localStorage,便于多次打开继续跟踪。</p>
</div>
<div class="toolbar">
<button type="button" id="clearChecks">清空勾选</button>
<button type="button" onclick="window.print()">打印计划</button>
</div>
<div class="tasks" aria-label="任务清单">
<label class="task">
<input type="checkbox" data-task="task-1">
<h3>Task 1:冷启动 PHY 校准延迟 + 条件化擦除</h3>
<p>延迟从 500ms 调整为 1500ms;移除冷启动无条件擦 PHY,仅在 <code>esp_wifi_init</code> 失败时擦除并重试。</p>
<small>main/server/wifi_ota.cpp L340-L342, L1524-L1531</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-2">
<h3>Task 2:修复重试耗尽标志</h3>
<p>新增 <code>m_retryExhaustedTimestamp</code>,耗尽 30s 后允许清零计数并重新触发连接;同时解码 reason。</p>
<small>main/server/wifi_ota.cpp L192-L209, main/server/wifi_ota.h</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-3">
<h3>Task 3:处理 IP_EVENT_STA_LOST_IP</h3>
<p>丢 IP 后标记未连接、打印日志、通知 reconnect task,避免无 IP 但状态仍为 connected。</p>
<small>main/server/wifi_ota.cpp IP_EVENT branch</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-4">
<h3>Task 4:主循环 WiFi 断线看门狗</h3>
<p>每 10s 检查连接状态;断线 60s 后强制 reconnect;连续 3 次失败后 stop/start WiFi driver。</p>
<small>main/server/wifi_ota.cpp L1679-L1707, main/server/wifi_ota.h</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-5">
<h3>Task 5:STA 保活配置</h3>
<p><code>listen_interval</code> 从 3 改为 10;<code>esp_wifi_start()</code> 后设置 <code>esp_wifi_set_inactive_time(WIFI_IF_STA, 60)</code>。</p>
<small>main/server/wifi_ota.cpp L324, L345</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-6">
<h3>Task 6:修复 SpiMaster mutex 初始化顺序</h3>
<p>先创建 <code>m_mutex</code>,再进入 <code>LockGuard lock(m_mutex)</code>,消除空指针加锁风险。</p>
<small>main/transport/spi_master.cpp L286-L294</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-7">
<h3>Task 7:完善 NVS 初始化错误处理</h3>
<p>对所有非 <code>ESP_OK</code> 初始化错误记录详细错误码,并尝试 erase/reinit 一次。</p>
<small>main/server/wifi_ota.cpp L1492-L1508</small>
</label>
<label class="task">
<input type="checkbox" data-task="task-8">
<h3>Task 8:ESP-IDF v6.0 编译验证</h3>
<p>全量构建并保留编译日志;构建通过后再进行实机冷启动和路由器重启测试。</p>
<small>idf.py build</small>
</label>
</div>
</section>
<section id="verification">
<div class="section-head">
<h2>验证矩阵</h2>
<p>每个场景至少保留串口日志;涉及路由器的场景同步保留 AP/DHCP 日志。</p>
</div>
<table class="matrix">
<thead>
<tr>
<th>场景</th>
<th>操作</th>
<th>通过标准</th>
</tr>
</thead>
<tbody>
<tr>
<td>长时间断电后冷启动</td>
<td>断电足够久后上电,抓 5 分钟日志,重复 5 次。</td>
<td>STA 能连接并获取 IP,不依赖手动重启。</td>
</tr>
<tr>
<td>路由器晚启动 60-120s</td>
<td>先启动 ESP32,延迟启动路由器或 WiFi 服务。</td>
<td>首次失败后仍能自动恢复 STA。</td>
</tr>
<tr>
<td>DHCP 服务慢或租约变化</td>
<td>缩短 DHCP 租约或重启 DHCP 服务。</td>
<td>LOST_IP 后标记断线并重新获取 IP。</td>
</tr>
<tr>
<td>路由器重启</td>
<td>ESP32 在线时重启路由器,重复 3 次。</td>
<td>断线后自动恢复,HTTP 服务重新可访问。</td>
</tr>
<tr>
<td>WiFi 密码错误</td>
<td>临时配置错误密码或错误 SSID。</td>
<td>reason 可读,不高频无限重试。</td>
</tr>
<tr>
<td>弱信号环境</td>
<td>拉远距离或降低 AP 发射功率。</td>
<td>RSSI、断线原因、恢复动作均可观察。</td>
</tr>
<tr>
<td>BLE OTA 后 WiFi</td>
<td>执行 BLE OTA 流程后访问 WiFi HTTP API。</td>
<td>WiFi 省电模式恢复为 <code>WIFI_PS_NONE</code>。</td>
</tr>
<tr>
<td>SPI 初始化</td>
<td>启动后执行 SPI/CAN 相关 API 或中继流程。</td>
<td>无 mutex NULL 风险,无初始化异常。</td>
</tr>
</tbody>
</table>
</section>
<section id="risks">
<div class="section-head">
<h2>风险与缓解</h2>
<p>修复目标是提高恢复能力,同时避免掩盖配置错误或产生高频重试。</p>
</div>
<div class="risk-grid">
<article class="risk">
<h3>启动速度变慢</h3>
<p>PHY 延迟仅冷启动生效,热启动不受影响。验证时记录冷启动总耗时。</p>
</article>
<article class="risk">
<h3>无限重连循环</h3>
<p>用 30s 冷却期和看门狗分级恢复控制频率,并保留错误 reason。</p>
</article>
<article class="risk">
<h3>HTTP 连接中断</h3>
<p>stop/start 仅在断线 60s+ 且多次强制重连失败后执行,此时 HTTP 已不可用。</p>
</article>
<article class="risk">
<h3>功耗略增</h3>
<p>设备为有线供电,<code>listen_interval=10</code> 与 inactive time 的稳定性收益优先。</p>
</article>
</div>
</section>
<section id="commands">
<div class="section-head">
<h2>执行命令</h2>
<p>根据本机 ESP-IDF 环境调整串口号和 Python/IDF 命令入口。</p>
</div>
<div class="grid-2">
<pre class="code-block"># 冷启动串口日志采集
python serial_monitor.py
# 预期输出文件
G:\Robot_v2\DCU\ESP32_COMM\serial_log.txt</pre>
<pre class="code-block"># ESP-IDF v6.0 构建
idf.py build
# 可选:清理后构建
idf.py fullclean
idf.py build</pre>
</div>
</section>
<section>
<div class="section-head">
<h2>决策记录</h2>
<p>以下备选方案暂不采用,除非后续日志推翻当前判断。</p>
</div>
<div class="checklist">
<div class="checkitem">
<div class="mark">×</div>
<div>
<strong>保留无条件 PHY 擦除</strong>
<span>PHY 校准数据是环境相关积累数据,保留通常比每次冷启动擦除更可靠。</span>
</div>
</div>
<div class="checkitem">
<div class="mark">×</div>
<div>
<strong>只增加 WIFI_STA_MAX_RETRY</strong>
<span>单纯增加次数不能解决耗尽后恢复、运行期断线、LOST_IP 等状态闭环问题。</span>
</div>
</div>
<div class="checkitem">
<div class="mark">×</div>
<div>
<strong>优先优化 DHCP 超时</strong>
<span>当前总等待 30s 已覆盖多数 DHCP 慢场景,需先用日志确认是否真的卡在 DHCP。</span>
</div>
</div>
<div class="checkitem">
<div class="mark">✓</div>
<div>
<strong>先增强诊断再修复</strong>
<span>reason 解码和状态日志能避免把认证、信道、供电、DHCP 问题混在一起处理。</span>
</div>
</div>
</div>
</section>
<p class="footer-note">
计划生成时间:2026-06-16。本文档为静态 HTML,可直接打开、打印或随代码评审归档。
</p>
</div>
<script>
const storageKey = "wifi-cold-boot-plan-checks";
const checks = Array.from(document.querySelectorAll("input[type='checkbox'][data-task]"));
function loadState() {
try {
const saved = JSON.parse(localStorage.getItem(storageKey) || "{}");
checks.forEach((box) => {
box.checked = Boolean(saved[box.dataset.task]);
});
} catch (_) {
checks.forEach((box) => {
box.checked = false;
});
}
}
function saveState() {
const state = {};
checks.forEach((box) => {
state[box.dataset.task] = box.checked;
});
localStorage.setItem(storageKey, JSON.stringify(state));
}
checks.forEach((box) => box.addEventListener("change", saveState));
document.getElementById("clearChecks").addEventListener("click", () => {
checks.forEach((box) => {
box.checked = false;
});
saveState();
});
loadState();
</script>
</body>
</html>