简介:基于ESP8266的softAP热点配网协议Demo,面向物联网开发者与嵌入式初学者,解决设备快速接入家庭或办公室WiFi的配网问题。示例通过开启设备临时热点,让手机或电脑连接后借助HTTP服务提交SSID与密码,再自动切换至Station模式联网,完整演示了softAP配网流程与协议实现。压缩包为rar格式,共117个文件,约6.38MB,包含30个头文件、20个静态库、12个bin固件,以及C源码、链接脚本、Makefile和烧录脚本等,既有可直接编译烧录的完整工程,也方便对照学习各模块。已有1406人学习下载,内容涵盖ESP8266硬件特性、软AP模式、HTTP服务器搭建、WiFi参数接收与Station模式切换等关键知识点,适合通过源码理解物联网设备配网流程。开发者可从中提取配网协议设计思路,迁移到智能家电、传感器节点等实际项目,也可参考其固件结构与编译配置快速上手ESP8266 SDK开发。
1. 8266 softAP 热点配网带协议:为什么我把 SmartConfig 换成了自建热点
A 同学最近调「模拟项目X」的联网功能,路由器开了 AP 隔离,手机用 SmartConfig 把 WiFi 密码广播给模块,五分钟没反应。换上 8266 的 softAP 热点配网,手机先连模块开的热点,按协议把 SSID 和密码发过去,模块重启后自动连路由器,全程不到十秒。8266 softAP 热点配网带协议的 Demo 解决的正是 IoT 首次配网这个实际问题:不开蓝牙、不依赖路由器广播、不要额外硬件,只要手机能连 WiFi 就能配。这个标题里真正要紧的不是「开热点」而是「带协议」——协议决定超时重试、状态确认和错误反馈,没有协议的 Demo 只能演示个灯,有了协议才具备交付价值。适合做小批量设备、想自己掌控配网流程、不想引第三方配网 SDK 的开发者。
2. softAP 配网选型与协议分层:动手前先看清三种方案的边界
2.1 三种主流配网方式的原理差异与选型理由
SmartConfig 的原理是手机连上家里的路由器,App 把 SSID 和密码编码成广播包或组播包发到局域网,模块以混杂模式监听附近信道上的数据包并解出密码。它的优点是用户不用断开手机 WiFi,缺点是依赖路由器不隔离广播、不加密管理帧,而且模块只能听 2.4G,手机在 5G 频段下发包时模块听不到,配网就静默失败。现实中不少家用路由器默认开启 AP 隔离,广播包到不了模块,这种场景下 SmartConfig 基本是废的。
BLE 配网是手机和模块通过蓝牙先建立连接,把 WiFi 凭据经 GATT 通道下发,模块再切换到 WiFi。可用性高,但需要额外的蓝牙芯片或双模模组,对只做 WiFi 的产品等于多了一套硬件成本,所以纯 WiFi 模块上很少用它。
softAP 配网的思路完全不同:模块自己开一个热点,手机主动连上来,此时手机与模块构成一个独立局域网,App 用 TCP 或 UDP 按预设协议把 WiFi 账号密码发给模块。这个方案有几个确定优点:路由器的 AP 隔离策略影响不到模块自己开的热点;手机只要能连 WiFi 就能配网,兼容性最好;配网过程中的所有反馈都能通过协议帧回给手机。代价同样确定:配网时手机需要暂时断开当前 WiFi,App 层必须把这一步引导清楚,否则用户看到 WiFi 断了会以为坏了。
我在有 AP 隔离、多 SSID、5G/2.4G 混用的项目里优先选 softAP,在纯手机可控的演示项目里才考虑 SmartConfig。三者的边界用一张表说清楚:
| 配网方式 | 依赖条件 | 成功率主要瓶颈 | 用户操作 | 额外硬件 | 典型场景 |
|---|---|---|---|---|---|
| SmartConfig | 手机与模块同一路由器(2.4G) | 路由器 AP 隔离与广播限制 | 免切换 WiFi | 无 | 家庭单路由 |
| BLE 配网 | 手机 BLE + 模块共存 | 蓝牙芯片与天线质量 | 免切换 WiFi | 需要 | 带蓝牙的设备 |
| softAP 配网 | 手机能连 WiFi | 无,独立局域网 | 需切到模块热点 | 无 | 首次配网、多路由器场景 |
软 AP 方案的另一个隐藏优势是反馈链路完整:SmartConfig 的 App 端拿不到「模块到底收没收到」的确定性反馈,而 softAP 因为手机和模块在同一个二层网络里,可以直接发探测帧确认模块在线。这也是标题里「带协议」的立足点——没有反馈闭环的配网,只能靠猜。
2.2 最小可跑通:用 8266 打开一个可被手机连接的热点
拿到 Demo 之前,先把最小热点跑起来。常见开发环境里用 Arduino 核心或原厂 SDK 都行,下面用常见的 Arduino 风格,因为做 Demo 迭代最快。代码只做两件事:开热点、打印模块自己的 IP。
#include <ESP8266WiFi.h> String apSSID; // 全局变量,后面协议处理回调里也要用 void setup() { Serial.begin(115200); // 热点 SSID 带芯片 ID 后缀,避免多台设备同时开热点时重名 apSSID = "X_AP_" + String(ESP.getChipId(), HEX); // 参数依次是 SSID、密码、信道、是否隐藏 SSID // 信道固定用 6,避开 2.4G 上最拥挤的信道 1 bool ok = WiFi.softAP(apSSID.c_str(), "12345678", 6, 0); if (!ok) { Serial.println("[AP] softAP start failed"); return; } // 固定软 AP 网段与网关地址,方便上位机按固定 IP 联调 WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); Serial.printf("[AP] SSID=%s, IP=%s\n", apSSID.c_str(), WiFi.softAPIP().toString().c_str()); } void loop() { // 空循环,留给后面的协议处理 }这段代码的逻辑是把芯片唯一 ID 拼进热点名,原因是多台设备同时配网时,手机端能通过名字后缀区分目标设备,避免连错热点。WiFi.softAP 的第一个参数是热点名,第二个是密码,至少 8 位;信道传 6 是个折中选择,因为 2.4G 三个互不重叠的信道是 1、6、11,6 在中间,配网这种短时连接够用。最后一个参数 0 表示广播 SSID,如果填 1 会隐藏热点,手机要手动输名字才能连,配网场景不建议隐藏。softAPConfig 固定网段是常见做法,否则模块随机选网段,联调脚本里写死的目标 IP 每次可能都不同。
代码烧进去后,用手机搜索 WiFi 列表,能看到以 X_AP_ 开头的热点,连接后查手机 IP,只要是 192.168.4.x 开头,说明模块的 AP 和 DHCP 服务已经正常。这一步跑通,再往里面加协议就不会被「热点起没起来」干扰。
3. 把配网协议先定明白:帧结构、状态机与超时重试参数
「带协议」是这个 Demo 区别于玩具代码的分水岭。如果只是让手机发一个字符串「SSID=xxx PASS=xxx」,模块按行读取,会踩到几个说不清的坑:SSID 里带空格或特殊符号时解析错位;手机发了两帧粘包后模块只读到半截;模块回复的「OK」手机没法区分是「收到了」还是「连上了」。协议要做的就是把每次交互变成可解析、可校验、有确定响应的帧。
3.1 协议帧结构设计:魔数、命令字、载荷与校验
我一般用定长帧头加变长载荷的结构,256 字节以内的设备 RAM 也能轻松处理。帧结构按字节划分如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 魔数 | 2 字节 | 固定 0xA5 0x5A,用于过滤杂包 |
| 版本 | 1 字节 | 协议版本号,首版 0x01 |
| 命令字 | 1 字节 | 0x01 探测,0x02 配网请求,0x03 配网确认,0x04 失败 |
| 载荷长度 | 2 字节 | 小端,实际限制在 128 |
| 载荷 | N 字节 | JSON 编码的 SSID 与密码 |
| 校验 | 1 字节 | 对版本到载荷末尾做 CRC8 |
魔数的作用是让模块在同一个端口上收包时快速丢弃非协议流量。版本号看起来多余,但设备出货后协议要升级时,旧 App 和新固件之间靠它决定是否兼容,我第一次做时省了版本号,后来升级命令字时老设备全部解析错。载荷长度占 2 字节是为扩展留余地,SSID 加密码用 1 字节也够,但后续想在配网协议里带个 token 或时间戳就会撞上限。校验用 CRC8 而不是异或,是因为异或对连续多位错误不敏感,配网这种关键操作宁可多花代码也不能让错误密码被当成正确密码保存。CRC8 的多项式两端约定一致即可,我习惯用查表法,代码三十多行,放在 Demo 里不占空间。
帧解析不要直接用结构体映射接收缓冲,因为 C 结构体存在字节对齐,memcpy 出来的字段可能错位。我用字节数组接收,再按偏移量手写解析:
#define FRAME_MAGIC0 0xA5 #define FRAME_MAGIC1 0x5A #define FRAME_CMD_REQ 0x02 #define FRAME_CMD_ACK 0x03 #define FRAME_CMD_ERR 0x04 #define PL_LEN_MAX 128 bool parseFrame(uint8_t *buf, uint16_t len, uint8_t *cmd, uint8_t *payload, uint16_t *payloadLen) { if (len < 7) return false; if (buf[0] != FRAME_MAGIC0 || buf[1] != FRAME_MAGIC1) return false; *cmd = buf[3]; // 载荷长度是两字节小端,高位左移 8 位拼接 *payloadLen = buf[4] | ((uint16_t)buf[5] << 8); if (*payloadLen == 0 || *payloadLen > PL_LEN_MAX) return false; memcpy(payload, &buf[6], *payloadLen); // 校验范围从版本字节到载荷末尾,不含魔数 if (crc8_buf(buf + 2, 4 + *payloadLen) != buf[6 + *payloadLen]) return false; return true; }这个函数有三个关键点:先判最小长度再取字段,避免越界;用小端拼接而不是强制转换,绕开结构体对齐问题;校验计算范围是「版本到载荷末尾」,不包括魔数,这是两端约定,一致就行。CRC8 的实现用查表法,初始化时算好 256 个表项,运行期每个字节查一次表,速度和代码量都适合单片机。
注意:解析前一定要校验魔数,模块和 App 共用一个端口时,局域网里的其它广播包很容易混进来,不校验魔数会把垃圾数据当成配网帧处理。
3.2 配网会话状态机:从 IDLE 到 APPLYING 的六态设计
协议帧定义好,还要定义模块在整个配网过程中的状态流转。没有状态机时,代码靠一堆 if 标记位容易漏掉异常分支——用户配网配到一半放弃,模块还在等载荷;收到配网请求后还没保存完又收到第二帧,旧数据被覆盖。Demo 里我会把状态机写成显式枚举,主循环只做状态迁移,处理函数里不直接读写 WiFi。
typedef enum { NET_IDLE, // 空转,等触发 NET_AP_WAIT, // 热点已开启,等手机连接 NET_CFG_RECV, // 收到完整协议帧,正在校验并保存 NET_APPLYING, // 已切换 STA 模式,正在连接目标路由 NET_DONE, // 连接成功,进入正常联网 NET_FAIL // 密码错误或超时,回错误帧后重开热点 } net_state_t; void onFrameReceived(uint8_t *buf, uint16_t len) { uint8_t cmd; uint8_t payload[PL_LEN_MAX]; uint16_t payloadLen; if (!parseFrame(buf, len, &cmd, payload, &payloadLen)) return; switch (cmd) { case 0x01: // 探测:手机询问模块是否在线 sendReply(0x03, NULL, 0); break; case 0x02: // 配网请求 if (netState == NET_AP_WAIT || netState == NET_FAIL) { decodePayload(payload, payloadLen, ssidBuf, passBuf); netState = NET_CFG_RECV; } break; } }所有协议帧先在回调里做解析和校验,通过后才修改状态;主循环按状态执行具体动作,这样保存配置、回确认帧、切换 WiFi 模式这些耗时操作不会被协议栈回调卡住。状态机把路径收敛得很直接:从 AP_WAIT 收帧进入 CFG_RECV,保存后进 APPLYING,连接成功进 DONE,失败进 FAIL 后重新回 AP_WAIT。
参数方面,ssidBuf 建议开 33 字节,passBuf 开 65 字节,多出来的 1 字节放字符串结束符;decodePayload 内部必须做长度检查,防止缓冲区溢出。确认帧一定要在保存完之后再发,手机端收到确认才敢提示「去切回你的 WiFi」,如果先回确认再保存,正好在保存时掉电,手机就给用户错误提示。
3.3 协议参数与边界:超时、重试、最大载荷怎么定才不翻车
参数值我一般按下表配,不同场景微调:
| 参数 | 推荐值 | 理由 |
|---|---|---|
| 手机连 AP 超时 | 30 秒 | 用户从 WiFi 列表切热点需要时间,短于 15 秒容易误报 |
| 等待配网帧超时 | 60 秒 | 超过 60 秒没收到完整帧,状态回到 AP_WAIT,释放内存 |
| 连接目标路由超时 | 10 秒 | STA 模式下探测 DHCP 一般 5 秒内完成,10 秒足够 |
| 失败重试次数 | 3 次 | 模块可能刚好落在路由器信号盲区,重试能覆盖瞬时抖动 |
| 最大载荷 | 128 字节 | 覆盖 SSID 32 + 密码 64 + 扩展字段 |
| 密码最小长度 | 8 位 | 与模块开热点时要求一致,避免用户输短密码 |
这些参数里最容易忽略的是「等待配网帧超时」。我见过有人把 UDP 接收放在阻塞循环里,手机 App 切后台再回来,模块已经卡死在等待状态,热点正常但就是连不上。正确做法是主循环里查一个 millis() 时间戳,超时就重置状态,而不是把 socket 接收写成死循环。另一个容易被忽略的是重试次数的计算口径——我按「连续三次连接失败才算 FAIL」,而不是每次失败都立刻回错误帧,因为 WiFi 信道的瞬时冲突很常见,试一次失败就报错,用户体验是偶尔莫名失败,重试 3 次能把偶发问题过滤掉。
4. 把协议落到 8266 代码里:NVS 保存、主循环与上位机联调
4.1 NVS 存储设计:SSID、密码、校验码如何可靠保存
配网的下半场是「保存并重连」。8266 在 Arduino 环境里用 EEPROM 库模拟 NVS,实际写的是 flash,所以写入流程和真实 NVS 有差别——调 EEPROM.put 只是改了内存镜像,必须调 commit 才真正落盘。我见过太多只有 put 没有 commit 的 Demo,重启后配置丢失,现象是模块每次都要重新配网,排查半天才发现是少调一个函数。
存储结构除了 SSID 和密码,我会额外存一个 magic 和一个 CRC8。magic 用来区分 flash 里是有效配置还是全 0xFF 的空白区;CRC8 用来检测数据有没有被写坏。读取时先查 magic 再验 CRC,两者任一不通过都当没有配置处理,直接进 AP 模式。
#include <EEPROM.h> #define CFG_MAGIC 0xA5 #define SSID_MAX_LEN 32 #define PASS_MAX_LEN 64 typedef struct { uint8_t magic; char ssid[SSID_MAX_LEN + 1]; char pass[PASS_MAX_LEN + 1]; uint8_t crc8; } wifi_cfg_t; void saveWifiConfig(const char *ssid, const char *pass) { wifi_cfg_t cfg; memset(&cfg, 0, sizeof(cfg)); cfg.magic = CFG_MAGIC; strncpy(cfg.ssid, ssid, SSID_MAX_LEN); // 留 1 字节给结束符 strncpy(cfg.pass, pass, PASS_MAX_LEN); cfg.crc8 = crc8_buf((uint8_t*)&cfg, sizeof(cfg) - 1); EEPROM.begin(sizeof(cfg)); EEPROM.put(0, cfg); EEPROM.commit(); // 必须调用,否则只改了内存 Serial.printf("[CFG] saved, crc=%02X\n", cfg.crc8); } bool loadWifiConfig(char *ssid, char *pass) { wifi_cfg_t cfg; EEPROM.begin(sizeof(cfg)); EEPROM.get(0, cfg); if (cfg.magic != CFG_MAGIC) return false; if (cfg.crc8 != crc8_buf((uint8_t*)&cfg, sizeof(cfg) - 1)) return false; strcpy(ssid, cfg.ssid); strcpy(pass, cfg.pass); return true; }关键点:strncpy 长度比数组少 1,确保字符串有结束符;crc8 计算范围取 sizeof(cfg)-1,把 crc8 字段排除在外,不然校验值永远是错的。EEPROM.begin 的参数是操作的字节数,这里传结构体大小 100 字节;8266 的模拟 EEPROM 最大可用 4096 字节,配网配置 100 字节足够。实际产品建议把这段存储放在用户参数区,避免 OTA 升级时被擦掉,但 Demo 阶段不必纠结分区。
注意:EEPROM.commit() 必须调用。只调 put 不调 commit,当前进程内能读到数据,断电重启后内存镜像清空,配置就丢了。
4.2 协议处理主循环与配网成功后的重连逻辑
存储写完,主循环要把「配网状态」和「WiFi 连接状态」串起来。我的做法是:模块上电先 loadWifiConfig,读到有效配置就直接以 STA 模式连目标路由;没读到才进入 softAP 模式等配网。这样用户配过一次网后,后续上电不用重新配。
重连逻辑放 WiFi 事件回调里更可靠,不要在 loop 里轮询 WiFi.status(),因为回调能拿到断开的具体原因,轮询只能拿到笼统数字。ssidBuf 和 passBuf 是全局缓冲区,由协议解析时填充。
void onWiFiEvent(WiFiEvent_t event) { switch (event) { case WIFI_EVENT_STAMODE_CONNECTED: Serial.println("[NET] connected to router"); break; case WIFI_EVENT_STAMODE_GOT_IP: netState = NET_DONE; Serial.printf("[NET] IP=%s\n", WiFi.localIP().toString().c_str()); break; case WIFI_EVENT_STAMODE_DISCONNECTED: if (netState == NET_APPLYING) { retryCount++; if (retryCount >= 3) { netState = NET_FAIL; // 重新开启热点,让手机能连回来继续配网 WiFi.softAP(apSSID.c_str(), "12345678", 6, 0); } else { WiFi.begin(ssidBuf, passBuf); } } break; } }回调逻辑里有个经验:在 APPLYING 状态收到 DISCONNECTED,不要立刻重新 begin,先累加 retryCount。路由器偶尔刷新密钥或切换信道会产生瞬时断连,立刻重连大概率能恢复;连续三次都断,说明密码或加密方式不对,再重试只会让用户等更久。重连失败后要重新开启 softAP,并把 netState 置为 NET_FAIL,这样手机端既能收到失败帧,也能重新连回热点继续配网。注意这里 WiFi.softAP 的参数必须和初始化保持一致,否则手机端缓存的网络凭据和新热点名不匹配。
注意回调里没有直接调 sendReply 回失败帧,因为回调上下文直接操作 UDP 可能和协议栈竞争。正确做法是主循环检测到 netState == NET_FAIL 时再发失败帧、再重开热点。主循环里另一个细节是状态迁移要留一个 tick:
void loop() { // 收包在回调中处理,这里只做状态迁移 if (netState == NET_CFG_RECV && millis() - lastRecvTime > 200) { // 留 200ms 给 WiFi 协议栈处理完收包流程,避免竞争 netState = NET_APPLYING; } yield(); }这个 200ms 的小延时常被人忽略。UDP 收包回调在协议栈上下文执行,如果回调里立刻做 WiFi.mode 切换,可能和协议栈正在处理的包产生竞争,表现是偶发重启。留一个 tick 让收包流程彻底结束再切模式,能显著减少这种玄学问题。
4.3 上位机快速联调:不开 App 也能验证协议
写手机 App 之前,先用电脑连模块热点,跑一个 Python 脚本把协议帧发出去,能收到确认帧再写 App,省掉 App 和固件同时调试的混乱。模块端监听 5000 端口,用 UDP 而不是 TCP 的原因:UDP 无连接,适合模块这种每次收包时间很短、不维护会话的场景;TCP 还要处理粘包、半包和连接断开重连,Demo 阶段不值得。UDP 丢包在同一个 AP 内基本可忽略,加上协议帧里有命令字和校验,真丢了手机端重发一帧就行。
# 先确认电脑 WiFi 连上了 X_AP_ 开头的热点,IP 为 192.168.4.x ipconfig # Windows 查看;Linux 用 ifconfig 或 ip addr# udp_provision_test.py import json import socket MODULE_IP = "192.168.4.1" # 模块 softAP 的固定网关地址 MODULE_PORT = 5000 # 与模块端监听端口一致 def build_frame(cmd, payload: bytes) -> bytes: ver = 0x01 return b"\xA5\x5A" + bytes([ver, cmd]) + len(payload).to_bytes(2, "little") + payload def main(): payload = json.dumps({"ssid": "MyHomeWiFi", "pass": "12345678"}).encode("utf-8") frame = build_frame(0x02, payload) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(3) s.sendto(frame, (MODULE_IP, MODULE_PORT)) try: data, addr = s.recvfrom(128) print("ACK:", data.hex()) except socket.timeout: print("No ACK, check AP connection and module console") if __name__ == "__main__": main()脚本里的 build_frame 和模块端 parseFrame 是对应的,关键是长度用 to_bytes(2, "little") 生成小端字节,和模块端 buf[4] | (buf[5] << 8) 的拼接顺序一致。载荷直接 JSON 而不是自定义 key=value,是为了避免 SSID 或密码里出现 &、=、空格时还要做一层转义。联调时收不到 ACK,第一件事是查电脑 IP 是不是 192.168.4.x,而不是查模块代码——热点连上但 DHCP 没拿到 IP 是常见现象,IP 不对时 UDP 包根本路由不到模块。
5. 8266 softAP 配网避坑实录:5 个高频翻车现场与排查方法
5.1 现象:手机连上热点但模块死活不回包
手机 WiFi 列表里能看到 X_AP_ 热点,连上也拿到了 192.168.4.x 的 IP,但配网 App 发出去的探测帧没有响应。原因多半有两种:一是模块在 WiFi 事件回调里执行了延时操作,比如回调里调 delay(1000),把协议栈卡住,UDP 数据到了但应用来不及取;二是 UDP 接收缓冲区设置太小,数据帧超过缓冲区长度后被协议栈直接丢弃。解决方式:把回调里的耗时操作全部挪到主循环,回调只做标记;接收缓冲开 512 字节,配网帧最大 128 字节,留足余量。Arduino 环境下注意回调里不要 Serial.print 长字符串,串口打印是阻塞的,打印量大了同样会卡协议栈。
5.2 现象:配网成功但模块连不上目标路由器
手机端显示「已发送」,模块也回 ACK 保存了,但模块始终进不了 STA 模式,或者连上又掉。这类问题十有八九出在字符串处理:用户输入密码时输入法自动在末尾加了空格,或 SSID 里包含全角字符,模块端没做任何清洗直接传给 WiFi.begin。WiFi.begin 对这些输入不会报错,只会不断重试失败。解决方式:协议解析层统一做规范化——载荷里的字符串先 trim 掉首尾空白;SSID 限长 32、密码限长 64,超长直接回失败帧;全角字符在手机端做输入校验拦截。模块端不要信任手机传来的任何字节,所有字段必须先过长度和字符集检查再写入 NVS。
5.3 现象:模块重启后配置丢失,每次开机都要重新配网
这是典型的「只 put 不 commit」问题。Arduino 环境里 EEPROM.put 只是更新内存镜像,EEPROM.commit 才把数据真正写入 flash。有人图省事在 saveWifiConfig 里写完就 return,上电时也能读到数据——因为内存镜像还在——但断电重启后镜像清空,配置就丢了。这个 bug 还不会每次必现,偶尔成功一次,表现很像玄学。解决方式:commit 必须调用;同时读配置时用 magic 加 CRC8 双重校验,不要只看 magic。如果 commit 后仍然丢,检查是不是用了错误的 EEPROM 起始地址,或者结构体有变化导致旧数据读出来校验失败。常规做法是把配置结构体版本号也放进去,结构体升级时能自动识别旧版本并清空重配。
5.4 现象:密码输错要等 10 秒才提示失败
模块回 ACK「已保存」后,手机端一直转圈,过了十秒才收到失败帧。原因是模块在 APPLYING 状态等待连接结果,DHCP 超时时间默认较长,密码错误时模块要等两三次 DHCP 重试失败后才进入 DISCONNECTED 回调。用户感知是「怎么这么久」,体验很差。解决方式:把连接等待超时缩短到 5 秒,主循环里查 millis,超时直接判 FAIL 并回错误帧;同时在上位机提示「配置已保存,正在连接,请稍候」,把等待期的预期拉起来。真正的原因可以在失败帧的载荷里带错误码,手机端按错误码展示对应文案。
5.5 现象:模块热点开着却时断时续,手机连接不稳定
热点能搜到,但手机一连就掉,或者连上之后马上断。常见原因有三个:模块默认开启了 WiFi 省电模式(modem sleep),热点模式下休眠让手机以为设备掉线;模块靠近开关电源或金属外壳,天线被干扰;2.4G 信道 1 上全是邻居的路由器,信道拥挤丢包率高。解决方式:初始化时显式调用 WiFi.setSleepMode(WIFI_NONE_SLEEP) 关掉省电;把信道从默认的 1 改到 6 或 13,避开密集区域;硬件上让天线远离电源和屏蔽壳。这个坑第一次遇到时百思不得其解,后来发现是省电模式在作怪——AP 开着但 beacon 间隔被拉长,手机端判定热点不稳定。这类问题靠抓包看不出明显异常,直接关省电模式是最快的排除手段。
6. 配网成功率怎么验收:把状态埋点当黑匣子用
Demo 能跑通不等于能交付。我给每个状态迁移加一行带毫秒时间戳的日志,这是排查配网问题最值回票价的习惯:
Serial.printf("[%lu] state -> %s\n", millis(), stateName(netState));一行日志不够,还要在关键节点打点:热点开启完成、收到配网帧、校验通过、NVS 保存完成、切换 STA 模式、DHCP 拿到 IP、连接失败。把这些事件打在同一张时间线里,用户报问题时只需要把设备串口日志发回来,就能看出卡在哪一步,不用复现就能定位七八成问题。我在模拟项目X里偷懒没加埋点,用户反馈配网失败时只能靠猜,加了之后基本不用问用户做了什么操作,看日志就能还原整条链路,省了至少一个星期的排查时间。
验收时按回归清单跑一遍,别只测「正常配网」这一条路。清单至少覆盖:正确密码配网应回 ACK 并上线;错误密码应在 5 秒内回错误帧并保持热点开启;配网成功后瞬间断电,重新上电应自动连已保存网络;两个模块同时开热点,手机应能通过热点名后缀区分;长按按键擦除配置后应重新进入 AP 模式。这五条覆盖了平时最容易翻车的分支,每轮固件改动后跑一遍,配网回归基本就稳了。
最后留一个习惯:配网代码评审时,我会先问「协议解析失败时有没有日志」「NVS 写入失败会怎样」「连接失败提示给不给用户」,三个问题答不上来的代码,上线后大概率要返工。希望帮到你。
本文还有配套的精品资源,点击获取