- 调整ESP32_COMM/finalize函数执行顺序:先进行SHA-256校验比对, 再调用esp_ota_end和设置启动分区 - 更新BLE传输收尾描述,明确设备端读回OTA分区进行SHA比对的流程, 以及FINISH事件的上报机制
1150 lines
32 KiB
HTML
1150 lines
32 KiB
HTML
<!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>
|