ESP32S3开发:实现WiFi扫描与连接功能
ESP32S3开发笔记:实现WiFi扫描与连接功能
一、项目概述
本文将详细讲解如何在ESP32S3开发板上实现WiFi扫描和连接功能。我们将使用LVGL图形库创建一个友好的用户界面,实现:
- 扫描并显示周围WiFi名称
- 点击WiFi名称后进入密码输入界面
- 输入密码连接WiFi
整个项目基于ESP-IDF框架开发,是一个非常实用的应用案例。无论你是嵌入式开发新手还是有经验的工程师,都能从中学到ESP32的WiFi功能实现方法。
二、硬件与软件准备
硬件需求
- ESP32S3开发板
- USB数据线
- 带显示屏的开发板(本例中使用320x240液晶屏)
软件环境
- ESP-IDF开发环境(本例使用v5.4.2)
- VSCode编辑器
- ESP-IDF VSCode插件
三、项目结构解析
与普通ESP-IDF项目相比,本项目有几个特殊之处:
- 自定义分区表:使用了自定义的分区表文件
partitions.csv
,为存储中文字库提供更大空间 - 中文字库文件:添加了中文字体支持,以便正确显示中文WiFi名称
- LVGL界面:使用LVGL图形库创建用户界面
3.1 项目目录结构
项目根目录
├── main
│ ├── app_ui.c // 应用程序UI相关代码
│ ├── app_ui.h // UI头文件
│ ├── esp32_s3_szp.c // 开发板BSP文件
│ ├── font_alipuhui20.c // 中文字库文件
│ ├── main.c // 主程序入口
│ └── CMakeLists.txt // 编译配置文件
├── partitions.csv // 自定义分区表
└── CMakeLists.txt // 主CMakeLists文件
四、分区表详解
4.1 为什么需要自定义分区表?
在ESP32开发中,分区表负责为Flash划分不同的区域,存放不同类型的文件。默认分区表为应用程序分配的空间是1MB,但由于我们的项目包含中文字库,体积较大(约3MB),因此需要自定义分区表,增加应用程序的存储空间。
4.2 分区表内容
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, , 0x1000,
factory, app, factory, , 7M,
与默认分区表相比,我们将factory
分区的大小从1MB增加到了7MB,这样就能容纳我们的应用程序和中文字库。
4.3 配置使用自定义分区表
在menuconfig中,需要选择Custom partition table CSV
选项:
- 在VSCode中打开终端
- 运行
idf.py menuconfig
- 进入
Partition Table
选项 - 选择
Custom partition table CSV
- 设置分区表文件名为
partitions.csv
五、中文字库制作
5.1 为什么需要中文字库?
ESP32默认不支持中文显示。由于WiFi名称可能包含中文字符,我们需要添加中文字体支持。本项目使用阿里普惠字体,它是免费可商用的。
5.2 字库制作步骤
1. 获取字体文件
- 阿里普惠字体:从阿里巴巴字体网站下载
- Awesome字体:用于显示WiFi图标,从官方网站下载
2. 使用LVGL在线工具制作字库
访问LVGL字体转换工具,按以下步骤设置:
-
基本设置
- Name:
font_alipuhui20
(自定义名称) - Size:
20
(像素大小) - Bpp:
4
(每个像素的位数,影响显示效果和文件大小)
- Name:
-
添加中文字体
- 选择阿里普惠字体文件
- Range:
0x20-0x2FA1F
(包含所有中文字符的Unicode范围)
-
添加WiFi图标字体
- 点击"Include another font"
- 选择fa-soild-900.ttf文件
- Range:
0xf1eb
(WiFi图标的Unicode码)
-
点击"Submit",等待生成字体文件
注意:生成过程可能需要10分钟左右,如果浏览器提示"页面无响应",选择"等待"即可。
5.3 在项目中使用字库
-
将生成的
font_alipuhui20.c
文件复制到main
文件夹 -
修改文件开头的包含头文件部分:
// 修改前 #ifdef LV_LVGL_H_INCLUDE_SIMPLE #include "lvgl.h" #else #include "lvgl/lvgl.h" #endif// 修改后 #include "lvgl.h"
-
在CMakeLists.txt中添加字体文件:
idf_component_register(SRCS "font_alipuhui20.c" "app_ui.c" "esp32_s3_szp.c" "main.c"INCLUDE_DIRS ".")
-
在使用字体的文件中声明:
LV_FONT_DECLARE(font_alipuhui20);
-
使用字体:
lv_obj_set_style_text_font(label, &font_alipuhui20, 0);
六、应用程序设计
6.1 主程序流程
主程序入口app_main()
函数的代码如下:
void app_main(void)
{// 初始化NVSesp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ret = nvs_flash_init();}ESP_ERROR_CHECK( ret );bsp_i2c_init(); // I2C初始化pca9557_init(); // IO扩展芯片初始化bsp_lvgl_start(); // 初始化液晶屏lvgl接口app_wifi_connect(); // 运行wifi连接程序
}
程序执行流程:
- 初始化NVS(非易失性存储,用于存储WiFi配置)
- 初始化I2C总线
- 初始化IO扩展芯片
- 初始化LVGL液晶屏接口
- 运行WiFi连接应用
6.2 WiFi扫描与连接界面
app_wifi_connect()
函数是整个应用的核心,它完成了以下几个主要功能:
void app_wifi_connect(void)
{lvgl_port_lock(0);// 创建WLAN扫描页面static lv_style_t style;lv_style_init(&style);lv_style_set_bg_opa( &style, LV_OPA_COVER ); // 背景透明度lv_style_set_border_width(&style, 0); // 边框宽度lv_style_set_pad_all(&style, 0); // 内间距lv_style_set_radius(&style, 0); // 圆角半径lv_style_set_width(&style, 320); // 宽lv_style_set_height(&style, 240); // 高wifi_scan_page = lv_obj_create(lv_scr_act());lv_obj_add_style(wifi_scan_page, &style, 0);// 在WLAN扫描页面显示提示lv_obj_t *label_wifi_scan = lv_label_create(wifi_scan_page);lv_label_set_text(label_wifi_scan, "WLAN扫描中...");lv_obj_set_style_text_font(label_wifi_scan, &font_alipuhui20, 0);lv_obj_align(label_wifi_scan, LV_ALIGN_CENTER, 0, -50);lvgl_port_unlock();// 扫描WLAN信息wifi_ap_record_t ap_info[DEFAULT_SCAN_LIST_SIZE]; // 记录扫描到的wifi信息uint16_t ap_number = DEFAULT_SCAN_LIST_SIZE;wifi_scan(ap_info, &ap_number); // 扫描附近wifilvgl_port_lock(0);// 扫描附近wifi信息成功后 删除提示文字lv_obj_del(label_wifi_scan);// 创建wifi信息列表wifi_list = lv_list_create(wifi_scan_page);lv_obj_set_size(wifi_list, lv_pct(100), lv_pct(100));lv_obj_set_style_border_width(wifi_list, 0, 0);lv_obj_set_style_text_font(wifi_list, &font_alipuhui20, 0);lv_obj_set_scrollbar_mode(wifi_list, LV_SCROLLBAR_MODE_OFF); // 隐藏滚动条// 显示wifi信息lv_obj_t * btn;for (int i = 0; i < ap_number; i++) {ESP_LOGI(TAG, "SSID \t\t%s", ap_info[i].ssid); // 终端输出wifi名称ESP_LOGI(TAG, "RSSI \t\t%d", ap_info[i].rssi); // 终端输出wifi信号质量// 添加wifi列表btn = lv_list_add_btn(wifi_list, LV_SYMBOL_WIFI, (const char *)ap_info[i].ssid);lv_obj_add_event_cb(btn, list_btn_cb, LV_EVENT_CLICKED, NULL); // 添加点击回调函数}lvgl_port_unlock();// 创建wifi连接任务xQueueWifiAccount = xQueueCreate(2, sizeof(wifi_account_t));xTaskCreatePinnedToCore(wifi_connect, "wifi_connect", 4 * 1024, NULL, 5, NULL, 1); // 创建wifi连接任务
}
这个函数可以分为四个主要部分:
-
创建扫描提示页面(4-21行):
- 创建全屏页面
- 显示"WLAN扫描中…"提示
-
执行WiFi扫描(23-26行):
- 调用
wifi_scan()
函数扫描附近WiFi
- 调用
-
显示WiFi列表(28-46行):
- 删除"扫描中"提示
- 创建WiFi列表控件
- 遍历扫描到的WiFi并添加到列表中
- 为每个WiFi名称添加点击事件
-
创建WiFi连接任务(48-50行):
- 创建消息队列用于传递WiFi账号和密码
- 创建WiFi连接任务,等待用户输入密码后执行连接
重要提示:使用
esp_lvgl_port
组件时,所有LVGL相关操作都必须位于lvgl_port_lock(0)
和lvgl_port_unlock()
之间,这确保了LVGL画面的正常显示。
6.3 密码输入界面
当用户点击WiFi名称后,会进入list_btn_cb()
函数,该函数负责创建密码输入界面:
// 进入输入密码界面
static void list_btn_cb(lv_event_t * e)
{// 获取点击到的WiFi名称const char *wifi_name=NULL;lv_event_code_t code = lv_event_get_code(e);lv_obj_t * obj = lv_event_get_target(e);if(code == LV_EVENT_CLICKED) {wifi_name = lv_list_get_btn_text(wifi_list, obj);ESP_LOGI(TAG, "WLAN Name: %s", wifi_name);}// 创建密码输入页面wifi_password_page = lv_obj_create(lv_scr_act());lv_obj_set_size(wifi_password_page, 320, 240);lv_obj_set_style_border_width(wifi_password_page, 0, 0); // 设置边框宽度lv_obj_set_style_pad_all(wifi_password_page, 0, 0); // 设置间隙lv_obj_set_style_radius(wifi_password_page, 0, 0); // 设置圆角// 创建返回按钮// ...// 显示选中的wifi名称// ...// 创建密码输入框ta_pass_text = lv_textarea_create(wifi_password_page);lv_obj_set_style_text_font(ta_pass_text, &lv_font_montserrat_20, 0);lv_textarea_set_one_line(ta_pass_text, true); // 一行显示lv_textarea_set_password_mode(ta_pass_text, false); // 是否使用密码输入显示模式lv_textarea_set_placeholder_text(ta_pass_text, "password"); // 设置提醒词lv_obj_set_width(ta_pass_text, 150); // 宽度lv_obj_align(ta_pass_text, LV_ALIGN_TOP_LEFT, 10, 40); // 位置lv_obj_add_state(ta_pass_text, LV_STATE_FOCUSED); // 显示光标// 创建"连接按钮"// ...// 创建"删除按钮"// ...// 创建数字、小写字母、大写字母的roller及确认按钮// ...
}
密码输入界面包含以下元素:
- 返回按钮:返回到WiFi列表
- WiFi名称显示
- 密码输入框
- 连接按钮(OK)
- 删除按钮(退格键)
- 三个字符选择器(roller):
- 数字(0-9)
- 小写字母(a-z)
- 大写字母(A-Z)
- 每个roller下的确认按钮
6.4 WiFi连接实现
点击"OK"按钮后,会调用btn_connect_cb
函数:
static void btn_connect_cb(lv_event_t * e)
{lv_event_code_t code = lv_event_get_code(e);if(code == LV_EVENT_CLICKED) {// 获取wifi名称char* wifi_name = NULL;wifi_name = lv_label_get_text(label_wifi_name);ESP_LOGI(TAG, "WiFi Name: %s", wifi_name);// 获取wifi密码const char* pass_text = lv_textarea_get_text(ta_pass_text);ESP_LOGI(TAG, "WiFi Password: %s", pass_text);// 删除密码界面lv_obj_del(wifi_password_page);// 显示"WLAN连接中..."lv_obj_t *label_wifi_connect = lv_label_create(wifi_scan_page);lv_label_set_text(label_wifi_connect, "WLAN连接中...");lv_obj_set_style_text_font(label_wifi_connect, &font_alipuhui20, 0);lv_obj_align(label_wifi_connect, LV_ALIGN_CENTER, 0, -50);// 发送wifi账号和密码到连接任务wifi_account_t wifi_account;memset(&wifi_account, 0, sizeof(wifi_account_t));strcpy((char *)wifi_account.ssid, wifi_name);strcpy((char *)wifi_account.password, pass_text);if (xQueueWifiAccount != NULL) {xQueueSend(xQueueWifiAccount, &wifi_account, 0);}}
}
此函数完成了以下操作:
- 获取用户选择的WiFi名称
- 获取用户输入的密码
- 删除密码输入界面
- 显示"WLAN连接中…"提示
- 将WiFi名称和密码通过队列发送给WiFi连接任务
wifi_connect
任务在接收到这些信息后会执行连接:
static void wifi_connect(void *pvParameters)
{wifi_account_t wifi_account;// 等待接收WiFi账号密码if (xQueueReceive(xQueueWifiAccount, &wifi_account, portMAX_DELAY)) {// 开始连接WiFiwifi_init_sta((char *)wifi_account.ssid, (char *)wifi_account.password);// 根据连接结果显示不同提示lvgl_port_lock(0);if (wifi_connect_status == WIFI_CONNECTED_OK) {// 连接成功lv_obj_t *label_wifi_connected = lv_label_create(wifi_scan_page);lv_label_set_text(label_wifi_connected, "WLAN连接成功!");lv_obj_set_style_text_font(label_wifi_connected, &font_alipuhui20, 0);lv_obj_align(label_wifi_connected, LV_ALIGN_CENTER, 0, -50);} else if (wifi_connect_status == WIFI_CONNECTED_FAIL) {// 连接失败lv_obj_t *label_wifi_connected = lv_label_create(wifi_scan_page);lv_label_set_text(label_wifi_connected, "WLAN连接失败!");lv_obj_set_style_text_font(label_wifi_connected, &font_alipuhui20, 0);lv_obj_align(label_wifi_connected, LV_ALIGN_CENTER, 0, -50);} else {// 连接异常lv_obj_t *label_wifi_connected = lv_label_create(wifi_scan_page);lv_label_set_text(label_wifi_connected, "WLAN连接异常!");lv_obj_set_style_text_font(label_wifi_connected, &font_alipuhui20, 0);lv_obj_align(label_wifi_connected, LV_ALIGN_CENTER, 0, -50);}lvgl_port_unlock();}vTaskDelete(NULL);
}
连接完成后,会根据连接结果在屏幕上显示相应的提示:
- 连接成功:显示"WLAN连接成功!"
- 连接失败:显示"WLAN连接失败!"
- 连接异常:显示"WLAN连接异常!"
七、运行与测试
- 编译并下载程序到ESP32S3开发板
- 程序运行后,屏幕会先显示"WLAN扫描中…"
- 扫描完成后,显示附近WiFi的名称列表
- 点击想要连接的WiFi
- 在密码输入界面输入密码
- 可以通过三个roller选择数字、小写字母和大写字母
- 每选择一个字符,点击下方的确认按钮(√)添加到密码框
- 如果输入错误,可以使用删除按钮删除
- 输入完成后点击"OK"按钮
- 程序会显示"WLAN连接中…",然后根据连接结果显示成功或失败信息
八、优化与拓展
8.1 信号强度显示
目前程序只显示了WiFi名称,没有显示信号强度。可以通过以下方式添加信号强度显示:
// 根据RSSI值选择不同的WiFi图标
const char* get_wifi_icon(int rssi)
{if (rssi > -50) {return LV_SYMBOL_WIFI; // 信号强} else if (rssi > -70) {return LV_SYMBOL_WIFI; // 信号中(这里可以用不同图标)} else {return LV_SYMBOL_WIFI; // 信号弱}
}// 在添加WiFi列表项时使用
btn = lv_list_add_btn(wifi_list, get_wifi_icon(ap_info[i].rssi), (const char *)ap_info[i].ssid);
8.2 密码输入增强
目前密码输入只支持数字和字母,可以添加特殊字符支持:
// 修改数字roller的选项,添加特殊字符
const char * opts_num = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n!\n@\n#\n$\n%\n^\n&\n*";
8.3 记住密码功能
可以使用NVS存储已连接的WiFi信息,下次自动连接:
// 存储WiFi信息到NVS
void save_wifi_config(const char* ssid, const char* password)
{nvs_handle_t my_handle;ESP_ERROR_CHECK(nvs_open("storage", NVS_READWRITE, &my_handle));ESP_ERROR_CHECK(nvs_set_str(my_handle, "ssid", ssid));ESP_ERROR_CHECK(nvs_set_str(my_handle, "password", password));ESP_ERROR_CHECK(nvs_commit(my_handle));nvs_close(my_handle);
}// 在连接成功后调用
if (wifi_connect_status == WIFI_CONNECTED_OK) {save_wifi_config(wifi_account.ssid, wifi_account.password);// ...
}
九、总结
通过本文,我们详细讲解了如何在ESP32S3开发板上实现WiFi扫描和连接功能,包括:
- 创建自定义分区表,解决中文字库占用空间过大的问题
- 使用免费商用的阿里普惠字体制作中文字库,支持显示中文WiFi名称
- 使用LVGL图形库创建友好的用户界面
- 实现WiFi扫描、显示列表、密码输入和连接功能
- 通过任务和队列实现不同功能模块之间的通信
这个项目是物联网应用的基础,为后续开发更复杂的联网应用奠定了基础。希望这篇教程对大家有所帮助!
参考资料
- ESP-IDF 分区表介绍
- ESP-IDF NVS 介绍
- 阿里巴巴字体网站
- LVGL字体制作工具
- Unicode字符范围查询