基于LVGL与SoftAP的嵌入式Wi-Fi屏幕配网方案实现
1. 项目概述:一块屏幕,让物联网设备“开口说话”
最近在捣鼓安信可的AiPi-Eyes-S1开发板,这玩意儿集成了Wi-Fi和蓝牙,还自带一块小巧的LCD屏幕,是入门物联网开发的绝佳玩具。但我在给它配网(连接Wi-Fi)时,遇到了一个很典型的问题:传统的智能硬件配网,要么需要设备先开启一个热点,手机连上去再配置;要么就得靠串口敲AT指令。前者对用户不友好,后者对开发者调试还行,但对最终产品来说简直是灾难。
于是,我萌生了一个想法:能不能利用板载的那块屏幕,直接在上面显示一个二维码或者一个输入界面,让用户像操作手机一样,直接输入Wi-Fi名称和密码来完成连接?这个想法听起来简单,但要把“屏幕输入”这个在手机上习以为常的功能,在资源极其有限的嵌入式MCU上跑起来,里面涉及到的技术栈和设计思路还挺有意思的。今天,我就把这个从零开始实现“屏幕输入连接Wi-Fi”的全过程,包括踩过的坑、绕过的弯,以及最终跑通的方案,完整地分享出来。
这个项目非常适合刚接触嵌入式开发,特别是对RTOS、LVGL图形库或者物联网配网流程感兴趣的朋友。即使你之前没碰过安信可的板子,只要你有C语言基础,对单片机有点了解,这套思路和代码框架也能很容易地迁移到其他带屏的物联网开发板上。整个过程,我们会从最基础的开发环境搭建讲起,一直深入到LVGL控件事件处理、Wi-Fi连接状态机设计等核心环节。
2. 核心思路与方案选型:为什么是LVGL+SoftAP?
拿到一块带屏的物联网开发板,想实现屏幕输入,首先得解决两个根本问题:第一,如何在屏幕上绘制出美观、可交互的界面(键盘、输入框、按钮);第二,设备在未联网时,如何接收来自手机或屏幕的配置信息。
2.1 图形界面方案:LVGL是不二之选
对于第一个问题,在嵌入式领域,LVGL(Light and Versatile Graphics Library)几乎是当前开源界的事实标准。它是一个用C语言编写的高度可裁剪的图形库,资源占用小,功能却非常强大,支持按钮、标签、键盘、列表等丰富的控件,以及流畅的动画和事件系统。AiPi-Eyes-S1使用的博流BL606P芯片,主频足够,且官方SDK已经适配了LVGL,这为我们省去了最痛苦的底层驱动移植工作。
所以,我们的图形方案很明确:基于LVGL,创建一个包含以下元素的界面:
- 两个文本输入框(
lv_textarea):分别用于输入SSID(Wi-Fi名称)和密码。 - 一个虚拟键盘(
lv_keyboard):当点击输入框时自动弹出。 - 一个连接按钮(
lv_btn):用于触发连接动作。 - 几个标签(
lv_label):用于显示提示信息和连接状态。
注意:在资源紧张的MCU上,要特别注意LVGL的内存配置。官方SDK通常会有默认配置,但如果你的界面比较复杂,出现了花屏或卡顿,可能需要去
lv_conf.h文件中调整LV_MEM_SIZE(内存池大小)和LV_DISP_DEF_REFR_PERIOD(屏幕刷新周期)等参数。
2.2 网络配置传输方案:SoftAP模式的权衡
第二个问题,即“如何传输配置信息”,通常有几种主流方案:
- 蓝牙配网(BLE):设备作为蓝牙外围设备,手机App扫描连接后发送配置。优点是不依赖网络,功耗低。缺点是需开发手机端App,用户流程多一步。
- 智能配网(SmartConfig):设备监听空中的特定格式无线报文,手机App发送包含Wi-Fi信息的加密包。优点是用户无需切换网络。缺点是对路由器环境有要求,兼容性有时不佳。
- 设备热点(SoftAP)模式:设备自身开启一个Wi-Fi热点,手机连接此热点后,通过HTTP服务器或Socket通信将配置信息发送给设备。优点是协议简单、可靠,只需一个简单的网页或POST请求即可完成,兼容性极好。
对于我们的“屏幕输入”场景,SoftAP模式是现阶段更直接、更可靠的选择。原因如下:
- 逻辑闭环:我们的核心是在设备屏幕上操作。设备开启热点后,屏幕界面负责收集Wi-Fi信息。这实际上是把配置信息的“输入界面”和“接收终端”都集成在了设备本身,逻辑非常清晰。
- 简化开发:我们无需额外开发手机App,也无需处理复杂的SmartConfig解码。只需要在设备端建立一个最简单的HTTP服务器,接收一个POST请求即可。
- 高可靠性:SoftAP是标准的Wi-Fi协议,几乎不存在兼容性问题。只要手机能搜到这个热点,就能完成配置。
因此,整体方案流程图可以概括为:
- 设备启动,初始化LVGL,绘制输入界面。
- 同时,设备Wi-Fi模块进入SoftAP模式,创建一个配置用的热点(如
AiPi-Eyes-XXXX)。 - 用户在设备屏幕的输入框内,用虚拟键盘输入家中路由器的SSID和密码。
- 用户点击“连接”按钮。
- 设备内部的程序将屏幕输入的SSID和密码,通过HTTP客户端请求,发送给设备自身SoftAP模式下的本地HTTP服务器(
http://192.168.4.1/configure)。 - 本地HTTP服务器接收到配置信息,解析出SSID和密码。
- 设备Wi-Fi模式从SoftAP切换为STA(站点)模式,并使用解析到的凭据去连接目标路由器。
- 连接成功或失败的结果,实时更新在设备屏幕上。
这个方案巧妙地将“输入”和“接收”都在单板上完成了,屏幕是交互前端,SoftAP+HTTP服务是数据传输后端。
3. 开发环境搭建与工程初始化
工欲善其事,必先利其器。安信可为AiPi-Eyes-S1提供了基于VSCode的集成开发环境,这大大降低了入门门槛。
3.1 工具链安装与SDK获取
首先,你需要安装以下软件:
- VSCode:从官网下载安装。
- 安信可一体化开发环境插件:在VSCode扩展商店中搜索“安信可”进行安装。这个插件集成了编译器、烧录工具和项目创建向导。
- Git:用于克隆SDK代码。
安装完插件后,在VSCode中通过命令面板(Ctrl+Shift+P)运行AiPi: Create Project from Example。在弹出的列表中,选择AiPi-Eyes-S1相关的示例工程。这里我建议选择helloworld或lvgl_example作为起点,因为它们已经包含了LVGL和屏幕驱动的基础配置,能避免从零开始配环境的诸多麻烦。
插件会自动将SDK代码克隆到本地,并配置好编译环境。工程目录结构通常如下:
your_project/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── src/ │ └── main.c # 主程序入口 ├── components/ # 组件目录,如lvgl、wifi、http等 ├── build/ # 编译输出目录 └── config/ # 板级配置3.2 关键组件配置检查
工程创建好后,有几处配置需要确认或修改,它们藏在CMakeLists.txt和config目录下的文件中。
- Wi-Fi功能使能:在
main/CMakeLists.txt或项目根目录的CMakeLists.txt中,确保包含了Wi-Fi组件。通常会有类似add_component(wifi)的语句。 - LVGL配置:LVGL的详细配置在
components/lvgl/lv_conf.h。对于我们的项目,需要确保以下功能被启用(值为1):#define LV_USE_TEXTAREA 1 // 启用文本输入框 #define LV_USE_KEYBOARD 1 // 启用键盘 #define LV_USE_BTN 1 // 启用按钮 #define LV_USE_LABEL 1 // 启用标签 #define LV_USE_LOG 1 // 启用日志,调试时非常有用 - 文件系统支持:如果后续想保存Wi-Fi配置(避免每次重启都重配),可能需要用到文件系统。检查
components/vfs是否被添加。
完成这些基础检查后,你可以尝试编译并烧录一个示例程序到开发板,确保屏幕能亮,串口有日志输出,这代表你的开发环境已经就绪。
4. 核心模块实现详解
环境准备好后,我们开始动手编码。整个项目可以拆解为四个核心模块:LVGL界面构建、SoftAP与HTTP服务器、Wi-Fi连接管理、以及连接所有环节的业务逻辑。
4.1 LVGL界面设计与事件处理
我们在main.c或单独的文件中创建界面。首先初始化LVGL并注册显示和输入设备驱动(这些通常在SDK的示例代码中已有,我们直接调用即可)。
// 界面创建函数 static void ui_create(void) { // 1. 创建底层容器,铺满屏幕 lv_obj_t *scr = lv_scr_act(); lv_obj_set_style_bg_color(scr, lv_color_hex(0xf0f0f0), LV_PART_MAIN); // 设置背景色 // 2. 创建SSID输入框和标签 lv_obj_t *label_ssid = lv_label_create(scr); lv_label_set_text(label_ssid, "Wi-Fi名称 (SSID):"); lv_obj_align(label_ssid, LV_ALIGN_TOP_LEFT, 20, 30); lv_obj_t *ta_ssid = lv_textarea_create(scr); lv_obj_set_size(ta_ssid, 200, 40); lv_obj_align_to(ta_ssid, label_ssid, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); lv_textarea_set_placeholder_text(ta_ssid, "请输入路由器名称"); lv_textarea_set_one_line(ta_ssid, true); // 单行输入 // 3. 创建密码输入框和标签 lv_obj_t *label_pwd = lv_label_create(scr); lv_label_set_text(label_pwd, "Wi-Fi密码:"); lv_obj_align_to(label_pwd, ta_ssid, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 25); lv_obj_t *ta_pwd = lv_textarea_create(scr); lv_obj_set_size(ta_pwd, 200, 40); lv_obj_align_to(ta_pwd, label_pwd, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); lv_textarea_set_placeholder_text(ta_pwd, "请输入密码"); lv_textarea_set_one_line(ta_pwd, true); lv_textarea_set_password_mode(ta_pwd, true); // 密码模式,显示为圆点 lv_textarea_set_password_show_time(ta_pwd, 1000); // 长按显示明文1秒 // 4. 创建连接按钮 lv_obj_t *btn_connect = lv_btn_create(scr); lv_obj_set_size(btn_connect, 100, 50); lv_obj_align(btn_connect, LV_ALIGN_BOTTOM_MID, 0, -50); lv_obj_t *label_btn = lv_label_create(btn_connect); lv_label_set_text(label_btn, "连接"); lv_obj_center(label_btn); // 5. 创建状态显示标签 lv_obj_t *label_status = lv_label_create(scr); lv_label_set_text(label_status, "状态: 等待输入"); lv_obj_align(label_status, LV_ALIGN_BOTTOM_LEFT, 20, -20); lv_obj_set_style_text_color(label_status, lv_color_hex(0x666666), LV_PART_MAIN); // 6. 创建虚拟键盘,并初始化为隐藏 lv_obj_t *kb = lv_keyboard_create(scr); lv_keyboard_set_textarea(kb, NULL); // 先不关联任何输入框 lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); // 隐藏键盘 // 7. 为输入框添加点击事件,用于弹出键盘 lv_obj_add_event_cb(ta_ssid, ta_event_cb, LV_EVENT_CLICKED, kb); lv_obj_add_event_cb(ta_pwd, ta_event_cb, LV_EVENT_CLICKED, kb); // 为键盘添加关闭事件 lv_obj_add_event_cb(kb, kb_event_cb, LV_EVENT_READY, NULL); lv_obj_add_event_cb(kb, kb_event_cb, LV_EVENT_CANCEL, NULL); // 8. 为连接按钮添加点击事件 lv_obj_add_event_cb(btn_connect, connect_btn_event_cb, LV_EVENT_CLICKED, NULL); // 将关键对象的指针保存到全局结构体,方便其他地方访问 ui_elements.ta_ssid = ta_ssid; ui_elements.ta_pwd = ta_pwd; ui_elements.label_status = label_status; ui_elements.kb = kb; }事件回调函数是交互的灵魂。这里给出键盘和按钮事件的简化示例:
// 输入框点击事件:弹出键盘并关联到当前输入框 static void ta_event_cb(lv_event_t *e) { lv_obj_t *ta = lv_event_get_target(e); lv_obj_t *kb = lv_event_get_user_data(e); lv_keyboard_set_textarea(kb, ta); // 将键盘与当前点击的输入框绑定 lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN); // 显示键盘 } // 键盘的“确定”或“取消”事件:隐藏键盘 static void kb_event_cb(lv_event_t *e) { lv_obj_t *kb = lv_event_get_target(e); lv_keyboard_set_textarea(kb, NULL); // 解除键盘与输入框的关联 lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); // 隐藏键盘 } // 连接按钮点击事件 static void connect_btn_event_cb(lv_event_t *e) { // 1. 从输入框获取用户输入的文本 const char *ssid = lv_textarea_get_text(ui_elements.ta_ssid); const char *password = lv_textarea_get_text(ui_elements.ta_pwd); // 2. 简单的输入验证 if(strlen(ssid) == 0) { lv_label_set_text(ui_elements.label_status, "状态: SSID不能为空"); return; } // 3. 更新状态提示 lv_label_set_text(ui_elements.label_status, "状态: 正在配置..."); // 4. 将获取到的SSID和密码,通过HTTP POST发送给本地的SoftAP服务器 // 这里假设我们有一个函数 `send_wifi_config_to_local()` 来实现 send_wifi_config_to_local(ssid, password); }实操心得:LVGL的对象管理要清晰。最好定义一个全局结构体来存放所有需要跨函数访问的UI对象指针(如
ui_elements),这比用lv_obj_get_child()遍历查找要可靠和高效得多。另外,LVGL运行在它自己的任务(线程)里,如果从其他任务(如网络事件回调)中更新UI,必须使用lv_timer_create()创建定时器回调,或者在网络回调中通过消息队列通知LVGL任务,绝对禁止在非LVGL任务中直接调用lv_label_set_text()等UI函数,否则会导致系统崩溃。
4.2 SoftAP模式与简易HTTP服务器实现
接下来,我们需要让设备开启一个热点,并建立一个能接收配置的HTTP服务器。安信可SDK通常基于博流原生的Wi-Fi API和常见的网络组件(如httpd)进行封装。
#include <bl_wifi.h> // 博流Wi-Fi API #include <httpd.h> // HTTP服务器组件 static char g_ap_ssid[32] = "AiPi-Eyes-Config"; static char g_ap_pass[64] = "12345678"; // 简单密码,方便连接 static struct httpd_ctx *g_httpd_ctx = NULL; // 启动SoftAP static int start_softap(void) { bl_wifi_ap_init(); // 初始化AP模式 // 设置AP参数 bl_wifi_ap_setup((uint8_t *)g_ap_ssid, strlen(g_ap_ssid), (uint8_t *)g_ap_pass, strlen(g_ap_pass), 1, // 信道 0); // 不隐藏SSID bl_wifi_ap_start(); // 启动AP printf("[Wi-Fi] SoftAP started: SSID=%s, IP=192.168.4.1\r\n", g_ap_ssid); return 0; } // HTTP请求处理回调 static int wifi_config_handler(struct httpd_connection *conn) { // 只处理POST请求 if (conn->request_method != HTTPD_METHOD_POST) { httpd_send_response(conn, 405, "Method Not Allowed"); return -1; } // 检查URL路径,例如 /configure if (strcmp(conn->uri, "/configure") != 0) { httpd_send_response(conn, 404, "Not Found"); return -1; } // 从请求体中获取POST数据(格式假设为 ssid=YourSSID&password=YourPass) char *body = conn->body_data; printf("[HTTP] Received POST data: %s\r\n", body); // 简易解析(生产环境建议使用更健壮的解析器) char ssid[33] = {0}; char password[65] = {0}; // 这里应实现一个简单的URL解码和键值对解析函数 parse_form_data if (parse_form_data(body, "ssid", ssid, sizeof(ssid)) && parse_form_data(body, "password", password, sizeof(password))) { printf("[HTTP] Parsed SSID: %s, Password: %s\r\n", ssid, password); // 将解析到的凭证存入全局变量或消息队列,供主循环处理 wifi_config_received(ssid, password); // 返回成功响应 httpd_send_response(conn, 200, "OK"); httpd_send_body(conn, "Configuration received. Connecting...", -1); } else { httpd_send_response(conn, 400, "Bad Request"); httpd_send_body(conn, "Invalid form data", -1); } return 0; } // 启动HTTP服务器 static int start_http_server(void) { struct httpd_ops ops = { .handler = wifi_config_handler, }; g_httpd_ctx = httpd_create(&ops, NULL); if (!g_httpd_ctx) { printf("[HTTP] Server creation failed!\r\n"); return -1; } httpd_start(g_httpd_ctx, 80); // 监听80端口 printf("[HTTP] Server started on port 80\r\n"); return 0; }在主初始化函数中,依次调用start_softap()和start_http_server()。这样,设备启动后,手机就能搜索到名为AiPi-Eyes-Config的热点,连接后理论上可以访问http://192.168.4.1,不过我们只处理/configure这个配置接口。
4.3 Wi-Fi连接状态机与配置发送
现在,界面可以输入,服务器可以接收,我们需要一个“桥梁”把两者连起来。这个桥梁就是:当用户在屏幕点击“连接”后,程序将SSID和密码,以HTTP客户端的身份,发送给http://192.168.4.1/configure。
这听起来有点奇怪:自己给自己发数据?没错,因为设备同时运行着SoftAP(IP是192.168.4.1)和STA(尚未连接)。从STA接口发往自身AP接口的请求,是可以通过环回网络走的。
// 模拟HTTP客户端发送配置(实际使用需根据SDK提供的HTTP客户端库调整) static void send_wifi_config_to_local(const char *ssid, const char *password) { char post_data[256]; // 构造表单数据,注意要对值进行URL编码 snprintf(post_data, sizeof(post_data), "ssid=%s&password=%s", url_encode(ssid), url_encode(password)); // 使用HTTP客户端库发送POST请求 // 这里以伪代码示意,实际调用如 http_client_post("http://192.168.4.1/configure", post_data, ...); int ret = http_client_post("http://192.168.4.1/configure", post_data, strlen(post_data), NULL, // 回调 NULL); // 用户数据 if (ret == 0) { printf("[HTTP Client] Config sent successfully.\r\n"); } else { lv_label_set_text(ui_elements.label_status, "状态: 发送配置失败"); printf("[HTTP Client] Failed to send config: %d\r\n", ret); } }当HTTP服务器wifi_config_handler收到数据并解析成功后,它会调用wifi_config_received(ssid, password)。这个函数负责触发真正的Wi-Fi连接流程。
Wi-Fi连接需要设计为一个简单的状态机,避免阻塞主循环:
typedef enum { WIFI_STATE_IDLE, WIFI_STATE_CONNECTING, WIFI_STATE_CONNECTED, WIFI_STATE_GOT_IP, WIFI_STATE_FAILED } wifi_state_t; static wifi_state_t g_wifi_state = WIFI_STATE_IDLE; static char g_target_ssid[33] = {0}; static char g_target_password[65] = {0}; void wifi_config_received(const char *ssid, const char *password) { strncpy(g_target_ssid, ssid, sizeof(g_target_ssid)-1); strncpy(g_target_password, password, sizeof(g_target_password)-1); g_wifi_state = WIFI_STATE_CONNECTING; // 可以在这里触发一个连接任务或设置一个标志位 } // 在主线循环或一个独立任务中处理Wi-Fi状态机 void wifi_state_machine_task(void) { switch(g_wifi_state) { case WIFI_STATE_CONNECTING: printf("[Wi-Fi] Connecting to %s...\r\n", g_target_ssid); lv_label_set_text(ui_elements.label_status, "状态: 连接中..."); // 调用SDK的Wi-Fi连接函数 if (bl_wifi_sta_init() == 0 && bl_wifi_sta_setup((uint8_t *)g_target_ssid, strlen(g_target_ssid), (uint8_t *)g_target_password, strlen(g_target_password)) == 0 && bl_wifi_sta_connect() == 0) { g_wifi_state = WIFI_STATE_CONNECTED; } else { g_wifi_state = WIFI_STATE_FAILED; lv_label_set_text(ui_elements.label_status, "状态: 连接失败"); } break; case WIFI_STATE_CONNECTED: // 等待获取IP地址,SDK通常有回调通知 // 这里简化处理,假设连接成功即认为OK lv_label_set_text(ui_elements.label_status, "状态: 已连接,获取IP中"); // 模拟一个延时后进入下一状态 vTaskDelay(2000 / portTICK_PERIOD_MS); g_wifi_state = WIFI_STATE_GOT_IP; break; case WIFI_STATE_GOT_IP: printf("[Wi-Fi] Connected and got IP.\r\n"); lv_label_set_text(ui_elements.label_status, "状态: Wi-Fi连接成功!"); // 可以在这里关闭SoftAP和HTTP服务器以节省资源 // httpd_stop(g_httpd_ctx); // bl_wifi_ap_stop(); g_wifi_state = WIFI_STATE_IDLE; break; case WIFI_STATE_FAILED: // 连接失败,可以重置状态,允许用户重试 printf("[Wi-Fi] Connection failed.\r\n"); // 可选:清空输入框 // lv_textarea_set_text(ui_elements.ta_ssid, ""); // lv_textarea_set_text(ui_elements.ta_pwd, ""); g_wifi_state = WIFI_STATE_IDLE; break; default: break; } }将wifi_state_machine_task()放入主循环或一个低优先级的FreeRTOS任务中周期性执行,即可驱动整个连接过程。
5. 系统整合与调试要点
将上述模块组合起来,主函数的逻辑就清晰了:
void main(void) { // 1. 硬件初始化(时钟、串口、GPIO等,SDK通常有封装) board_init(); // 2. 初始化LVGL并创建UI lv_init(); lv_port_disp_init(); // 显示驱动初始化 lv_port_indev_init(); // 输入设备(触摸屏)初始化 ui_create(); // 3. 启动SoftAP和HTTP服务器 start_softap(); start_http_server(); // 4. 打印提示信息到串口 printf("\r\n\r\n"); printf("===================================\r\n"); printf("AiPi-Eyes-S1 Wi-Fi Config Demo\r\n"); printf("SoftAP SSID: %s\r\n", g_ap_ssid); printf("Connect to it and input Wi-Fi info on screen.\r\n"); printf("===================================\r\n"); // 5. 主循环 while (1) { lv_timer_handler(); // 处理LVGL任务,必须周期性调用 wifi_state_machine_task(); // 处理Wi-Fi连接状态机 vTaskDelay(10 / portTICK_PERIOD_MS); // 让出CPU } }5.1 调试过程中常见的坑与解决方案
LVGL界面卡死或无响应
- 可能原因:
lv_timer_handler()没有被周期性调用。必须确保它在主循环或一个高优先级任务中每隔几毫秒执行一次。 - 排查:检查是否在某个地方有长时间的阻塞操作(如
while(1)或vTaskDelay(1000))。所有耗时操作都应拆分成状态机或放到独立任务中。
- 可能原因:
触摸屏不准或没反应
- 可能原因:触摸屏驱动初始化失败或校准数据不对。
- 解决:首先确认
lv_port_indev_init()被正确调用。AiPi-Eyes-S1的SDK示例中通常有触摸屏测试程序,可以先烧录测试,确认硬件正常。如果触摸有偏移,需要在驱动代码中查找校准矩阵或参数进行调整。
SoftAP启动失败,手机搜不到热点
- 可能原因:Wi-Fi驱动初始化顺序错误,或信道设置不合法。
- 解决:确保先调用
bl_wifi_ap_init()再调用bl_wifi_ap_setup()和bl_wifi_ap_start()。信道通常设置为1-13。可以通过串口日志查看AP启动的返回值。
HTTP服务器无法访问或POST失败
- 可能原因:防火墙或路由问题(在自组网环境下较少见),更可能是服务器绑定IP错误或端口被占用。
- 排查:
- 在设备上使用
ifconfig或类似命令查看SoftAP的IP地址是否正确(应为192.168.4.1)。 - 确保手机已成功连接到设备热点,并且手机获取到的IP是192.168.4.x网段。
- 在电脑上使用串口调试助手,查看设备打印的HTTP访问日志,确认请求是否到达。
- 在设备上使用
自己POST给自己收不到数据
- 可能原因:HTTP客户端请求的URL或端口错误;网络栈处理环回地址异常。
- 解决:确保POST的目标URL是
http://192.168.4.1:80/configure。有些嵌入式网络栈对localhost或127.0.0.1的支持更好,也可以尝试http://127.0.0.1/configure。最稳妥的方式是,在发送POST请求前,先让设备作为客户端用GET方法访问一下自己的服务器,测试通路是否正常。
Wi-Fi STA连接总是失败
- 可能原因:密码错误;路由器加密方式不支持(如WPA3);信号太弱。
- 排查:
- 首先在手机上确认SSID和密码是否正确。
- 查看串口日志,SDK的Wi-Fi连接函数通常会返回具体的错误码(如密码错误、找不到AP等)。
- 尝试将路由器加密方式改为WPA2-PSK(最通用)。
- 让设备靠近路由器。
6. 功能优化与扩展思路
基础功能跑通后,可以考虑以下几个优化方向,让项目更完善、更实用:
配置信息保存:连接成功后,将SSID和密码加密后保存到SPI Flash的文件系统中。下次设备上电时,优先尝试自动连接保存的网络,失败后再进入SoftAP配网模式。这需要引入文件系统(如LittleFS)和简单的加密/解密(如AES)。
网页配网作为备用方案:虽然我们主打屏幕输入,但保留网页配网作为备用方案能提升用户体验。可以在HTTP服务器上创建一个简单的配置页面(HTML),当手机连接设备热点后,浏览器自动弹出或手动输入192.168.4.1即可打开页面进行配置。这样即使屏幕损坏,也能完成配网。
更美观的UI与交互:使用LVGL的样式系统,美化按钮、输入框。添加连接动画(如旋转的加载图标)。在输入密码时,提供“显示/隐藏”密码的切换按钮。
多网络协议支持:在HTTP服务器基础上,同时支持更轻量的UDP广播发现和配置协议,或者兼容简单的蓝牙配网指令,增加配网方式的灵活性。
超时与错误处理:为配网过程增加超时机制(如2分钟内未完成配置则重启)。对HTTP POST的数据进行更严格的校验和防溢出处理。
实现这个项目的真正价值,不在于复现一个配网功能,而在于理解如何在一个资源受限的MCU上,协调图形界面、网络服务、用户输入、状态管理等多个异步事件源。它涉及RTOS的任务设计思想、事件驱动编程、模块化设计等嵌入式开发的核心概念。当你把屏幕上的一个点击动作,最终转换成路由器上的一个成功连接时,这种打通软硬件各层的成就感,正是嵌入式开发的乐趣所在。
