基于ESP32S3的智能终端项目--4.2 FreeRTOS 任务调度&WiFi功能
目录
为 ESP32-S3 智能终端添加 WiFi 连接功能(基于 FreeRTOS 与 LVGL)
2. SquareLine Studio 中的界面设计编辑
5.1 WiFi 开关事件 ui_event_OnOffWiFi
5.2 滚动列表选择事件 ui_event_RollerWiFi
5.3 “输入密码”按钮事件 ui_event_EnterPass
5.4 键盘完成事件 ui_event_KeyboardPass
5.5 取消按钮事件 ui_event_CancelEnterPass
项目演示:
wifi
为 ESP32-S3 智能终端添加 WiFi 连接功能(基于 FreeRTOS 与 LVGL)
在上一篇文章中,我们使用 FreeRTOS 将 LVGL 刷新任务独立到 Core 1,实现了流畅的 GUI。现在,我们来为智能终端添加 WiFi 连接功能,让设备能够扫描附近网络、输入密码并连接,同时在界面上实时显示状态。本文将继续沿用 FreeRTOS 多任务架构,将 WiFi 处理放在 Core 0 的后台任务中,保证界面不卡顿。
1. WiFi 功能概述
我们希望实现以下交互流程:
-
用户点击 WiFi 开关(
ui_OnOffWiFi),显示 WiFi 设置面板。 -
面板中包含一个滚动列表(
ui_RollerWiFi)用于显示扫描到的 WiFi 名称,一个“输入密码”按钮(ui_EnterPass),以及一个状态标签(ui_LabelWiFiState)。 -
点击“输入密码”后弹出密码输入面板(
ui_PanelPass),包含文本框(ui_TextAreaWiFiPass)和键盘(ui_KeyboardPass),用户输入密码后点击键盘“完成”触发连接。 -
连接过程中显示等待动画(
ui_SpinnerWiFi),连接成功或失败后更新状态标签。
2. SquareLine Studio 中的界面设计

根据截图,我们需要在“设置”屏幕(ui_Set)中创建以下关键部件(命名与代码保持一致):
| 部件类型 | 变量名 | 作用 |
|---|---|---|
| 开关 | ui_OnOffWiFi |
开启/关闭 WiFi 功能,切换面板可见性 |
| 滚动列表 | ui_RollerWiFi |
显示扫描到的 WiFi 名称,供用户选择 |
| 按钮 | ui_EnterPass |
点击后弹出密码输入面板 |
| 标签 | ui_LabelWiFiState |
显示当前连接状态(已连接/未连接/连接中) |
| 面板 | ui_PanelPass |
密码输入面板容器 |
| 文本框 | ui_TextAreaWiFiPass |
输入 WiFi 密码 |
| 键盘 | ui_KeyboardPass |
虚拟键盘,输入完成后点击“Ready”触发连接 |
| 旋转器 | ui_SpinnerWiFi |
连接等待动画 |
| 标签 | ui_LabelWiFiName |
显示当前选中的 WiFi 名称 |
此外,还有一个“取消”按钮(ui_CancelEnterPass)用于关闭密码面板。
在 SquareLine Studio 中设计好这些部件后,导出 UI 代码,并将其移植到 PlatformIO 项目中(参考上一篇文章)。
3. 全局数据结构(config.h)
我们需要在 config.h 中定义保存 WiFi 状态的结构体,并声明全局变量:
c
// config.h
#ifndef CONFIG_H
#define CONFIG_H
#include <Arduino.h>
#include <WiFi.h>
typedef struct {
int screen_light; // 屏幕亮度(已实现)
String WiFi_scan_one; // 临时存储单个扫描结果
String WiFi_scan_all; // 存储所有扫描结果(用换行分隔)
bool WiFi_button_flag; // 是否需要重新扫描(控制滚动列表显示)
char WiFi_name[64]; // 当前选中的 WiFi 名称
const char* WiFi_password; // 输入的密码(注意:指向的是文本框内容,需谨慎)
bool WiFi_connect_stauts; // 是否已连接(true=已连接)
bool WiFi_connect_flag; // 是否需要发起连接(由键盘“完成”触发)
int WiFi_connect_timeout; // 连接超时次数(每次 100ms,默认 50 次即 5 秒)
} cfg_set;
extern cfg_set Set;
#endif
在 config.cpp 中初始化:
c
#include "config.h"
cfg_set Set = {
.screen_light = 100,
.WiFi_scan_one = "",
.WiFi_scan_all = "",
.WiFi_button_flag = true,
.WiFi_name = "",
.WiFi_password = "",
.WiFi_connect_stauts = false,
.WiFi_connect_flag = false,
.WiFi_connect_timeout = 50 // 5秒(每次延时100ms)
};
注意:WiFi_password 使用了 const char*,直接指向文本框返回的指针,但文本框内容可能被释放,实际项目中建议使用固定字符数组并复制内容。为简化,这里保留原样。
4. 主要函数实现
4.1 WiFi 扫描函数 WiFi_scan()
调用 WiFi.scanNetworks() 扫描周围网络,将结果拼接成字符串(每行一个 SSID),存储到 Set.WiFi_scan_all 中,并更新滚动列表。
cpp
void WiFi_scan() {
int n = WiFi.scanNetworks();
if (n == 0) {
Set.WiFi_scan_all = "附近无可用WiFi\n";
} else {
Set.WiFi_scan_all = ""; // 清空
for (int i = 0; i < n; ++i) {
Set.WiFi_scan_one = WiFi.SSID(i) + "\n";
Set.WiFi_scan_all += Set.WiFi_scan_one;
}
Set.WiFi_button_flag = 0; // 扫描完成,清除扫描标志
}
// 更新滚动列表(由 WiFi_update 负责调用)
}
4.2 WiFi 连接函数 WiFi_connect()
使用 WiFi.begin(ssid, password) 尝试连接,循环检测状态,超时后根据结果更新 UI。
cpp
void WiFi_connect() {
WiFi.begin(Set.WiFi_name, Set.WiFi_password);
for (int i = 0; i < Set.WiFi_connect_timeout; ++i) {
uint32_t connect_status = WiFi.status();
if (connect_status == WL_CONNECTED) {
Set.WiFi_connect_stauts = 1;
break;
} else {
Set.WiFi_connect_stauts = 0;
}
vTaskDelay(100 / portTICK_PERIOD_MS); // 延时100ms
}
// 连接完成后更新 UI
if (Set.WiFi_connect_stauts) {
// 连接成功:更新状态标签、开关状态,并可触发时间同步等(暂略)
lv_label_set_text_fmt(ui_LabelWiFiState, "已连接 %s", WiFi.SSID());
lv_obj_add_state(ui_OnOffWiFi, LV_STATE_CHECKED);
// 隐藏等待动画
lv_obj_add_flag(ui_SpinnerWiFi, LV_OBJ_FLAG_HIDDEN);
} else {
lv_label_set_text(ui_LabelWiFiState, "连接失败");
if (lv_obj_has_state(ui_OnOffWiFi, LV_STATE_CHECKED)) {
lv_obj_clear_state(ui_OnOffWiFi, LV_STATE_CHECKED);
}
lv_obj_add_flag(ui_SpinnerWiFi, LV_OBJ_FLAG_HIDDEN);
}
Set.WiFi_connect_flag = 0; // 清除连接请求标志
}
4.3 WiFi 状态更新函数 WiFi_update()
该函数由后台任务周期性调用(例如每 50ms),负责:
-
检查是否有连接请求(
Set.WiFi_connect_flag),若有则调用WiFi_connect()。 -
根据当前连接状态更新 UI 标签和开关状态。
-
根据
Set.WiFi_button_flag决定是否显示“正在扫描”提示或显示扫描结果。
cpp
void WiFi_update() {
// 1. 处理连接请求
if (Set.WiFi_connect_flag) {
WiFi_connect();
}
// 2. 根据连接状态更新 UI
if (Set.WiFi_connect_stauts || WiFi.status() == WL_CONNECTED) {
lv_label_set_text_fmt(ui_LabelWiFiState, "已连接 %s", WiFi.SSID());
lv_obj_add_state(ui_OnOffWiFi, LV_STATE_CHECKED);
} else {
lv_label_set_text(ui_LabelWiFiState, "未连接");
if (lv_obj_has_state(ui_OnOffWiFi, LV_STATE_CHECKED)) {
lv_obj_clear_state(ui_OnOffWiFi, LV_STATE_CHECKED);
}
}
// 3. 更新滚动列表(根据扫描标志)
if (Set.WiFi_button_flag == 1) {
// 显示“正在扫描”提示,并执行扫描
lv_roller_set_options(ui_RollerWiFi, "正在扫描附近WiFi...\n", LV_ROLLER_MODE_NORMAL);
WiFi_scan(); // 扫描后 Set.WiFi_button_flag 会被置 0
} else {
// 显示扫描结果
lv_roller_set_options(ui_RollerWiFi, Set.WiFi_scan_all.c_str(), LV_ROLLER_MODE_NORMAL);
}
}
4.4 开机自动连接上次 WiFi(WiFi_init)
在设备启动时,可以尝试连接之前保存的 WiFi(如果有)。这里简单使用 WiFi.begin() 无参数,它会尝试连接上次成功连接的 AP。
cpp
void WiFi_init() {
WiFi.mode(WIFI_STA);
WiFi.begin(); // 尝试连接保存的 WiFi
for (int i = 0; i < Set.WiFi_connect_timeout; ++i) {
uint32_t connect_status = WiFi.status();
if (connect_status == WL_CONNECTED) {
Set.WiFi_connect_stauts = 1;
break;
} else {
Set.WiFi_connect_stauts = 0;
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
// 根据连接结果更新 UI(与 WiFi_connect 类似)
if (Set.WiFi_connect_stauts) {
lv_label_set_text_fmt(ui_LabelWiFiState, "已连接 %s", WiFi.SSID());
lv_obj_add_state(ui_OnOffWiFi, LV_STATE_CHECKED);
} else {
lv_label_set_text(ui_LabelWiFiState, "未连接");
// 开关保持未选中
}
}
在 setup() 中调用 WiFi_init() 即可。
5. 事件回调函数(与 UI 交互)
SquareLine Studio 生成的 UI 代码中,我们需要为部件添加事件回调,并将它们绑定到相应的函数。以下是几个关键回调的实现。
5.1 WiFi 开关事件 ui_event_OnOffWiFi
当用户点击 WiFi 开关时,切换相关面板的可见性,并控制扫描或断开。
cpp
void ui_event_OnOffWiFi(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_VALUE_CHANGED) {
// 切换 WiFi 设置面板的可见性
_ui_flag_modify(ui_RollerWiFi, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
_ui_flag_modify(ui_EnterPass, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
_ui_flag_modify(ui_LabelWiFiState, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
if (lv_obj_is_visible(ui_RollerWiFi)) {
// 面板变为可见:启动扫描
Set.WiFi_button_flag = 1;
} else {
// 面板隐藏:断开 WiFi 连接
WiFi.disconnect();
Set.WiFi_connect_stauts = 0;
}
}
}
5.2 滚动列表选择事件 ui_event_RollerWiFi
当用户在滚动列表中选择一个 WiFi 名称时,将选中的字符串保存到 Set.WiFi_name 中。
cpp
void ui_event_RollerWiFi(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_VALUE_CHANGED) {
lv_roller_get_selected_str(ui_RollerWiFi, Set.WiFi_name, sizeof(Set.WiFi_name));
}
}
5.3 “输入密码”按钮事件 ui_event_EnterPass
点击按钮后显示密码输入面板(ui_PanelPass),并更新标签显示当前选中的 WiFi 名称。
cpp
void ui_event_EnterPass(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_CLICKED) {
_ui_flag_modify(ui_PanelPass, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
// 在密码面板顶部显示选中的 WiFi 名称
lv_label_set_text_fmt(ui_LabelWiFiName, "%s", Set.WiFi_name);
lv_label_set_long_mode(ui_LabelWiFiName, LV_LABEL_LONG_SCROLL_CIRCULAR);
}
}
5.4 键盘完成事件 ui_event_KeyboardPass
当用户在虚拟键盘上点击“Ready”(或对应完成键)时,获取输入的密码,隐藏密码面板,显示等待动画,并设置连接标志。
cpp
void ui_event_KeyboardPass(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_READY) {
// 隐藏密码面板
_ui_flag_modify(ui_PanelPass, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
// 显示等待动画
_ui_flag_modify(ui_SpinnerWiFi, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_TOGGLE);
// 获取密码并设置连接标志
Set.WiFi_password = lv_textarea_get_text(ui_TextAreaWiFiPass);
Set.WiFi_connect_flag = 1;
}
}
5.5 取消按钮事件 ui_event_CancelEnterPass
如果用户不想输入密码,点击“取消”直接关闭密码面板。
cpp
void ui_event_CancelEnterPass(lv_event_t * e) {
lv_event_code_t event_code = lv_event_get_code(e);
if (event_code == LV_EVENT_CLICKED) {
_ui_flag_modify(ui_PanelPass, LV_OBJ_FLAG_HIDDEN, _UI_MODIFY_FLAG_ADD); // 隐藏
}
}
6. 创建 FreeRTOS 任务处理 WiFi
与 LVGL 刷新任务类似,我们为 WiFi 创建一个独立的后台任务,在 Core 0 上运行,每隔一小段时间调用 WiFi_update()。
在 main.cpp 中添加:
cpp
// 任务函数声明
void wifi_task(void *pt);
void setup() {
// ... 其他初始化(LVGL、显示器、触摸、ui_init)
WiFi_init(); // 开机自动连接
// 创建 WiFi 任务,绑定到 Core 0
xTaskCreatePinnedToCore(
wifi_task,
"wifi_task",
1024 * 5, // 栈大小 5KB
NULL,
1, // 优先级略低于 LVGL 任务(LVGL 为 2)
NULL,
0 // Core 0
);
// 创建 LVGL 任务(Core 1)...
}
void wifi_task(void *pt) {
while (1) {
WiFi_update();
vTaskDelay(50); // 每 50ms 更新一次状态(可根据需要调整)
}
}
这样,WiFi 扫描、连接等操作就在后台进行,不会阻塞 LVGL 刷新。
7. 测试与效果
编译烧录后,运行效果:
-
开机后自动尝试连接上次保存的 WiFi,成功则开关自动打开,状态显示“已连接 xxx”。
-
点击 WiFi 开关,显示滚动列表和输入按钮;滚动列表显示扫描到的网络。
-
选择一个网络,点击“输入密码”,弹出密码面板。
-
输入密码后点击键盘“Ready”,显示等待动画,连接成功后状态更新。
-
关闭 WiFi 开关,断开连接并隐藏面板。
整个过程界面流畅,无卡顿。
8. 注意事项
-
密码安全:本示例中密码以
const char*直接引用文本框内容,实际应用中建议复制到固定缓冲区,避免指针失效。 -
超时时间:
WiFi_connect_timeout设为 50 次循环,每次 100ms,共 5 秒。可根据需要调整。 -
扫描阻塞:
WiFi.scanNetworks()本身是阻塞的,在后台任务中执行不会影响界面,但扫描期间如果用户频繁操作可能稍有延迟,可考虑添加“扫描中”提示。 -
WiFi 状态同步:
WiFi.status()可能变化,因此我们在WiFi_update中定期检查并更新 UI。 -
内存管理:
Set.WiFi_scan_all是String类型,可能动态分配内存,注意避免碎片化。
9. 总结
通过将 WiFi 功能独立为一个 FreeRTOS 任务,我们成功地为 ESP32-S3 智能终端添加了流畅的 WiFi 配置界面。用户可以通过 LVGL 控件直观地扫描网络、输入密码并查看连接状态,所有操作都不影响主界面的刷新。这种“任务分离 + 双核绑定”的设计模式,使得复杂的嵌入式 GUI 项目也能保持良好性能。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)