
上个月帮朋友调一块 ESP32 开发板设备本身跑得好好的最后却卡在“怎么让设备连上他家的 WiFi”这一步。我说你直接把 SSID 和密码写在代码里不就行了他说你自己调试当然没问题可这个设备是要交给一个完全不会用串口、不会看代码的人用的难道让人家拿电脑过来敲命令这个问题一下子点醒了我配网Provisioning是每一个 IoT 项目绕不开的坎它决定了你的设备是“能用”还是“好用”。这篇文章我会以 ESP32 / ESP8266 搭配 MicroPython 为背景把 IoT 设备 WiFi 配网这件事从头到尾讲透。内容包括配网到底在解决什么问题、SmartConfig / SoftAP / BLE 三种主流方式的原理与取舍、MicroPython 环境下 SmartConfig 的真实可用性、搭一个完整 SoftAP网页配网流程的代码、以及配网成功后的配置保存与自动重连。最后还会把我自己踩过的坑、排查的顺序一并分享出来。不管你是刚拿到开发板的新手还是已经在做量产固件的工程师看完这篇应该都能少走不少弯路。1. 先说清楚配网到底在配什么1.1 没有屏幕的设备怎么开口要密码想想手机连 WiFi 的过程你打开设置、选中网络、弹出一个键盘输入密码回车之后手机就上网了。这个交互能成立是因为手机有屏幕、有键盘、有输入法甚至还有保存历史记录的功能。这些我们习以为常的“基础设施”在一颗小小的 ESP32 上基本都不存在更别说很多 IoT 设备连按键都没有。设备能做的只有两件事一是作为站点STA去连接某个已知的网络二是作为热点AP让别人来连接自己。配网的本质就是把“WiFi 名称 密码”这对网络凭证从用户手里安全地搬运到设备内部让设备有能力完成第一次入网。因为这一步发生在设备还没有任何网络连接的时候所以它天然是一个“鸡生蛋”的问题要上网才能配网但配网就是为了上网。所有的配网方案都是在解决“在设备离线时通过一个临时的带外通道把凭证送进去”这件事。1.2 好用的配网方案至少要满足四个条件第一个是成功率。方案听着再高级用户操作三步就有一步失败那四舍五入就是不可用。很多配网功能在开发者手里一切正常一到用户家里就翻车往往是方案稳定性不够而不是用户笨。第二个是操作门槛。用户愿意为配网付出的耐心通常不超过五分钟。如果一个配网流程需要卸载某个 App、打开开发者模式、或者手动改网卡配置那这个产品基本可以告别大众市场了。第三个是可恢复性。WiFi 密码改了、路由器换了、设备放的位置信号太差都会导致设备失联。配网方案必须能随时重新进入最好还能保留“恢复出厂”的能力。第四个是状态反馈。配网过程中用户在等设备到底在扫描、在连接、还是没有信号一个能从 LED 灯或者声音里读出来的状态提示比任何日志都好使。2. 三种主流配网方式原理与优劣一次讲透2.1 SmartConfig手机“念咒语”设备“侧耳听”SmartConfig 最早是乐鑫提出的配网方案在 ESP 系列上对应的具体协议叫 ESP-Touch。它的核心思路非常巧妙手机保持在已经连接的 WiFi 下然后把 SSID 和密码编码到大量 UDP 广播报文的长度、时序或者载荷里向空气中“喊话”。ESP32 / ESP8266 此时并不连接路由器而是把 WiFi 网卡设置成混杂模式promiscuous mode像个收音机一样在空中抓取所有信道上的 802.11 数据帧再从中解析出手机发送的凭证。这个方案的优点是用户操作极简手机连着自己的 WiFi打开配网页面点一下“开始配置”设备收到后自动联网全程不用切换网络。听起来非常理想。但它的缺点也很明显手机必须连在 2.4GHz 频段上5GHz 网络下协议根本不工作路由器如果开启了 AP 隔离、多播过滤、或者信道切换过于频繁都会导致设备收不到完整报文周围无线环境越乱成功率就越低。在 MicroPython 环境下 SmartConfig 还有另一层麻烦不是所有固件都内置了对应的库后面专门有一节讲这个。2.2 SoftAP让设备帮你开一间临时接待室SoftAP 配网的思路就朴素得多设备先进入热点模式开出一个名字类似ESP_Config_4A2F的临时 WiFi。用户用手机连接这个热点然后浏览器访问一个固定 IP通常192.168.4.1页面上会有输入框让用户填写家里路由器的 WiFi 名称和密码。设备收到这些参数后立刻关闭热点切换到站点模式去连接目标路由器。这个方案的优点非常实际它把用户手机和待配网设备之间的通道建立在一个已经连接成功的局域网上没有混杂模式那种靠“抓包”的玄学成分。只要热点能连上、网页能打开配网成功率就非常高。而且用户只需要手机自带浏览器不需要安装任何 App。很多量产模块默认提供的就是 SoftAP 配网原因就在这里。缺点也显而易见用户必须手动切换 WiFi 网络在手机上输一次热点密码如果热点加密的话操作步骤比 SmartConfig 多。另外如果设备断电或者配置出错用户可能卡在“连不上原来的 WiFi”这个状态里所以配网流程设计必须想清楚如何退出、如何恢复。2.3 BLE 配网和“混合式”方案什么时候才用它如果你用的是带蓝牙的芯片比如 ESP32 本身就内置 BLE还可以走 BLE 配网手机通过蓝牙把自己的 WiFi 凭证发给设备。BLE 通道天然稳定、不依赖无线环境抓包而且可以顺便做设备绑定和设备信息同步。代价是代码量明显增加要么在手机里集成 BLE SDK要么开发一个小程序或 App门槛比前两种高很多。更常见的是“混合式”打法配网时走 BLE 或 SoftAP配网成功后数据走 WiFi。这不是互相替代的关系而是根据产品定位选择入口通道的问题。如果你做的是需要装 App 的智能家居单品BLE 配网体验确实最好如果你做的是想“微信扫一扫就能配置”的轻量设备SoftAP 网页是最省事的。三种方案对比如下维度SmartConfig / ESP-TouchSoftAP 网页BLE 配网用户操作一键不切换网络切换热点开网页填表打开 App 蓝牙配对成功率容易被干扰波动大稳定基本一次成功稳定依赖组件需要带 SmartConfig 的 App/小程序仅需手机浏览器需要 App/小程序 BLE 协议栈代码复杂度中低高量产常见度中高中常见于智能锁等我的个人看法是如果你只能背一种方案背 SoftAP 配网是最值的。它逻辑清晰、好调试、也不依赖任何云端能力。3. MicroPython 里跑通 SmartConfig固件与 API 实测记录3.1 第一步确认你的 MicroPython 固件支持什么很多从 Arduino 转到 MicroPython 的朋友第一反应是“ESP32 不都支持 ESP-Touch 吗”在 Arduino 生态里确实如此因为厂商把完整的库和示例都给你备好了。但 MicroPython 是一个精简到极致的环境官方固件只封装了一小部分硬件功能SmartConfig 并没有被默认带上或者说不同固件版本的差异很大。我在实际测试中发现部分较新的 ESP32 MicroPython 固件里确实包含了一个esp32.esp_touch模块但 API 与 Arduino 版完全不同而且稳定性一般另外有一些社区定制固件则选择直接砍掉这个模块以减小固件体积。所以在写任何代码之前请先在你的设备上执行import esp32 dir(esp32)查看输出里有没有esp_touch或类似名称。如果ImportError说明当前固件不支持最靠谱的选择就是放弃 SmartConfig改用 SoftAP 配网或者换一个包含了该支持的社区固件。3.2 可以尝试的 SmartConfig 代码框架如果你的固件确实带esp_touch可以把下面这个流程当成起点。再次强调具体 API 以你烧录的固件文档为准网上流传的 Arduino 代码不能直接套用。import network import time wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): try: from esp32 import esp_touch configurator esp_touch() except ImportError: print(当前固件缺少 esp_touch 模块) raise print(请通过配套 App 发起配网...) # 不同固件的启动方式差异很大 # 有的需要传入回调函数有的是 start() / smartconfig_start() # configurator.smartconfig_start() # 这里只演示等待联网的通用轮询逻辑 timeout 60 while timeout and not wlan.isconnected(): time.sleep_ms(1000) timeout - 1 if wlan.isconnected(): print(配网成功, wlan.ifconfig()) else: print(配网超时)这里有个很关键的点你用来发起配网的手机 App 或者小程序必须和你的设备在同一个路由器下而且手机要连着2.4GHz的 WiFi。很多调试了半天不成功的人第一个要排查的就是手机是不是连在 5GHz 频段上。3.3 AirKiss另一个可用思路但 MicroPython 支持并不友好除了乐鑫的 ESP-Touch国内不少物联网方案还会用到 AirKiss 协议它和 ESP-Touch 的实现思路本质上是一路货靠混杂模式抓包、从 UDP 报文里还原 WiFi 凭证。它在某些 IM 生态的配网流程里出现频率很高但在 MicroPython 环境下几乎找不到能直接跑的现成库。原因也不难理解AirKiss 对 802.11 帧的解码深度要求比较高MicroPython 的 socket 层根本接触不到原始无线帧必须靠 C 层扩展去实现。除非你对底层移植很有经验否则我不建议在 MicroPython 项目里硬磕 AirKiss。真要在意这个生态用原厂 SDK 或者 Arduino 更合适。所以关于 MicroPython 的 SmartConfig我的最终结论是能做但别太指望。它适合作为“尝鲜玩法”不建议作为量产产品的主配网通道。你真正需要认真掌握的是 SoftAP 配网。4. SoftAP HTTP 网页配网最稳的方案与完整代码4.1 为什么我建议量产优先选 SoftAP前面说过 SoftAP 成功率高但还有一个常被忽略的理由它把配网流程变成了一个完全可观测的 HTTP 交互。用户在网页上提交表单设备从请求里拿到参数、开始连接路由器、再把连接结果返回给页面所有环节都能加日志、打点、失败重试。出了问题你甚至不用连串口只要看用户描述卡在哪一步就能推测出来。MicroPython 在这个场景下还有个先天优势socket模块和network模块都是内置的不需要额外装任何库写一个几十行的 HTTP 服务完全够用。所以接下来我会给一套可以直接抄走的代码它包含了热点创建、HTTP 服务、参数解析、连接路由器、响应结果这一整条链路。4.2 完整代码热点、网页、表单解析一把抓代码设计上我倾向于把配网逻辑写成一个独立的模块和业务主程序解耦。下面这段代码可以在 ESP32 或 ESP8266 的 MicroPython 环境里直接运行开启一个名为ESP_Config_XXXX的开放热点然后提供http://192.168.4.1的配网页面。import network import socket import time import ubinascii import machine import json import os def start_ap(): # 用 MAC 后四位生成热点名称避免多设备重名 uid ubinascii.hexlify(machine.unique_id()).decode().upper() ap_ssid ESP_Config_ uid[-4:] ap network.WLAN(network.AP_IF) # 这里故意不配置密码让用户少输一次密码降低操作门槛 ap.config(essidap_ssid) ap.active(True) while not ap.active(): time.sleep_ms(200) print(热点已开启:, ap_ssid) print(配网页面: http://192.168.4.1) return ap def make_webpage(): html !DOCTYPE html html langzh head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title设备配网/title /head body h2配置 WiFi 网络/h2 form methodpost action/config pWiFi 名称: input namessid required/p pWiFi 密码: input namepassword typepassword/p pbutton typesubmit保存并连接/button/p /form /body /html return html def parse_form(body): # 简单解码 application/x-www-form-urlencoded 表单 parts {} if not body: return parts # 会被浏览器编码成空格 body body.replace(b, b ) for item in body.split(b): if b in item: key, val item.split(b, 1) parts[key.decode()] urldecode(val) return parts def urldecode(s): # 解码 %XX 形式的内容 try: s s.decode(utf-8) except Exception: s s.decode(latin-1) out i 0 while i len(s): if s[i] % and i 2 len(s): out chr(int(s[i1:i3], 16)) i 3 else: out s[i] i 1 return out def try_connect_and_respond(cl, ssid, password): sta network.WLAN(network.STA_IF) sta.active(True) sta.disconnect() time.sleep_ms(200) print(尝试连接:, ssid) sta.connect(ssid, password) # 最多等待 15 秒期间轮询连接状态 for _ in range(30): if sta.isconnected(): break time.sleep_ms(500) if sta.isconnected(): # 连接成功保存配置并关闭热点 save_wifi_config(ssid, password) ap.active(False) msg 连接成功设备 IP: sta.ifconfig()[0] resp HTTP/1.1 200 OK\r\nContent-Type: text/plain; charsetutf-8\r\n\r\n msg else: msg 连接失败请检查 WiFi 名称和密码后再试 resp HTTP/1.1 200 OK\r\nContent-Type: text/plain; charsetutf-8\r\n\r\n msg cl.sendall(resp) cl.close() def save_wifi_config(ssid, password): with open(wifi.json, w) as f: json.dump({ssid: ssid, password: password}, f) def start_http_server(): addr socket.getaddrinfo(0.0.0.0, 80)[0][-1] s socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(addr) s.listen(2) print(HTTP 服务已启动) while True: cl, _ s.accept() try: cl.settimeout(5) request b while b\r\n\r\n not in request: chunk cl.recv(1024) if not chunk: break request chunk header, _, body request.partition(b\r\n\r\n) first_line header.split(b\r\n)[0].decode() if first_line.startswith(GET): cl.sendall(HTTP/1.1 200 OK\r\nContent-Type: text/html; charsetutf-8\r\n\r\n make_webpage()) cl.close() elif first_line.startswith(POST): content_length 0 for line in header.split(b\r\n): if line.lower().startswith(bcontent-length:): content_length int(line.split(b:)[1].strip()) while len(body) content_length: body cl.recv(1024) form parse_form(body) ssid form.get(ssid, ).strip() password form.get(password, ).strip() if ssid: try_connect_and_respond(cl, ssid, password) else: cl.sendall(HTTP/1.1 200 OK\r\nContent-Type: text/plain; charsetutf-8\r\n\r\nSSID 不能为空) cl.close() except Exception as e: if cl: cl.close() finally: if cl: cl.close() ap start_ap() start_http_server()这段代码有几个地方是特意这么设计的我说一下理由。热点不设密码虽然安全性看起来差一些但实际配网场景只有几分钟而且设备本身还没有上网风险可控。如果设了密码用户就必须先在手机上输一次热点密码再打开网页填一次路由器密码操作成本直接翻倍很容易把用户绕晕。表单解析中那个urldecode函数不可少。浏览器会把密码里的中文、空格、%、、#等字符做 URL 编码如果直接拿原始字符串去连接路由器就会因为特殊字符处理不当而连接失败。我在实际项目里就遇到过用户的 WiFi 密码里带#第一次没做解码设备死活连不上。4.3 参数解析与隐藏 WiFi 的处理细节上面的parse_form是一个可用的简化版实现。如果你希望更严谨建议对比一下键值重复提交、空密码、超长 SSID 这些边界情况。MicroPython 环境没有完整的标准库所以我习惯“够用就好”保证正常链路不出错即可。关于隐藏 WiFi如果你的目标路由器关闭了 SSID 广播MicroPython 里wlan.connect(ssid, password)依然可能连不上。最简单有效的处理方式是让用户在配网页面里填 WiFi 名称时不要漏字母同时让设备在连接失败后打印“找不到该网络”的提示或者在后台先执行一次wlan.scan()把扫描到的 BSSID 拿出来再连接。def connect_with_scan(sta, ssid, password): nets sta.scan() for net in nets: # net 结构: (ssid, bssid, channel, rssi, authmode, hidden) if net[0].decode() ssid: sta.connect(ssid, password) print(已找到网络BSSID:, ubinascii.hexlify(net[1]).decode()) return print(未在扫描列表中找到:, ssid)另外我在 HTTPServer 部分用了cl.settimeout(5)这是为了防止某个客户端连上来之后一直不发完整请求把 HTTP 服务整个拖死。MicroPython 是单线程执行模型这种“僵死连接”一旦出现配网就会彻底卡住超时设置能兜底。5. 配网后的“最后一公里”配置持久化与自动重连5.1 把 WiFi 配置落到文件别让用户配第二次配网成功的最终目标不是这一次能上网而是以后每次开机都能自动连上。所以最直接的方案是把配网得到的 SSID 和密码写到文件里下次开机先读取文件、再发起连接连不上时才重新进入配网模式。MicroPython 使用的也是常规文件系统操作直接open()即可。上面代码里的save_wifi_config用了项目根目录下的wifi.json格式非常简单方便后期加字段比如记录配网时间、AP 的 BSSID 等。值得注意的一个细节是**写文件时先保证配置写入完成再关闭热点。**如果反过来设备可能在文件写入中途就断网断电配置文件损坏下次开机依然要重新配网。不要用f.write()之后立刻ap.active(False)多用几行.flush()或close()也不亏。5.2 开机自动决策能连就连连不上才进配网开机逻辑不应该是“永远进入配网”也不应该是“永远按旧配置连接”而是一个简单的优先级判断import network import json import time def load_wifi_config(): try: with open(wifi.json, r) as f: return json.load(f) except Exception: return None def connect_with_timeout(ssid, password, timeout_s15): sta network.WLAN(network.STA_IF) sta.active(True) sta.connect(ssid, password) deadline time.time() timeout_s while time.time() deadline: if sta.isconnected(): return True time.sleep_ms(200) return sta.isconnected() def boot_decision(): sta network.WLAN(network.STA_IF) cfg load_wifi_config() if cfg and connect_with_timeout(cfg.get(ssid), cfg.get(password)): print(自动重连成功) return print(无可用配置进入配网模式) # 这里调用上一节提供的 start_ap() start_http_server()这样用户第一次拿到设备开机后看到配网页面之后只要没换路由器设备每次都会自动联网整个过程对用户透明。5.3 断线重连状态检测与多级恢复策略光靠开机重连还不够路由器重启、设备断电重启、信道变化都会造成断线。比较稳妥的做法是在主循环里做低频率的状态检测以 30 到 60 秒为周期检查wlan.isconnected()。如果发现离线不要立刻疯狂重连那样会把路由器信道占用得很高我习惯用“三次快重连 一次进入配网”的多级策略def monitor_wifi(): sta network.WLAN(network.STA_IF) fail_count 0 while True: time.sleep_ms(30000) if sta.isconnected(): fail_count 0 continue fail_count 1 if fail_count 3: fail_count 0 cfg load_wifi_config() if cfg: connect_with_timeout(cfg[ssid], cfg[password], timeout_s10) else: # 连不上旧配置停掉 STA进入配网 sta.disconnect() break这里断线重连和开机重连都调用了同一个connect_with_timeout逻辑保持单一入口后续要加“记录重连次数上报服务端”之类的功能也方便。MicroPython 的time.sleep_ms会阻塞整个主进程所以这个监控循环只适合放在业务不复杂的场景如果主程序需要同时处理传感器采集、MQTT 上报等建议改成machine.Timer定时器回调来触发检查。6. 那些年踩过的配网坑排查链路与调试心得6.1 反复失败时我按这个顺序排查很多人在 SmartConfig 失败后第一时间怀疑代码但我的经验是90% 的问题出在环境而不是出在 MicroPython 代码。这是我惯用的排查顺序确认手机连的 WiFi 是 2.4GHz 还是 5GHz。ESP32 / ESP8266 只有 2.4GHz 无线能力SmartConfig 协议也是在 2.4GHz 下设计的。手机连在 5GHz 网络上不管你发多少包设备都听不见。确认路由器的无线设置里没有开“AP 隔离”或“客户端隔离”。这个功能经常出现在访客网络模式里开了之后同一 WiFi 下的设备之间无法通信SmartConfig 报文会被路由器直接丢弃。用另一台手机开启热点把手机连到这个热点上做一次 SmartConfig 测试。这样可以避开复杂路由环境快速验证设备端协议栈是否正常。换一个固件版本再试。不同 MicroPython 固件对esp_touch的实现差异很大有可能是固件 bug。实在不行就切 SoftAP 配网。不是丢人而是成熟产品团队的常用操作。排查的时候一定要开串口日志。MicroPython 的print直接往串口输出配合 REPL 你甚至可以在设备运行中实时观察wlan.isconnected()的变化。永远不要在没有日志的情况下盲试配网。6.2 SoftAP 一直连不上 / 打不开页面的几种情况SoftAP 配网看起来简单实际也会踩不少坑我之前翻车最多的是下面这几种热点打开了手机却搜不到。先确认ap.active(True)之后是不是等了足够时间再确认热点 SSID 里有没有中文字符或特殊字符。部分手机对特殊字符的 SSID 显示异常建议热点名称只用字母、数字、下划线。手机连上了热点但浏览器打不开192.168.4.1。最典型的原因是手机保留了之前连接某个网络的“自动登录”行为或者手机 DNS 缓存指向了别的地址。让用户清一下浏览器缓存或者在手机上手动把 DNS 改成192.168.4.1通常能解决。另外确认一下设备是不是同时开着 STA 模式STA 和 AP 同时工作时IP 分配偶尔会冲突必要时先sta.disconnect()再开 AP。页面能打开但提交表单后页面提示连接失败。这个要分两类看一类是 WiFi 密码确实写错了另一类是路由器配置了 MAC 地址过滤或者白名单。排查时在日志里打一下收到的ssid和password看看内容是否完整、有没有被截断。6.3 几个值得养成的调试习惯第一给配网状态加 LED 指示。我通常用板载 LED 做三种状态快闪表示正在等待配网、慢闪表示正在连接、常亮表示联网成功。用户看不看得懂日志无所谓但灯一亮就能判断设备心情这个体验落差是巨大的。第二给配网模式加超时和出口。设备不能永远停在配网模式里我一般只保持 10 分钟超时后自动关闭热点。同时保留一个物理按键长按 5 秒强制清除配置文件并重启进配网这是最原始的“恢复出厂”但对用户最友好。第三配网成功之后先把配置落盘再给页面返回结果。不少人在测试板上直接连接成功后就不管了一旦重新上电所有配置归零又得配一次。像我之前代码里那样把save_wifi_config放在连接成功分支的最前面这一个小动作能省很多售后沟通。第四把你自己的手机开发热点作为调试基准。开发阶段天天换路由器容易把环境问题混进代码问题。我用自己手机热点调通配网逻辑之后再换真实路由器测试这样每一次变量都很清晰。最后分享一个我自己的体会配网方案的“上限”由协议决定但“下限”由你给用户设计的状态反馈兜底。SmartConfig 确实很酷可它在你无法控制用户路由器环境时并不够稳SoftAP 配网虽然土但每一步都有明确反馈出错时你知道往哪查。真到了量产阶段你会感谢这个“不够酷但足够稳”的选择。