Files
ESP32_COMM/docs/wifi_cold_boot_fix_plan.html
T

1150 lines
32 KiB
HTML
Raw Normal View History

2026-06-16 23:17:06 +08:00
<!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>