这篇博客档总结我在 HiSpark WS63(LiteOS + lwIP)平台上实现 WiFi AP 配网(强制门户 | Captive Portal)的完整流程,以及开发过程中遇到的关键问题和解决方案。
1. 什么是强制门户
Captive Portal(强制门户)是一种网络认证机制。当设备(手机/电脑)连接到一个新的 WiFi 热点时,操作系统会自动检测该网络是否需要额外认证。如果检测到”有网关在但无法访问互联网”,系统就会自动弹出一个窗口,引导用户访问配网页面。
iOS、Android、Windows 都有各自的检测 URL(如 captive.apple.com、connectivitycheck.gstatic.com、msftconnecttest.com),通过访问这些 URL 来判断网络状态。
在访问的时候,达到以下条件时,系统大概率会弹窗:
- 连接到WiFi热点,拿到IP地址
- DHCP中下发了网关(Router)和 DNS 服务器地址,两个地址和配网用的HTTP服务IP一致
- 访问检测URL时,手机DNS解析被开发板劫持到本地网关IP
- HTTP请求被302重定向到配网页面
2. 整体架构
进入 AP 模式后,代码架构和业务流程如下图所示
3. 代码实现
3.1 强制启动 AP 模式
在 bsp_wifi.c 中,实现bsp_wifi_smart_init() 启动 AP 模式
让开发板开个热点出来给手机:
1 | int bsp_wifi_smart_init(void) { |
热点(AP) 配置参数可以定义在 bsp_wifi.h:
1 |
3.2 AP 网络接口配置
在init_ap_mode() 中设置静态 IP 并启动 DHCP 服务器
这样当手机连接开发版的时候,开发板可以给手机分配ip:
1 | struct netif* netif_p = netifapi_netif_find("ap0"); |
3.3 Captive Portal 服务初始化
在主任务的初始化流程中调用自己实现的:
1 | captive_portal_service_init(); |
该函数创建 captive_portal_task,同时监听两个 socket:
- UDP 53:DNS 查询
- TCP 80:HTTP 请求
使用 lwip_select 实现单线程多路复用:
1 | fd_set read_fds; |
3.4 DNS 劫持
所有 DNS A 记录查询都返回 AP IP(192.168.1.1)
让手机的每个网络请求都指向开发板的配网页面(Http服务):
1 | // 解析 DNS 查询中的域名 |
3.5 HTTP 路由与 302 重定向
这是强制门户(弹出配网页面)的核心代码。
当请求来到开发板ip的时候,需要让手机请求的地址转到 192.168.1.1,而不是直接返回网页内容。
实现上分为两步:
第一步:Host 头检查
只有直接访问小车 IP 的请求才走正常路由,其他一律 走302(重定向):
1 | bool is_direct_ip = false; |
第二步:请求分发
当手机访问开发板上的网页时,跳转到对应的api服务
1 | if (is_get && strcmp(path, "/status") == 0) { |
3.6 配网页面与状态轮询
内嵌在固件中的 HTML 页面包含:
- WiFi 名称(SSID)和密码输入框
- “连接”按钮,通过
POST /config提交 - JavaScript 轮询
GET /status获取连接状态
连接状态由 wifi_switch_task 后台任务维护,通过全局变量 g_captive_status 暴露给 HTTP 服务。
3.7 WiFi 模式切换
当手机完成网页操作,开发板通过手机发来的POST /config 请求接收 SSID 和密码后,程序会创建后台任务 执行网络切换,不阻塞 HTTP 服务:
1 | static void wifi_switch_task(void* arg) { |
4. 遇到的问题及解决方案
问题 1:返回 HTTP 200,地址栏不跳转
现象:手机访问 baidu.com,小车返回了 HTML 内容(HTTP 200),但地址栏仍显示 baidu.com,没有弹窗。
原因:浏览器以为收到的 HTML 就是 baidu.com 的网页内容,自然不会跳转。
解决:必须返回 302 Found,让浏览器主动跳转到小车的 IP 地址:
1 | HTTP/1.1 302 Found |
问题 2:DNS 劫持后,连直接访问 192.168.1.1 也被 302
现象:加了全局 302 后,用户在浏览器地址栏输入 192.168.1.1 也会触发重定向,页面无法显示。
原因:没有区分”DNS 劫持过来的流量”和”用户直接访问小车 IP 的流量”。
解决:检查 HTTP 请求头中的 Host 字段。只有当 Host 是小车 IP 时,才走正常路由:
1 | if (strstr(buf, "Host: 192.168.1.1") == NULL) { |
问题 3:DHCP 没有下发网关(最隐蔽)
现象:手机 WiFi 详情里”路由器/网关”显示 0.0.0.0,系统完全不发起 Captive Portal 探测。串口日志中没有 DNS 查询和 HTTP 请求。
根因分析:
手机连接 WiFi 后,操作系统会检查:
- 是否拿到了 IP 地址?
- 是否拿到了网关(Router)和 DNS?
如果网关是 0.0.0.0,系统认为这是一个”没有互联网能力的局域网设备”,直接静默处理,绝不弹窗。
lwIP 的 DHCP 服务器默认不下发网关(LWIP_DHCPS_GW 默认为 0),导致 DHCP Offer/ACK 报文中没有 Router 选项。
排查曲折:
第一次:在
bsp_wifi.c里加#define CONFIG_DHCPS_GW 1→ 无效
原因:C 宏只在定义它的.c编译单元内有效,dhcps.c编译时看不到。第二次:改到
lwipopts.h→ 仍无效
原因:CMake 中LWIP_CONFIG_FILE被硬编码为"lwip/lwipopts_default.h",lwipopts.h根本没被 lwIP 源文件包含。最终解决:在
lwipopts_default.h中直接定义#define CONFIG_DHCPS_GW 1。
这个文件是实际被LWIP_CONFIG_FILE指定的配置文件,所有 lwIP 源文件都能看到。
lwipopts_default.h 中的关键逻辑:
1 |
当 LWIP_DHCPS_GW = 1 后,dhcps.c 在构造 DHCP Offer/ACK 时就会加入 Router 选项(Option 3),地址为 AP 接口的 IP(192.168.1.1)。同时 LWIP_DHCPS_DNS_OPTION 会确保 DNS 服务器也下发为 192.168.1.1。
5. 关键配置项汇总
| 配置项 | 值 | 说明 |
|---|---|---|
BSP_WIFI_AP_SSID |
"WS63_Robot" |
热点名称 |
BSP_WIFI_AP_PASSWORD |
"" |
开放网络(无密码) |
BSP_WIFI_AP_CHANNEL |
13 |
WiFi 频道 |
| AP 静态 IP | 192.168.1.1 |
固定网关地址 |
CONFIG_DHCPS_GW |
1 |
启用 DHCP 网关下发 |
| DNS 端口 | 53 |
标准 DNS 端口 |
| HTTP 端口 | 80 |
标准 HTTP 端口 |
| DHCP 租约起始偏移 | 2 |
客户端 IP 从 192.168.1.2 开始分配 |
6. 调试技巧
查看手机是否拿到网关
手机连接 WS63_Robot 后,在 WiFi 详情中查看:
- 路由器/网关:必须是
192.168.1.1 - DNS:必须是
192.168.1.1
如果显示 0.0.0.0,说明 LWIP_DHCPS_GW 未生效,DHCP 配置有问题。
串口日志观察点
1 | [WiFi] AP 热点已启用: SSID=WS63_Robot, 开放网络(无密码), 频道=13 |
如果没有看到 [Portal] DNS 查询 日志,说明手机根本没发起探测 → 检查 DHCP 网关。
手动测试
- 手机连接热点后,不要等弹窗,直接在浏览器地址栏输入
192.168.1.1 - 如果能看到配网页面,说明 HTTP 服务正常
- 如果看不到,检查串口是否有
客户端连接日志 - 在浏览器里访问
http://neverssl.com,观察是否 302 跳转到192.168.1.1
7. 总结
实现 Captive Portal 的关键不是”搭一个网页”,而是**让操作系统”认为这个网络需要认证登陆”**。这要求:
- DNS 劫持:让所有域名解析到本地 IP
- 302 重定向:把劫持流量引导到配置页面
- DHCP 网关下发:这是最容易被忽略的一点——手机必须拿到网关和 DNS,系统才会发起探测
如果只做了前两点而第三点缺失,结果就是:手机静默连接,没有任何弹窗,用户必须手动打开浏览器输入 IP 才能配网——体验大打折扣。
