目录

为 ESP32-S3 智能终端添加 WiFi 连接功能(基于 FreeRTOS 与 LVGL)

1. WiFi 功能概述

2. SquareLine Studio 中的界面设计​编辑

3. 全局数据结构(config.h)

4. 主要函数实现

4.1 WiFi 扫描函数 WiFi_scan()

4.2 WiFi 连接函数 WiFi_connect()

4.3 WiFi 状态更新函数 WiFi_update()

4.4 开机自动连接上次 WiFi(WiFi_init)

5. 事件回调函数(与 UI 交互)

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

6. 创建 FreeRTOS 任务处理 WiFi

7. 测试与效果

8. 注意事项

9. 总结


项目演示:

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 项目也能保持良好性能。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐