跳到主要内容

LVGL 86 面板设计指南

本教程利用 LVGL 图形库实现 86 面板的屏幕界面设计及控件交互逻辑设计,旨在搭建一个完整的嵌入式 GUI 应用程序。

示例程序:lvgl_project_86panel.tar.gz

1.简介

在智能家居控制场景中,86面板作为全屋设备控制终端,其简易的界面和显眼的按钮满足了人们室内中近距离控制设备的场景。接下来,我们通过 LVGL 图形库设计一套简易的智能终端,包含了三个界面,分别为主界面、wifi配置界面、副界面。其功能清单如下:

界面功能描述
主界面显示 CPU 和内存占用实时显示当前设备的 CPU 和内存使用情况,帮助用户了解设备的运行状态。
主界面显示 WIFI 和 ETH 连接状态显示当前的网络连接状态,包括 WIFI 和以太网(ETH)的 IP 地址信息。如果没有网络连接,显示默认的 --
主界面显示时间显示当前时间,支持通过 NTP 服务器每小时校准一次,确保时间的准确性。
副界面继电器开关提供两个继电器开关,用户可以通过滑动开关控制继电器的开合。
副界面音频播放器提供音量调整功能,并支持播放/暂停控制。
WiFi 配置界面可配置 WiFi 信息用户可以输入 WiFi 的 SSID 和密码,支持保存和连接操作。
WiFi 配置界面WiFi 控制按键用户可以控制 WiFi 连接或者断开,扫描附近 WiFi。

2.项目架构搭建

  1. 在界面设计开始前,需要搭建 LVGL 项目的基本依赖库和教程编译环境,完成此部分可参考文档 LVGL 移植教程。假设你完成了 LVGL 库移植,并将工程目录如下分类:

    lvgl/lvgl_project_86panel
    ├── lv_conf.h
    ├── lv_drivers
    ├── lv_drv_conf.h
    ├── lvgl
    ├── main.c
    ├── Makefile
    ├── mouse_cursor_icon.c
    └── source
  2. 进入 source 文件夹,创建自己的子项目,此处项目名以 86panel_demo 为例。

    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source$ mkdir 86panel_demo
    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source$ cd 86panel_demo/
    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo$
  3. 进入项目文件夹,创建 ui.c 和 ui.h 文件用来存放界面 UI 显示函数及其函数声明,创建 ui_custom.cui_custom.h 文件用来存放应用逻辑函数及其函数声明。

    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo$ touch ui.c ui.h ui_custom.c ui_custom.h
  4. 创建 screen 文件夹,存放 ui_ScreenMain.c , ui_ScreenWpa.c , ui_ScreenPlayer.c 文件以实现具体界面功能细节。

    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo$ mkdir screens
    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo$ cd screens/
    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo/screens$ touch ui_ScreenMain.c ui_ScreenWpa.c ui_ScreenPlayer.c
  5. 返回上级目录 86panel_demo ,通过创建 imagesfonts 文件夹来分别存放图像和字体资源,可以使项目结构更加清晰。

    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo/screen$ cd ..
    luckfox@luckfox:~/lvgl/lvgl_project_86panel/source/86panel_demo$ mkdir images fonts
  6. 至此,文件结构搭建完成。

    lvgl/lvgl_project_86panel/source/86panel_demo
    ├── fonts
    ├── images
    ├── screens
    │ ├── ui_ScreenMain.c
    │ ├── ui_ScreenWpa.c
    │ └── ui_ScreenPlayer.c
    ├── ui.c
    ├── ui_custom.c
    ├── ui.h
    └── ui_custom.h
  7. 创建初始界面,在 ui.h 头文件中声明 ui_init() 函数。

    #ifndef _LCUKFOX_86PANEL_UI_H
    #define _LCUKFOX_86PANEL_UI_H

    #include "lvgl/lvgl.h"

    void ui_init(void);

    #endif
  8. ui.c 文件中实现界面初始化。

    #include "ui.h"

    ///////////////////// SCREENS ////////////////////

    void ui_init(void)
    {
    lv_disp_t * dispp = lv_disp_get_default();
    lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT);
    lv_disp_set_theme(dispp, theme);
    }
    • lv_disp_get_default() 获取当前默认的显示设备。lv_theme_default_init() 初始化默认主题。然后使用lv_disp_set_theme(dispp, theme)将初始化的主题应用到显示设备上。
  9. main.c 中包含 ui.h 头文件和调用屏幕初始化函数 ui_init() ,并修改 disp_drv.hor_resdisp_drv.ver_res 适配屏幕分辨率 720x720

    #include "source/86panel_demo/ui.h"

    int main(void)
    {
    /*Initialize and register a display driver*/
    ...
    disp_drv.hor_res = 720;
    disp_drv.ver_res = 720;
    ...

    /*Create a Demo*/
    ui_init();
    ...
    }

3.主界面

主界面示例程序如下:lvgl_project_86panel_main.tar.gz

3.1 设置主界面背景图

本节包含屏幕控件设置背景图片样式的介绍。

  1. 先选择好背景界面图才能更好地为后续控件样式奠定基调。在本教程中,使用暗色主题作为 UI 设计的整体样式导向。在 ui_ScreenMain.c 文件中,设置屏幕背景图以及颜色。

    void ui_ScreenMain_screen_init(void)
    {
    ...
    ui_ScreenMain = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(ui_ScreenMain, lv_color_hex(0x0D0D0D), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_img_src(ui_ScreenMain, &ui_img_luckfox_logo_png, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_img_opa(ui_ScreenMain, 15, LV_PART_MAIN | LV_STATE_DEFAULT);
    ...
    }

    代码步骤说明

    • 创建屏幕:使用 lv_obj_create(NULL) 创建一个新的顶级屏幕对象。
    • 设置背景颜色:通过 lv_obj_set_style_bg_color() 将背景颜色设置为深灰色 (0x0D0D0D)。
    • 设置背景图片:利用 lv_obj_set_style_bg_img_src() 设置背景图片为 Luckfox_logo 图像。
    • 设置图片透明度:调用 lv_obj_set_style_bg_img_opa() 设置背景图片的透明度为15,使其呈现几乎透明的效果。
  2. 在设置背景图片时,传入了 ui_img_luckfox_logo_png 参数,这是采用 C 语言数组的方式来显示图片的。转换流程如下:

    • 获取背景图片
    • 通过在线转换工具将背景图片转换为 C 语言数组格式的文件,先选择图片文件,后选择 Color formatCF_INDEXED_8_BITOutput formatC array 表示 C 语言数组格式,点击生成即可获得 luckfox_logo.c

  3. 随后,将文件移入 images 文件夹中。工程文件结构如下:

    /lvgl/lvgl_project_86panel/source/86panel_demo
    .
    ├── fonts
    ├── images
    │   └── luckfox_logo.c
    ├── screens
    │   ├── ui_screenMain.c
    │   ├── ui_screenPlayer.c
    │   └── ui_screenWpa.c
    ├── ui.c
    ├── ui_custom.c
    ├── ui.h
    └── ui_custom.h
  4. ui.h 文件中添加宏声明,让编译器知道一个名为 luckfox_logo 的图像资源存在。

    LV_IMG_DECLARE(luckfox_logo);
  5. ui.c 文件中调用 ui_ScreenMain_screen_init() 函数。

    #include "ui.h"

    void ui_ScreenMain_screen_init(void);
    lv_obj_t * ui_ScreenMain;

    ///////////////////// SCREENS ////////////////////

    void ui_init(void)
    {
    lv_disp_t * dispp = lv_disp_get_default();
    lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED),false, LV_FONT_DEFAULT);
    lv_disp_set_theme(dispp, theme);
    ui_ScreenMain_screen_init(); //add function
    lv_disp_load_scr(ui_ScreenMain); //load ui_ScreenMain
    }
    • ui_ScreenMain_screen_init()调用自定义的屏幕初始化函数,此时屏幕上尚未有画面,需要通过 lv_disp_load_scr(ui_ScreenMain) 加载屏幕。
  6. 至此主界面背景图设置完成,运行图像如下:

3.2 显示 CPU 和内存占用

本节包含文本标签控件的介绍。

  1. 显示 CPU 和内存占用,此部分直接在 lv_conf.h 中将性能参数的宏定义设置为1。

    /*1: Show CPU usage and FPS count*/
    #define LV_USE_PERF_MONITOR 1
    #if LV_USE_PERF_MONITOR
    #define LV_USE_PERF_MONITOR_POS LV_ALIGN_TOP_LEFT
    #endif

    /*1: Show the used memory and the memory fragmentation
    * Requires LV_MEM_CUSTOM = 0*/
    #define LV_USE_MEM_MONITOR 1
    #if LV_USE_MEM_MONITOR
    #define LV_USE_MEM_MONITOR_POS LV_ALIGN_TOP_RIGHT
    #endif
    • 设置后即可在屏幕的系统层(sys)上显示信息,该层的内容总是可见的。
  2. 开启此宏定义后,在 lv_perf.c 中函数 _lv_disp_refr_timer() 中的内容将会生效,此处主要是标签部件的使用:

    void _lv_disp_refr_timer()
    {
    ...
    lv_obj_t * perf_label = perf_monitor.perf_label;
    if(perf_label == NULL) {
    perf_label = lv_label_create(lv_layer_sys());
    lv_obj_set_style_bg_opa(perf_label, LV_OPA_50, 0);
    lv_obj_set_style_bg_color(perf_label, lv_color_black(), 0);
    lv_obj_set_style_text_color(perf_label, lv_color_white(), 0);
    lv_obj_set_style_pad_top(perf_label, 3, 0);
    lv_obj_set_style_pad_bottom(perf_label, 3, 0);
    lv_obj_set_style_pad_left(perf_label, 3, 0);
    lv_obj_set_style_pad_right(perf_label, 3, 0);
    lv_obj_set_style_text_align(perf_label, LV_TEXT_ALIGN_RIGHT, 0);
    lv_label_set_text(perf_label, "?");
    lv_obj_align(perf_label, LV_USE_PERF_MONITOR_POS, 0, 0);
    perf_monitor.perf_label = perf_label;
    }
    ...
    }

    代码步骤说明

    • 创建标签:若 perf_label 变量为空,则调用 lv_label_create(lv_layer_sys()) 创建一个新的标签对象,该对象将在系统层中显示,意味着它是一个全局、跨屏幕的对象。
    • 设置标签背景透明度:使用 lv_obj_set_style_bg_opa() 设置标签背景的透明度为50%,使其背景呈现半透明效果。
    • 设置文字颜色:通过 lv_obj_set_style_text_color() 将文字颜色设置为白色(十六进制值 0xFFFFFF)。
    • 设置内边距:利用 lv_obj_set_style_pad_top(), lv_obj_set_style_pad_bottom(), lv_obj_set_style_pad_left(), 和 lv_obj_set_style_pad_right() 分别设置顶部、底部、左侧和右侧的内边距为3个像素,以确保文本周围有足够的空间。
    • 设置文本对齐方式:调用 lv_obj_set_style_text_align() 设置文本对齐方式为右对齐,使得文本在标签内靠右显示。
    • 设置标签文本内容:使用 lv_label_set_text() 设置标签的文本内容为 ?
    • 设置控件位置:最后,调用 lv_obj_align(perf_label, LV_ALIGN_TOP_RIGHT, 0, 0) 设置控件的位置,使其位于父容器的右上角。
  3. 创建完控件后,通过 lv_label_set_text_fmt()函数可以使用格式化字符串设置标签的文本内容,对于 lv_label_set_text() 是直接设置标签的文本内容。

    lv_label_set_text_fmt(perf_label, "%"LV_PRIu32" FPS\n%"LV_PRIu32"%% CPU", fps, cpu);
  4. 同理,内存占用函数也是如此将获取的信息,内存占用的计算是通过 LVGL 提供的 lv_mem_monitor 函数完成的。

    if(lv_tick_elaps(mem_monitor.mem_last_time) > 300) {
    mem_monitor.mem_last_time = lv_tick_get();
    lv_mem_monitor_t mon;
    lv_mem_monitor(&mon); //Get memory usage status
    uint32_t used_size = mon.total_size - mon.free_size;;
    uint32_t used_kb = used_size / 1024;
    uint32_t used_kb_tenth = (used_size - (used_kb * 1024)) / 102;
    lv_label_set_text_fmt(mem_label,
    "%"LV_PRIu32 ".%"LV_PRIu32 " kB used (%d %%)\n"
    "%d%% frag.",
    used_kb, used_kb_tenth, mon.used_pct,
    mon.frag_pct);
    }

3.3 显示 WiFi 状态

本节包含 LVGL 图片控件,标签控件,定时器控件的使用以及获取 WiFi 状态函数的编写。

  1. ui.c 中定义网络状态显示控件全局变量,定义如下:

    lv_obj_t * ui_PanelWifi;
    lv_obj_t * ui_ImageWifi;
    lv_obj_t * ui_LabelWifiName;
    lv_obj_t * ui_LabelWifiIP;
    lv_obj_t * ui_LabelWIP;
    lv_obj_t * ui_Timer;
    • 定义了 WiFi 显示容器控件ui_PanelWifi,WiFi 图标 ui_ImageWifi ,WiFi 名称 ui_LabelWifiName 以及 WiFi IP 内容ui_LabelWifiIP,WiFi IP 标识 ui_LabelWIP
  2. ui.h 中外部声明控件变量,可以供 ui_ScreenMain.c 文件使用,内容如下:

    extern lv_obj_t * ui_PanelWifi;
    extern lv_obj_t * ui_ImageWifi;
    extern lv_obj_t * ui_LabelWifiName;
    extern lv_obj_t * ui_LabelWifiIP;
    extern lv_obj_t * ui_LabelWIP;
    extern lv_obj_t * ui_Timer;
  3. 编写 ui_ScreenMain.c 文件,放置 WiFi 信息显示容器。

    void ui_ScreenMain_screen_init(void)
    {
    ...
    ui_PanelWifi = lv_obj_create(ui_ScreenMain);
    lv_obj_set_width(ui_PanelWifi, 320);
    lv_obj_set_height(ui_PanelWifi, 200);
    lv_obj_set_x(ui_PanelWifi, 30);
    lv_obj_set_y(ui_PanelWifi, 490);
    lv_obj_set_style_radius(ui_PanelWifi, 15, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_PanelWifi, lv_color_hex(0x1F1F1F), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_PanelWifi, lv_color_hex(0x3F3622), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_PanelWifi, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_left(ui_PanelWifi, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_right(ui_PanelWifi, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_top(ui_PanelWifi, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_bottom(ui_PanelWifi, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    ...
    }

    代码步骤说明

    • 创建面板:调用 lv_obj_create(ui_ScreenMain) 创建一个新的面板对象并添加到 ui_ScreenMain 屏幕中。
    • 设置尺寸与位置
      • 使用 lv_obj_set_width()lv_obj_set_height() 分别设置面板的宽度和高度为320像素和200像素。
      • 使用 lv_obj_set_x()lv_obj_set_y() 设置面板的位置,分别为X坐标30像素和Y坐标490像素。
    • 设置样式属性
      • 圆角半径:使用 lv_obj_set_style_radius(); 设置面板的圆角半径为15像素。
      • 背景颜色:使用 lv_obj_set_style_bg_color(); 设置面板的背景颜色为深灰色(十六进制值 0x1F1F1F)。
      • 边框颜色:使用 lv_obj_set_style_border_color(); 设置边框颜色为棕色(十六进制值 0x3F3622)。
      • 内边距:使用 lv_obj_set_style_pad_left(), lv_obj_set_style_pad_right(), lv_obj_set_style_pad_top(), 和 lv_obj_set_style_pad_bottom() 分别设置顶部、底部、左侧和右侧的内边距为0像素,以方便后续器件摆放位置对应到外边距。也可以使用 lv_obj_set_style_pad_all() 同时将上下左右内边距设置为0像素。
  4. 放置 WiFi 图标显示控件在 ui_PanelWifi 容器中,图片源的获取也是通过在线转换为C语言格式进行设置。

    void ui_ScreenMain_screen_init(void)
    {
    ...
    ui_ImageWifi = lv_img_create(ui_PanelWifi);
    lv_img_set_src(ui_ImageWifi, &ui_img_icon_wifi_on_png);
    lv_obj_set_width(ui_ImageWifi, 72);
    lv_obj_set_height(ui_ImageWifi, 72);
    lv_obj_set_x(ui_ImageWifi, 30);
    lv_obj_set_y(ui_ImageWifi, 36);
    ...
    }

    代码步骤说明

    • 创建图像对象:使用 lv_img_create(ui_PanelWifi)ui_PanelWifi 面板上创建一个新的图像对象。
    • 设置图像源:在创建完图片控件后,使用 lv_img_set_src() 函数设置图片源。
    • 设置图像的位置与尺寸。
    • 相应的,需要将图片转换为C语言格式,并在 ui.h 中添加 LV_IMG_DECLARE(ui_img_icon_wifi_on_png)
  5. 设置 WiFi 标签,用于显示 WiFi 名称,IP 地址,标识IP。

    void ui_ScreenMain_screen_init(void)
    {
    ...
    ui_LabelWifiName = lv_label_create(ui_PanelWifi);
    lv_obj_set_width(ui_LabelWifiName, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelWifiName, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelWifiName, 118);
    lv_obj_set_y(ui_LabelWifiName, 56);
    lv_label_set_text(ui_LabelWifiName, "WS-WIFI-5G");
    lv_obj_set_style_text_color(ui_LabelWifiName, lv_color_hex(0xF6AC05), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelWifiName, &lv_font_montserrat_28, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelWifiIP = lv_label_create(ui_PanelWifi);
    lv_obj_set_width(ui_LabelWifiIP, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelWifiIP, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelWifiIP, 118);
    lv_obj_set_y(ui_LabelWifiIP, 130);
    lv_label_set_text(ui_LabelWifiIP, "192.168.100.456");
    lv_obj_set_style_text_color(ui_LabelWifiIP, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelWifiIP, &lv_font_montserrat_22, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelWIP = lv_label_create(ui_PanelWifi);
    lv_obj_set_width(ui_LabelWIP, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelWIP, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelWIP, 54);
    lv_obj_set_y(ui_LabelWIP, 130);
    lv_label_set_text(ui_LabelWIP, "IP:");
    lv_obj_set_style_text_color(ui_LabelWIP, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelWIP, &lv_font_montserrat_22, LV_PART_MAIN | LV_STATE_DEFAULT);
    ...
    }

    代码步骤说明

    • 创建标签:在 ui_PanelWifi 面板上创建了三个标签 (ui_LabelWifiName, ui_LabelWifiIP, ui_LabelWIP) 并设置了它们的文本内容、样式属性(颜色和字体)以及位置。
    • ui_LabelWifiName 用来设置当前 WiFi 的名称。
    • ui_LabelWifiIP 用来设置当前 WiFi 的 IP 地址。
    • ui_LabelIP1 用来标识 WiFi 的 IP 地址。
  6. 创建定时器控件,实时获取 wifi 的信息来更新显示标签。

    void ui_ScreenMain_screen_init(void)
    {
    ...
    lv_obj_t * ui_Timer = lv_timer_create(lvgl_taskmain_cb, 1000, 0);
    // lv_timer_set_repeat_count(ui_Timer, -1);
    ...
    }

    定时器创建说明

    • 创建定时器需使用 lv_timer_create() 函数或者 lv_timer_create_basic() 函数,使用 lv_timer_create() 需填入定时器回调函数,定时周期和用户参数。而 lv_timer_create_basic() 则无需传递参数,但必须调用 lv_timer_set_cb() 函数,其默认定时周期为 DEF_PERIOD
    • 创建完定时器后,默认为无限重复不会自动删除定时器。如果想重复指定次数后,自动删除定时器,可以使用 void lv_timer_set_repeat_count(lv_timer_t * timer, int32_t repeat_count) 函数设置定时器重复计数次数,当repeat_count 参数设置为 -1 时,则无限重复。
  7. ui_custom.c 中编写 wifi 状态更新函数 update_wifi_status() 以及回调函数 lvgl_taskmain_cb()

    void lvgl_taskmain_cb(lv_timer_t *timer)
    {
    if(timer_count % 5 == 0){
    update_wifi_status();
    timer_count = 0;
    }
    timer_count++;
    }

    void update_wifi_status(void)
    {
    FILE *fp;
    char command[MAX_LINE_LEN];
    char result[MAX_LINE_LEN];
    char ssid[MAX_LINE_LEN] = {0};
    char ip[MAX_LINE_LEN] = {0};
    strcpy(command, "wpa_cli status");

    fp = popen(command, "r");
    if (fp == NULL) {
    printf("Failed to run command\n");
    pclose(fp);
    return ;
    }
    while (fgets(result, sizeof(result)-1, fp) != NULL)
    {
    if(strstr(result, "wpa_state=SCANNING"))
    {
    break;
    }else if (strstr(result, "ssid=")) {
    sscanf(result, "ssid=%[^ \n]", ssid);
    }else if (strstr(result, "ip_address=")) {
    sscanf(result, "ip_address=%[^ \n]", ip);
    }
    }
    if(strlen(ssid) == 0)
    {
    lv_label_set_text(ui_LabelWifiName, "------");
    lv_obj_set_style_border_color(ui_PanelWifi, lv_color_hex(0x1F1F1F), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(ui_LabelWifiName, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_img_set_src(ui_ImageWifi, &ui_img_icon_wifi_off_png);
    }
    else
    {
    lv_label_set_text(ui_LabelWifiName, ssid);
    lv_obj_set_style_border_color(ui_PanelWifi, lv_color_hex(0x3F3622), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(ui_LabelWifiName, lv_color_hex(0xF6AC05), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_img_set_src(ui_ImageWifi, &ui_img_icon_wifi_on_png);
    }
    lv_obj_set_style_text_font(ui_LabelWifiName, &lv_font_montserrat_24, LV_PART_MAIN | LV_STATE_DEFAULT);
    if(strlen(ip) == 0)
    {
    lv_label_set_text(ui_LabelWifiIP, "No IP");
    lv_obj_set_style_text_font(ui_LabelWifiIP, &lv_font_montserrat_18, LV_PART_MAIN | LV_STATE_DEFAULT);
    }
    else
    {
    lv_label_set_text(ui_LabelWifiIP, ip);
    lv_obj_set_style_text_font(ui_LabelWifiIP, &lv_font_montserrat_24, LV_PART_MAIN | LV_STATE_DEFAULT);
    }
    pclose(fp);
    }

    代码步骤说明

    • lvgl_taskmain_cb :定义为LVGL定时器的回调函数,它每隔1秒被调用一次,每隔5秒调用一次 WiFi 状态函数。
    • update_wifi_status :负责获取当前Wi-Fi连接的状态(SSID和IP地址),并调用 LVGL 库中的 API 更新到UI上。
      • 构造并执行命令:构造一个字符串 "wpa_cli status" 并使用 popen 执行该命令并打开管道进行读取。
      • 解析输出结果
        • 如果找到包含 wpa_state=SCANNING 的行,表示设备正在扫描可用网络,此时直接跳出循环。
        • 如果找到包含 ssid= 的行,则提取出SSID名称。
        • 如果找到包含 ip_address= 的行,则提取出IP地址。
      • 更新 UI 元素:处理 WiFi 连接和未连接时的 UI 显示标识。
  8. 至此,显示 WiFi 状态部分完成,运行图像如下:

3.4 显示 ETH 状态

本节包含LVGL 图片控件,标签控件的使用以及获取 ETH 状态函数的编写。

  1. ui.c 中定义网络状态显示控件全局变量,定义如下:

    lv_obj_t * ui_PanelEth;
    lv_obj_t * ui_ImageEth;
    lv_obj_t * ui_LabelEth;
    lv_obj_t * ui_LabelEthIP;
    lv_obj_t * ui_LabelNetIP;
    • 定义了 ETH 显示容器控件ui_PanelEth,ETH 图标 ui_ImageEth ,ETH 接口名称 ui_LabelEth 以及 ETH IP 内容ui_LabelEthIP,ETH IP 标识 ui_LabelNetIP
  2. ui.h 中外部声明控件变量,可以供 ui_ScreenMain.c 文件使用,内容如下:

    extern lv_obj_t * ui_PanelEth;
    extern lv_obj_t * ui_ImageEth;
    extern lv_obj_t * ui_LabelEth;
    extern lv_obj_t * ui_LabelEthIP;
    extern lv_obj_t * ui_LabelNetIP;
  3. 编写 ui_ScreenMain.c 文件,配置 ETH 信息显示主件。

    void ui_ScreenMain_screen_init(void)
    {
    ....
    ui_PanelEth = lv_obj_create(ui_ScreenMain);
    lv_obj_set_width(ui_PanelEth, 320);
    lv_obj_set_height(ui_PanelEth, 200);
    lv_obj_set_x(ui_PanelEth, 370);
    lv_obj_set_y(ui_PanelEth, 490);
    lv_obj_set_style_radius(ui_PanelEth, 15, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_PanelEth, lv_color_hex(0x1F1F1F), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_PanelEth, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_PanelEth, lv_color_hex(0x1F1F1F), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_PanelEth, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_left(ui_PanelEth, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_right(ui_PanelEth, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_top(ui_PanelEth, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_bottom(ui_PanelEth, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_ImageEth = lv_img_create(ui_PanelEth);
    lv_img_set_src(ui_ImageEth, &ui_img_icon_eth_off_png);
    lv_obj_set_width(ui_ImageEth, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_ImageEth, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_ImageEth, 30);
    lv_obj_set_y(ui_ImageEth, 36);

    ui_LabelEth = lv_label_create(ui_PanelEth);
    lv_obj_set_width(ui_LabelEth, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelEth, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelEth, 118);
    lv_obj_set_y(ui_LabelEth, 56);
    lv_label_set_text(ui_LabelEth, "------------");
    lv_obj_set_style_text_color(ui_LabelEth, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_LabelEth, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelEth, &lv_font_montserrat_28, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelEthIP = lv_label_create(ui_PanelEth);
    lv_obj_set_width(ui_LabelEthIP, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelEthIP, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelEthIP, 118);
    lv_obj_set_y(ui_LabelEthIP, 130);
    lv_label_set_text(ui_LabelEthIP, "---.---.---.---");
    lv_obj_set_style_text_color(ui_LabelEthIP, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_LabelEthIP, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelEthIP, &lv_font_montserrat_22, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelNetIP = lv_label_create(ui_PanelEth);
    lv_obj_set_width(ui_LabelNetIP, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelNetIP, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelNetIP, 54);
    lv_obj_set_y(ui_LabelNetIP, 130);
    lv_label_set_text(ui_LabelNetIP, "IP:");
    lv_obj_set_style_text_color(ui_LabelNetIP, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_LabelNetIP, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelNetIP, &lv_font_montserrat_22, LV_PART_MAIN | LV_STATE_DEFAULT);
    ....
    }
    • 与 WiFi 状态显示主件一致,通过创建容器、图标以及文本标签配置UI主件。
  4. ui_custom.c 中添加 ETH 信息获取函数。

    void lvgl_task1_cb(lv_timer_t *timer)
    {
    if(timer_count % 5 == 0){
    update_wifi_status();
    update_eth_status();
    timer_count = 1;
    }
    timer_count++;
    }

    void update_eth_status(void)
    {
    FILE *fp;
    char command[MAX_LINE_LEN];
    char result[MAX_LINE_LEN];
    char eth_name[MAX_LINE_LEN] = {0};
    char eth_ip_address[MAX_LINE_LEN] = {0};
    strcpy(command, "ifconfig");

    fp = popen(command, "r");
    if (fp == NULL) {
    printf("Failed to run command\n");
    return;
    }

    int in_eth_block = 0;
    int has_ip = 0;
    while (fgets(result, sizeof(result) - 1, fp) != NULL)
    {
    if (strstr(result, "eth0")) {
    in_eth_block = 1;
    sscanf(result, "%s", eth_name);
    lv_label_set_text(ui_LabelEth, eth_name);
    continue;
    }

    if (in_eth_block && result[0] == '\n') {
    in_eth_block = 0;
    break;
    }

    if (in_eth_block && strstr(result, "inet addr:")) {
    char *ip_start = strstr(result, "inet addr:");
    if (ip_start) {
    ip_start += 10;
    char *ip_end = strchr(ip_start, ' ');
    if (ip_end) {
    *ip_end = '\0';
    }
    strncpy(eth_ip_address, ip_start, MAX_LINE_LEN - 1);
    eth_ip_address[MAX_LINE_LEN - 1] = '\0';
    has_ip = 1;
    }
    break;
    }
    }
    pclose(fp);

    if (has_ip) {
    lv_label_set_text(ui_LabelEthIP, eth_ip_address);
    lv_obj_set_style_border_color(ui_PanelEth, lv_color_hex(0x3F3622), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(ui_LabelEth, lv_color_hex(0xF6AC05), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_img_set_src(ui_ImageEth, &ui_img_icon_eth_on_png);
    } else {
    lv_label_set_text(ui_LabelEthIP, "No IP");
    lv_obj_set_style_border_color(ui_PanelEth, lv_color_hex(0x1F1F1F), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(ui_LabelEth, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_img_set_src(ui_ImageEth, &ui_img_icon_eth_off_png);
    }
    }

    代码步骤说明

    • **命令执行:**使用 popen(command, "r") 执行 ifconfig 命令,其中 command 被设置为 "ifconfig",以获取所有网络接口的状态信息。

    • 变量 in_eth_block 用于标识当前是否正在处理eth0接口的信息块。

    • 变量 has_ip 用于标记是否找到了有效的IP地址。

    • **解析输出:**循环读取 ifconfig 的每一行输出:

      • 如果发现包含 "eth0" 的行,设置 in_eth_block 为1,并尝试从该行中提取eth0接口的名称。
      • 如果在eth0信息块内遇到空行,认为eth0接口的信息已经结束,退出循环。
      • 如果在eth0信息块内并且行中包含 "inet addr:",则进一步解析出IP地址,并设置 has_ip 为1。
    • 关闭文件指针:使用 pclose(fp) 关闭由 popen 打开的文件指针。

    • 更新UI:根据 has_ip 的值判断是否有找到有效的IP地址。

      • 如果IP有效,则调用 lv_label_set_text 函数更新标签, lv_obj_set_style_border_color 修改边框颜色, lv_obj_set_style_text_color 修改文本颜色, lv_img_set_src 修改图标。
      • 如果没有找到有效的IP地址,则设置默认文本 "No IP" 并打印一条消息到控制台。
  5. 至此,显示 ETH 状态部分完成,运行图像如下:

3.5 显示时间

本节主要介绍自定义字体和如何通过 Socket 套接字获取 NTP服务器时间。

  1. ui.c 中定义时间显示控件全局变量,定义如下:

    lv_obj_t * ui_LabelTime;
    lv_obj_t * ui_LabelDate;
    • 定义了当前时间标签 ui_LabelTime ,日期标签 ui_LabelDate
  2. ui.h 中外部声明控件变量,可以供 ui_ScreenMain.c 文件使用,内容如下:

    extern lv_obj_t * ui_LabelTime;
    extern lv_obj_t * ui_LabelDate;
  3. ui_SrceenMain.c 中编写时间显示控件:

    void ui_ScreenMain_screen_init(void)
    {
    ...
    ui_LabelTime = lv_label_create(ui_ScreenMain);
    lv_obj_set_width(ui_LabelTime, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelTime, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_LabelTime, -5);
    lv_obj_set_y(ui_LabelTime, -150);
    lv_obj_set_align(ui_LabelTime, LV_ALIGN_CENTER);
    lv_label_set_text(ui_LabelTime, "20:38");
    lv_obj_set_style_text_color(ui_LabelTime, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_LabelTime, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelTime, &ui_font_HarmonyOS200, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelDate = lv_label_create(ui_ScreenMain);
    lv_obj_set_width(ui_LabelDate, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelDate, LV_SIZE_CONTENT);
    lv_obj_set_align(ui_LabelDate, LV_ALIGN_CENTER);
    lv_label_set_text(ui_LabelDate, "2024-12-25 WED");
    lv_obj_set_style_text_color(ui_LabelDate, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_LabelDate, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelDate, &lv_font_montserrat_36, LV_PART_MAIN | LV_STATE_DEFAULT);
    }
    • 这些代码段创建并配置了两个标签 ui_LabelTimeui_LabelDate,分别用于显示时间和日期,并设置了它们的文本内容、颜色、透明度和字体,之前已详细介绍过相关函数的使用。
  4. 其中,ui_font_HarmonyOS200 是自定义字体,LVGL 库中提供的字体大小范围有限,所以需要调用自定义字体。添加自定义字体流程如下:

    • 首先,点击在线字体转换工具,将字库文件转换成 C 语言数组字体文件。

    • 随后,将文件移入 fonts 文件夹中。工程目录如下:

      /lvgl/lvgl_project_86panel/source/86panel_demo
      .
      ├── fonts
      │   └── ui_font_HarmonyOS200.c
      ├── images
      │   └── luckfox_logo.c
      ├── screens
      │   ├── ui_screenMain.c
      │   ├── ui_screenPlayer.c
      │   └── ui_screenWpa.c
      ├── ui.c
      └── ui.h
    • ui.h 中声明字体变量。

      LV_FONT_DECLARE(ui_font_HarmonyOS200);
  5. ui_custom.c 中实现时间校准处理函数 update_time() 通过NTP协议与服务器同步时间,并更新系统时钟和UI显示。

    char* update_time(void)
    {
    static char time_str[20];
    const char *ntp_server = "202.120.2.101";

    int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sockfd == -1)
    {
    perror("socket error");
    return NULL;
    }

    struct timeval timeout = {5, 0};
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));

    struct addrinfo hints,*results;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_protocol = IPPROTO_UDP;
    int status = getaddrinfo(ntp_server, "123", &hints, &results);
    if (status != 0)
    {
    printf("getaddrinfo error: %s" ,gai_strerror(status));
    return NULL;
    }
    struct sockaddr_in ntp_addr;
    memcpy(&ntp_addr, results->ai_addr, sizeof(struct sockaddr_in));
    freeaddrinfo(results);
    ntp_addr.sin_port = htons(123);

    char ntp_pkt[48];
    memset(ntp_pkt, 0, sizeof(ntp_pkt));
    ntp_pkt[0] = 0x1b;

    if (sendto(sockfd, ntp_pkt, sizeof(ntp_pkt), 0, (struct sockaddr *)&ntp_addr, sizeof(ntp_addr)) == -1)
    {
    perror("sendto error");
    close(sockfd);
    return NULL;
    }

    int nbytes;
    char buf[1024];
    struct sockaddr_in srv_addr;
    socklen_t srv_addr_len = sizeof(srv_addr);
    while ((nbytes = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&srv_addr, &srv_addr_len)) == -1)
    {
    if (errno == EAGAIN)
    {
    printf("Timeout occurred while waiting for NTP response.\n");
    }
    else
    {
    perror("recvfrom error");
    }
    close(sockfd);
    return NULL;
    }
    if (nbytes == sizeof(ntp_pkt))
    {
    char command[64];
    unsigned long long *timestamp = (unsigned long long *)&buf[40];
    time_t linux_time = ntohl(*timestamp) - 2208988800UL;

    linux_time = linux_time + 8*3600;
    struct tm *tm_ntp = localtime(&linux_time);

    char temp_time_str[20];
    strftime(temp_time_str, sizeof(temp_time_str), "%Y-%m-%d %H:%M:%S", tm_ntp);
    strftime(time_str, sizeof(time_str), "%Y-%m-%d %b %H:%M:%S", tm_ntp);
    printf("time_str:%s\n",time_str);
    snprintf(command, sizeof(command), "date -s \"%s\"", temp_time_str);
    system(command);
    system("hwclock -w");
    system("sync");
    }
    else
    {
    printf("Received invalid NTP packet.");
    }
    close(sockfd);
    return time_str;
    }

    void format_and_update_labels(const char *time_str) {
    char date_part[30];
    char time_part[20];
    printf("Formatted time: %s\n", time_str);
    const char *space1 = strchr(time_str, ' ');
    const char *space2 = strchr(space1 + 1, ' ');

    if (space1 && space2) {
    snprintf(date_part, sizeof(date_part), "%.10s %.3s",
    time_str,
    space1 + 1);
    snprintf(time_part, sizeof(time_part), "%.*s", 5,space2 + 1);

    lv_label_set_text(ui_LabelDate, date_part);
    lv_label_set_text(ui_LabelTime, time_part);
    } else {
    fprintf(stderr, "Invalid time string format.\n");
    }
    }

    代码步骤说明

    • update_time() 函数
      • 创建UDP套接字:初始化与NTP服务器通信所需的UDP套接字。
      • 设置超时选项:配置套接字接收超时时间为5秒。
      • 解析NTP服务器地址:使用getaddrinfo()解析NTP服务器的地址信息。
      • 构建NTP请求包:构造标准的NTP请求数据包并发送给NTP服务器。
      • 接收NTP响应:接收来自NTP服务器的响应,并检查是否成功接收到正确的数据包。
      • 解析时间戳:从NTP响应数据包中提取时间戳,转换为本地时间。
      • 更新系统时间:通过系统命令更新操作系统的时间,并同步硬件时钟。
      • 返回格式化的时间字符串:返回格式化后的当前时间字符串。
    • format_and_update_labels() 函数
      • 分割时间字符串:从完整的NTP时间字符串中提取日期和时间部分。
      • 更新LVGL标签:使用lv_label_set_text()函数更新LVGL界面上的日期和时间标签。
  6. 在定时器回调函数 lvgl_taskmain_cb() 添加NTP时间更新函数与实时时间更新函数。

    void lvgl_taskmain_cb(lv_timer_t *timer)
    {
    if(timer_count % 5 == 0){
    update_wifi_status();
    update_eth_status();
    }
    if(timer_count / 3600 == 1){
    timer_count = 1;
    char* update = update_time();
    if(update == NULL){
    perror("update_time return NULL");
    }else{
    format_and_update_labels(update);
    }
    }
    time_t now = time(NULL);
    struct tm *tm_now = localtime(&now);
    char current_time_str[30];
    strftime(current_time_str, sizeof(current_time_str), "%Y-%m-%d %b %H:%M:%S", tm_now);
    format_and_update_labels(current_time_str);
    timer_count++;
    }

    代码步骤说明

    • Wi-Fi和以太网状态更新: 每5次调用时(即每隔5个定时器周期),调用 update_wifi_status()update_eth_status() 函数来更新网络状态。

    • 时间信息更新: 每3600次调用时(假设定时器间隔是1秒,则大约每小时),调用 update_time() 获取新的时间信息,并通过 format_and_update_labels() 更新到UI上。

    • 当前时间字符串生成与更新: 使用 time()localtime() 获取当前时间,并通过 strftime() 格式化成字符串。调用 format_and_update_labels(current_time_str) 更新UI上的时间显示。

  7. 至此,显示时间部分完成,运行图像如下:

3.6 切换界面功能

本节介绍切换界面标识和如何通过事件切换不同界面。

  1. LVGL 的事件处理机制,能够监听事件,识别事件源,并完成事件处理,其事件类型可以分为:输入设备事件、绘图事件、自定义事件、其他事件、特别事件,例如短按、长按为输入设备事件。

  2. ui_SrceenMain.c 中添加切换界面事件。

    lv_obj_add_event_cb(ui_PanelWifi, ui_event_PanelWifi, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ScreenMain, ui_event_ScreenMain, LV_EVENT_ALL, NULL);
    • 用户可以通过 lv_obj_add_event_cb 函数来添加事件,例如:ui_PanelWifi 为指向对象的指针,ui_event_PanelWifi 为事件回调函数, LV_EVENT_ALL 是事件类型,NULL 为用户传输的参数。
  3. ui.c 中实现事件回调函数。

    void _ui_screen_change(lv_obj_t ** target, lv_scr_load_anim_t fademode, int spd, int delay, void (*target_init)(void))
    {
    if(*target == NULL)
    target_init();
    lv_scr_load_anim(*target, fademode, spd, delay, false);
    }

    void ui_event_PanelWifi(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED) {
    _ui_screen_change(&ui_ScreenWpa, LV_SCR_LOAD_ANIM_FADE_ON, 0, 0, &ui_ScreenWpa_screen_init);
    }
    }

    void ui_event_ScreenMain(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_BOTTOM) {
    lv_indev_wait_release(lv_indev_get_act());
    _ui_screen_change(&ui_ScreenPlayer, LV_SCR_LOAD_ANIM_MOVE_BOTTOM, 0, 0, &ui_ScreenPlayer_screen_init);
    }
    }

    代码步骤说明

    • _ui_screen_change 函数实现显示屏幕的切换,并可选择带动画效果

      • 检查目标屏幕是否已创建: 如果 *target 是 NULL,表示目标屏幕还没有被创建或初始化,此时将调用 target_init() 函数来初始化并创建目标屏幕。
      • 加载新屏幕并应用动画: *target 已经创建好的目标屏幕对象,fademode 动画模式,决定如何从当前屏幕切换到目标屏幕,spd 动画的速度(以毫秒为单位),delay 在执行动画之前的延迟时间(以毫秒为单位),false: 表示是否删除旧的屏幕。设置为 false,意味着不会自动删除旧的屏幕对象。
    • ui_event_PanelWifi 函数实现点击 WiFi 信息面板触发 WiFi 配置界面切换,并初始化该界面。

      • 获取事件类型:使用 lv_event_get_code(e) 获取当前事件的类型。

      • 获取事件目标对象:使用 lv_event_get_target(e) 获取触发事件的对象。

      • 判断是否为点击事件:如果事件类型是 LV_EVENT_CLICKED,则执行后续的操作。

      • 屏幕切换及初始化:调用 _ui_screen_change 函数切换到Wi-Fi配置界面,并使用淡入动画效果。

    • ui_event_ScreenMain 函数实现手势事件的处理,当检测到下滑手势时,将切换到播放器界面。

    • 判断是否为下滑手势事件: 判断事件类型是 LV_EVENT_GESTURE 并且通过 lv_indev_get_gesture_dir(lv_indev_get_act()) 检测到的方向是 LV_DIR_BOTTOM

    • 等待输入设备释放:调用 lv_indev_wait_release(lv_indev_get_act()) 确保输入设备(如触摸屏)完成释放动作,避免重复触发。

    • 切换 WiFi 配置界面时为短按事件触发,切换副界面是为下滑事件触发。

4.WiFi 配置界面

WiFi 配置界面示例程序如下:lvgl_project_86panel_wifi.tar.gz

4.1 账号密码设置功能

本节介绍文本框控件的使用以及账号密码信息的设置。

  1. 为了在不同场景下可以更换 WiFi ,需要提供输入控件来配置 WiFi 信息以至于可以联网登录,在 ui_ScreenWpa.c 添加文本框控件。

    void ui_ScreenWpa_screen_init(void)
    {
    ui_LabelSSID = lv_label_create(ui_ScreenWpa);
    ...

    ui_TextAreaSSID = lv_textarea_create(ui_ScreenWpa);
    lv_obj_set_width(ui_TextAreaSSID, 440);
    lv_obj_set_height(ui_TextAreaSSID, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_TextAreaSSID, 240);
    lv_obj_set_y(ui_TextAreaSSID, 146);
    lv_textarea_set_text(ui_TextAreaSSID, "Waceshare-WiFI-5G");
    lv_textarea_set_one_line(ui_TextAreaSSID, true);
    lv_obj_set_style_text_color(ui_TextAreaSSID, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_TextAreaSSID, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_TextAreaSSID, &lv_font_montserrat_32, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_TextAreaSSID, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_TextAreaSSID, 15, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_TextAreaSSID, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_TextAreaSSID, 25, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_side(ui_TextAreaSSID, LV_BORDER_SIDE_FULL, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelPW = lv_label_create(ui_ScreenWpa);
    ...

    ui_TextAreaPW = lv_textarea_create(ui_ScreenWpa);
    lv_obj_set_width(ui_TextAreaPW, 440);
    lv_obj_set_height(ui_TextAreaPW, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_TextAreaPW, 240);
    lv_obj_set_y(ui_TextAreaPW, 265);
    lv_textarea_set_text(ui_TextAreaPW, "123456");
    lv_textarea_set_one_line(ui_TextAreaPW, true);
    lv_obj_set_style_text_color(ui_TextAreaPW, lv_color_hex(0xA9A8A8), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_TextAreaPW, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_TextAreaPW, &lv_font_montserrat_32, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_TextAreaPW, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_TextAreaPW, 15, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_TextAreaPW, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_TextAreaPW, 25, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_left(ui_TextAreaPW, 20, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_right(ui_TextAreaPW, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_top(ui_TextAreaPW, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_bottom(ui_TextAreaPW, 10, LV_PART_MAIN | LV_STATE_DEFAULT);

    lv_obj_set_style_text_color(ui_TextAreaPW, lv_color_hex(0xFFFFFF), LV_PART_CURSOR | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_TextAreaPW, 255, LV_PART_CURSOR | LV_STATE_DEFAULT);
    }

    代码步骤说明

    • 创建文本框:通过 lv_textarea_create() 函数创建一个新的文本框对象,并将其添加到指定的父容器( ui_ScreenWpa 屏幕)中。
    • 设置文本内容:使用 lv_textarea_set_text() 函数为文本框设定初始文本 。
    • 单行模式:调用 lv_textarea_set_one_line(true) 将文本框设置为单行模式,限制输入或显示的内容不超过一行。
  2. ui_custom.c 中定义 wifi_scr_init函数以及 wifi_conf_get 函数。

    static void wifi_conf_get(char* ssid, char* passwd)
    {
    FILE *file = fopen(WPA_FILE_PATH, "r");
    if (file == NULL) {
    printf("Failed to open file.\n");
    return ;
    }

    char line[MAX_LINE_LEN];
    int inside_network_block = 0;

    while (fgets(line, MAX_LINE_LEN, file)) {
    // Enter network={} block
    if (strstr(line, "network={")) {
    inside_network_block = 1;
    continue;
    }
    // Exit network={} block
    if (strstr(line, "}")) {
    inside_network_block = 0;
    }
    // Inside network={} block
    if (inside_network_block) {
    if (strstr(line, "ssid=")) {
    sscanf(line, " ssid=\"%[^\"]\"", ssid);
    }
    if (strstr(line, "psk=")) {
    sscanf(line, " psk=\"%[^\"]\"", passwd);
    }
    }
    }
    fclose(file);
    return ;
    }

    void wifi_scr_init()
    {
    char ssid[MAX_CONF_LEN];
    char passwd[MAX_CONF_LEN];

    memset(ssid, 0, MAX_CONF_LEN);
    memset(passwd, 0, MAX_CONF_LEN);
    wifi_conf_get(ssid,passwd);

    if (strlen(ssid) == 0) {
    ssid[0] = '\0';
    }
    if (strlen(passwd) == 0) {
    passwd[0] = '\0';
    }

    lv_textarea_set_text(ui_TextAreaSSID, ssid);
    lv_textarea_set_text(ui_TextAreaPW, passwd);
    }

    代码步骤说明

    • wifi_conf_get 函数主要功能是从指定路径的配置文件(wpa_supplicant)中读取 WiFi 的 SSID 和密码,并将它们存储到提供的缓冲区中。

      • 打开配置文件,循环读取文件中的每一行,寻找包含网络配置的部分(即network={}块)。
      • 在找到的网络配置块内,查找 ssid= psk= 对应的行,并使用 sscanf 解析出 SSID 和密码值。
      • 关闭文件。
    • wifi_scr_init 函数负责将配置信息显示在 LVGL 的文本框控件上。

      • 通过调用 wifi_conf_get 获取 SSID 和密码。
      • 检查ssidpasswd是否为空字符串,如果是,则确保它们至少包含终止符\0
      • 使用lv_textarea_set_text函数更新LVGL界面上相应的文本框控件内容,分别对应于SSID和密码。
  3. ui_ScreenWpa_screen_init 函数中调用 wifi_scr_init 函数使初始化界面时自动填充 /etc/wpa_supplicant.conf 文件的信息到文本框中。

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    wifi_scr_init();
    ...
    }
  4. 至此,账号密码显示部分完成,运行图像如下:

4.2 虚拟键盘功能

本节介绍虚拟键盘控件的使用。

  1. 在 wifi 配置界面时,常常需要不同 WiFi 的名称(SSID)和密码(passward)来配置信息,此时就需要给文本框添加输入事件并且可以弹出虚拟键盘以供使用。首先在 ui_ScreenWpa.c 中添加虚拟键盘控件。

    #include "../ui.h"

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_Keyboard1 = lv_keyboard_create(ui_ScreenWpa);
    lv_obj_set_width(ui_Keyboard1, 720);
    lv_obj_set_height(ui_Keyboard1, 365);
    lv_obj_set_x(ui_Keyboard1, 0);
    lv_obj_set_y(ui_Keyboard1, 171);
    lv_obj_set_align(ui_Keyboard1, LV_ALIGN_CENTER);
    lv_obj_add_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); /// Flags
    lv_obj_set_style_bg_color(ui_Keyboard1, lv_color_hex(0x3D3D3D), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_Keyboard1, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_Keyboard1, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_Keyboard1, 255, LV_PART_MAIN | LV_STATE_DEFAULT);

    lv_obj_set_style_bg_color(ui_Keyboard1, lv_color_hex(0x7A7A7A), LV_PART_ITEMS | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_Keyboard1, 255, LV_PART_ITEMS | LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(ui_Keyboard1, lv_color_hex(0xFFFFFF), LV_PART_ITEMS | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_Keyboard1, 255, LV_PART_ITEMS | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_Keyboard1, &lv_font_montserrat_28, LV_PART_ITEMS | LV_STATE_DEFAULT);
    ...
    }
    • 虚拟键盘控件创建通过 lv_keyboard_create() ,函数中设置了控件的位置大小并且将样式设置为暗色调,在默认情况下隐藏键盘。
  2. 添加点击文本框弹出虚拟键盘事件,在 ui_SrceenWpa.c 中添加事件回调函数。

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    lv_obj_add_event_cb(ui_Keyboard1, ui_event_keyboard, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_TextAreaSSID, ui_event_TextAreaSSID, LV_EVENT_ALL, ui_Keyboard1);
    lv_obj_add_event_cb(ui_TextAreaPW, ui_event_TextAreaPW, LV_EVENT_ALL, ui_Keyboard1);
    ...
    }
  3. ui.c 中实现事件回调函数。

    void ui_event_TextAreaSSID(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED) {
    lv_keyboard_set_textarea(ui_Keyboard1, ui_TextAreaSSID);
    lv_obj_clear_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); /// Flags
    }
    }

    void ui_event_TextAreaPW(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED) {
    lv_keyboard_set_textarea(ui_Keyboard1, ui_TextAreaPW);
    lv_obj_clear_flag(ui_Keyboard1, LV_OBJ_FLAG_HIDDEN); /// Flags
    }
    }

    void ui_event_keyboard1(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t *target = lv_event_get_target(e);
    if(event_code == LV_EVENT_READY || event_code == LV_EVENT_CANCEL){
    lv_obj_add_flag(target, LV_OBJ_FLAG_HIDDEN);
    }
    }
    • 在函数 ui_event_TextAreaSSID()ui_event_TextAreaPW() 中,点击文本框清除虚拟键盘隐藏标志,并使用 lv_keyboard_set_textarea() 函数设置虚拟键盘关联的对象。
    • 在虚拟键盘回调事件 ui_event_keyboard() 中,按下回车按键与取消按键时隐藏虚拟键盘,达到关闭键盘的效果。
  4. 至此,显示虚拟键盘完成,运行图像如下:

4.3 WiFi 扫描功能

本节主要介绍如何通过按键启动 WiFi 扫描并填充信息到下拉列表中。

  1. 创建下拉列表,以便显示扫描信息。

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_PanelList = lv_obj_create(ui_ScreenWpa);
    ...

    ui_DropdownSSID = lv_dropdown_create(ui_PanelList);
    lv_dropdown_set_options(ui_DropdownSSID,
    "Waceshare-WiFI-5G\nWaceshare-WiFI-5G\nWaceshare-WiFI-5G\nWaceshare-WiFI-5G\nWaceshare-WiFI-5G\nWaceshare-WiFI-5G");
    lv_obj_set_width(ui_DropdownSSID, 440);
    lv_obj_set_height(ui_DropdownSSID, 64);
    lv_obj_set_x(ui_DropdownSSID, 240);
    lv_obj_set_y(ui_DropdownSSID, 18);
    lv_obj_add_flag(ui_DropdownSSID, LV_OBJ_FLAG_SCROLL_ON_FOCUS); /// Flags
    lv_obj_set_style_text_color(ui_DropdownSSID, lv_color_hex(0x545454), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_DropdownSSID, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_DropdownSSID, &lv_font_montserrat_32, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_radius(ui_DropdownSSID, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_DropdownSSID, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_DropdownSSID, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_DropdownSSID, lv_color_hex(0x333333), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_DropdownSSID, 255, LV_PART_MAIN | LV_STATE_DEFAULT);

    lv_obj_set_style_text_color(lv_dropdown_get_list(ui_DropdownSSID), lv_color_hex(0x545454),
    LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(lv_dropdown_get_list(ui_DropdownSSID), 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(lv_dropdown_get_list(ui_DropdownSSID), &lv_font_montserrat_30,
    LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(lv_dropdown_get_list(ui_DropdownSSID), lv_color_hex(0x000000),
    LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(lv_dropdown_get_list(ui_DropdownSSID), 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(lv_dropdown_get_list(ui_DropdownSSID), lv_color_hex(0x333333),
    LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(lv_dropdown_get_list(ui_DropdownSSID), 255, LV_PART_MAIN | LV_STATE_DEFAULT);

    lv_obj_set_style_text_color(lv_dropdown_get_list(ui_DropdownSSID), lv_color_hex(0x808080),
    LV_PART_SELECTED | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(lv_dropdown_get_list(ui_DropdownSSID), 255, LV_PART_SELECTED | LV_STATE_DEFAULT);

    ui_LabelWLAN = lv_label_create(ui_PanelList);
    ...
    ...
    }

    代码步骤说明

    • **创建下拉列表控件:**使用 lv_dropdown_create() 函数创建一个新的下拉列表对象,并将其添加到指定的父容器中。
    • **设置下拉列表选项:**使用 lv_dropdown_set_options() 方法为下拉列表设定多个选项。每个选项之间用换行符 \n 分隔。
    • 调整尺寸与位置
      • 设置下拉列表的宽度和高度,以适应您的设计需求。
      • 设置下拉列表在屏幕上的具体位置(X, Y坐标),确保它正确地放置在其父容器内。
    • 设置标志位
      • 添加 LV_OBJ_FLAG_SCROLL_ON_FOCUS 标志,以便在焦点移动时能够自动滚动到选中的项目。
    • 自定义样式
      • 文本颜色与透明度:设置下拉列表文本的颜色及透明度,以确保良好的可读性。
      • 字体大小与类型:选择合适的字体大小和类型,以增强用户体验。
      • 圆角半径:通过设置圆角半径来调整下拉列表的外观,使其更加现代化或符合特定的设计规范。
      • 背景颜色与透明度:设置背景颜色及其透明度,这里特别提到将背景设为黑色以匹配黑色主题。
      • 边框颜色与透明度:定义边框的颜色和透明度,增加视觉上的层次感。
  2. 创建 WiFi 信息显示文本框,标识信号强度与加密方式。

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_LabelRSSI = lv_label_create(ui_ScreenWpa);
    lv_label_set_text(ui_LabelRSSI, "RSSI");

    ...
    ui_TextAreaRSSI = lv_textarea_create(ui_ScreenWpa);
    lv_textarea_set_text(ui_TextAreaRSSI, "--dbm");

    ...
    ui_LabelMGMT = lv_label_create(ui_ScreenWpa);
    lv_label_set_text(ui_LabelMGMT, "PSK");

    ...
    ui_TextAreaMgnt = lv_textarea_create(ui_ScreenWpa);
    lv_textarea_set_text(ui_TextAreaMgnt, "------");
    ...
    }
  3. 创建扫描按键。

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_ButtonScan = lv_btn_create(ui_PanelBtn);
    lv_obj_set_width(ui_ButtonScan, 150);
    lv_obj_set_height(ui_ButtonScan, 80);
    lv_obj_set_x(ui_ButtonScan, 30);
    lv_obj_set_y(ui_ButtonScan, 24);
    lv_obj_set_style_radius(ui_ButtonScan, 20, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_ButtonScan, lv_color_hex(0x404040), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_ButtonScan, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_width(ui_ButtonScan, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    // lv_obj_set_style_shadow_spread(ui_ButtonScan, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_ImageScan = lv_img_create(ui_ButtonScan);
    lv_img_set_src(ui_ImageScan, &ui_img_scan_png);
    lv_obj_set_width(ui_ImageScan, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_ImageScan, LV_SIZE_CONTENT);
    lv_obj_set_align(ui_ImageScan, LV_ALIGN_CENTER);
    ...
    }

    代码步骤说明

    • 创建按钮:使用 lv_btn_create() 函数在指定的父容器 ui_PanelBtn 中创建一个新的按钮对象 ui_ButtonScan
    • 设置按钮的尺寸和位置。
    • 设置样式:使用 lv_obj_set_style_shadow_width() 函数将按钮阴影宽度设置为0,默认的按钮会有一点阴影增加立体感,但此时 UI 整体为平面,故设置为0。lv_obj_set_style_shadow_spread() 函数为阴影向外扩散的程度,如果阴影宽度为 0,即使设置了阴影扩散值,也不会有任何实际效果。
    • 创建图像:使用 lv_img_create() 函数在按钮 ui_ButtonScan 内部创建一个新的图像对象 ui_ImageScan
    • 设置图像源:调用 lv_img_set_src() 方法为图像对象设置具体的图片资源,这里使用的是 &ui_img_scan_png,表示一个预先定义好的PNG格式图像资源。
    • 自动调整尺寸:通过 lv_obj_set_width()lv_obj_set_height() 方法分别设置图像的宽度和高度为 LV_SIZE_CONTENT,这意味着图像将根据其原始尺寸自动调整大小,不会被拉伸或压缩。
    • 对齐方式:最后,使用 lv_obj_set_align() 方法将图像在其父对象(即按钮 ui_ButtonScan)内居中对齐,确保图像位于按钮的中央位置。
  4. ui_SrceenWpa.c 中添加扫描按键事件回调函数。

    #include "../ui.h"

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    lv_obj_add_event_cb(ui_DropdownSSID, ui_event_DropdownSSID, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ButtonScan, ui_event_ButtonScan, LV_EVENT_ALL, NULL);
    ...
    }
  5. ui.c 中实现事件回调函数。

    void ui_event_DropdownSSID(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if (event_code == LV_EVENT_VALUE_CHANGED)
    {
    char ssid[MAX_CONF_LEN];
    memset(ssid, 0, MAX_CONF_LEN);
    lv_dropdown_get_selected_str(ui_DropdownSSID, ssid, MAX_CONF_LEN);
    lv_textarea_set_text(ui_TextAreaSSID, ssid);
    for (int i = 0; i < network_count; i++){
    // printf("ui_id:[%s] len:%zu\n", networks[i].ssid, strlen(networks[i].ssid));
    // printf("ui_ssid:[%s] len:%zu\n", ssid, strlen(ssid));
    if (strstr(networks[i].ssid, ssid) != NULL) {
    char signal_level_str[20];
    snprintf(signal_level_str, sizeof(signal_level_str), "%d dbm", networks[i].signal_level);
    lv_textarea_set_text(ui_TextAreaMgnt,networks[i].flags);
    printf("ui_TextAreaMgnt:%s\n",networks[i].flags);
    lv_textarea_set_text(ui_TextAreaRSSI,signal_level_str);
    printf("ui_TextAreaRSSI:%s\n",signal_level_str);
    break;
    }
    }
    }
    }

    void ui_event_ButtonScan(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if (event_code == LV_EVENT_RELEASED && scan_is_button_pressed == false)
    {
    scan_is_button_pressed = true;
    wifi_scanning_ssid();
    scan_is_button_pressed = false;
    }
    }

    代码步骤说明

    • ui_event_DropdownSSID 函数:当用户更改下拉列表选项时,更新文本区域显示的SSID,并根据选择的SSID更新信号强度和标志信息标签。
    • ui_event_ButtonScan 函数:监听按钮点击事件,一旦检测到按钮被释放且 没有被重复点击,则启动Wi-Fi扫描。
  6. ui_custom.c 中实现扫描逻辑。

    int wifi_scanning_ssid()
    {
    network_count = 0;
    char command[] = "wpa_cli -i wlan0 scan_results";
    FILE *fp = popen(command, "r");
    if (fp == NULL) {
    perror("Error opening pipe");
    return -1;
    }

    char line[MAX_LINE_LEN];
    // Skip the first two lines as they contain header information
    fgets(line, MAX_LINE_LEN, fp);

    // Parse each line to extract the SSID
    while (fgets(line, MAX_LINE_LEN, fp) != NULL) {
    char *token = strtok(line, "\t ");
    int count = 0;
    while (token != NULL) {
    if (count == 4) { // SSID
    strncpy(networks[network_count].ssid, token, MAX_CONF_LEN);
    networks[network_count].ssid[MAX_CONF_LEN - 1] = '\0'; // Ensure null-termination
    } else if (count == 2) { // Signal level
    networks[network_count].signal_level = atoi(token);
    } else if (count == 3) { // Flags
    strncpy(networks[network_count].flags, token, MAX_CONF_LEN);
    networks[network_count].flags[MAX_CONF_LEN - 1] = '\0'; // Ensure null-termination
    }
    token = strtok(NULL, "\t ");
    count++;
    }
    if (strlen(networks[network_count].ssid) > 0) {
    network_count++;
    }
    if (network_count >= MAX_NETWORKS) {
    break;
    }
    }

    // Create a string with SSIDs separated by '\n'
    char ssid_string[MAX_NETWORKS * (MAX_CONF_LEN + 1)]; // 1 additional character for '\n'
    ssid_string[0] = '\0'; // Ensure ssid_string is empty initially

    for (int i = 0; i < network_count; i++) {
    if(strcmp(networks[i].ssid,"\n") && strlen(networks[i].ssid) < 16 )
    {
    strcat(ssid_string, networks[i].ssid);
    }
    }

    // Print the SSID string
    printf("SSID String:\n%s", ssid_string);
    if(ssid_string == NULL || *ssid_string == '\0')
    {
    if(ui_DropdownSSID != NULL)
    {
    lv_dropdown_set_options(ui_DropdownSSID, "scanning");
    }
    }
    else
    {
    if(ui_DropdownSSID != NULL)
    {
    strcat(ssid_string, "\n...");
    lv_dropdown_set_options(ui_DropdownSSID, ssid_string);
    }
    }

    pclose(fp);
    return 0;
    }
    • wifi_scanning_ssid 函数:执行Wi-Fi网络扫描命令并解析输出结果,提取SSID、信号强度和标志信息。将提取的信息存储在网络结构数组中,构建一个包含所有SSID的字符串并设置为下拉列表的选项。如果扫描未发现任何网络,则设置下拉列表显示“scanning”提示;否则,附加“...”至SSID列表末尾,并更新下拉列表选项。此函数通过管道执行命令并读取结果来实现网络扫描功能。
  7. 至此,按键扫描功能以及下拉框存储附近 SSID 部分完成,运行图像如下:

4.4 WiFi 连接功能

本节主要介绍如何通过按键启动 WiFi 连接。

  1. 创建 WiFi 连接按键,启动 WiFi 连接功能。

    #include "../ui.h"

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_ButtonConnect = lv_btn_create(ui_PanelBtn);
    lv_obj_set_width(ui_ButtonConnect, 150);
    lv_obj_set_height(ui_ButtonConnect, 80);
    lv_obj_set_x(ui_ButtonConnect, 200);
    lv_obj_set_y(ui_ButtonConnect, 24);
    lv_obj_set_style_radius(ui_ButtonConnect, 20, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_ButtonConnect, lv_color_hex(0x404040), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_ButtonConnect, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_width(ui_ButtonConnect, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_spread(ui_ButtonConnect, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_ImageConnect = lv_img_create(ui_ButtonConnect);
    lv_img_set_src(ui_ImageConnect, &ui_img_connect_png);
    lv_obj_set_width(ui_ImageConnect, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_ImageConnect, LV_SIZE_CONTENT);
    lv_obj_set_align(ui_ImageConnect, LV_ALIGN_CENTER);
    ...
    }
    • 与 WiFi 扫描按键设置类似
  2. ui_SrceenWpa.c 中添加连接按键事件回调函数。

    lv_obj_add_event_cb(ui_ButtonConnect, ui_event_ButtonConnect, LV_EVENT_ALL, NULL);
  3. ui.c 中实现事件回调函数。

    void ui_event_ButtonConnect(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t *obj = lv_event_get_target(e);

    if (event_code == LV_EVENT_RELEASED)
    {
    const char *ssid = lv_textarea_get_text(ui_TextAreaSSID);
    const char *passwd = lv_textarea_get_text(ui_TextAreaPW);
    if (strlen(ssid) != 0 && strlen(passwd) != 0) {
    wifi_connect(ssid, passwd);
    }
    }
    }
    • ui_event_ButtonConnect 函数:当连接按钮被释放时,获取SSID和密码文本区域的内容,若两者均非空,则调用 wifi_connect 函数加载Wi-Fi配置以尝试连接网络。
  4. ui_custom.h 中实现 WiFi 连接逻辑。

    void wifi_connect(const char* ssid, const char* password)
    {
    FILE *wpa_supplicant_pipe;
    char buffer[MAX_CONF_LEN];

    // open wpa_supplicant pipe
    wpa_supplicant_pipe = popen("wpa_cli", "w");
    if (wpa_supplicant_pipe == NULL) {
    perror("popen");
    exit(1);
    }
    printf("connect test\n");
    // set network ssid adn psk
    memset(buffer,0,MAX_CONF_LEN);
    snprintf(buffer, MAX_CONF_LEN, "set_network 0 ssid \"%s\"\n", ssid);
    fputs(buffer, wpa_supplicant_pipe);

    memset(buffer,0,MAX_CONF_LEN);
    snprintf(buffer, MAX_CONF_LEN, "set_network 0 psk \"%s\"\n", password);
    fputs(buffer, wpa_supplicant_pipe);

    // save wifi conf
    fputs("save_config\n", wpa_supplicant_pipe);
    pclose(wpa_supplicant_pipe);

    // save wifi conf to /etc/wpa_supplicant.conf
    FILE *file = fopen(WPA_FILE_PATH, "r");
    if (file == NULL) {
    printf("Failed to open file.\n");
    return ;
    }

    FILE *temp_file = fopen("temp_wpa_supplicant.conf", "w");
    if (temp_file == NULL) {
    printf("Failed to create temporary file.\n");
    fclose(file);
    return ;
    }

    char line[MAX_LINE_LEN];
    int inside_network_block = 0;

    while (fgets(line, MAX_LINE_LEN, file)) {
    // Enter network={} block
    if (strstr(line, "network={")) {
    inside_network_block = 1;
    fputs(line, temp_file);
    continue;
    }
    // Exit network={} block
    if (strstr(line, "}")) {
    inside_network_block = 0;
    }
    // Inside network={} block
    if (inside_network_block) {
    if (strstr(line, "ssid=")) {
    memset(buffer,0,MAX_CONF_LEN);
    sprintf(buffer, " ssid=\"%s\"\n",ssid);
    fputs(buffer, temp_file);
    }
    else if (strstr(line, "psk=")) {
    memset(buffer,0,MAX_CONF_LEN);
    sprintf(buffer, " psk=\"%s\"\n",password);
    fputs(buffer, temp_file);
    }
    else {
    fputs(line, temp_file);
    }
    }
    else {
    fputs(line, temp_file);
    }
    }

    fclose(file);
    fclose(temp_file);

    remove(WPA_FILE_PATH);
    rename("temp_wpa_supplicant.conf", WPA_FILE_PATH);
    //printf("SSID and PSK replaced successfully.\n");

    // reconnect wifi
    // system("killall -9 wpa_cli");
    system("killall -9 wpa_supplicant");
    system("killall -9 udhcpc");

    sleep(1);
    system("wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf");
    sleep(1);
    system("wpa_cli reconfigure &");
    sleep(5);
    system("udhcpc -i wlan0 &");
    return ;
    }
    • wifi_connect 函数:通过管道与 wpa_cli 交互设置指定的SSID和密码,并保存配置。接着,打开并读取当前的WPA配置文件,根据输入的SSID和密码更新其中的相关信息,保存至临时文件后替换原配置文件。最后,重启 wpa_supplicantudhcpc 服务以应用新的Wi-Fi配置。

4.5 WiFi 断开功能

本节主要介绍如何通过按键断开 WiFi 连接。

  1. 创建 WiFi 断开按键,启动 WiFi 断开功能。

    #include "../ui.h"

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_ButtonDiscon = lv_btn_create(ui_PanelBtn);
    lv_obj_set_width(ui_ButtonDiscon, 150);
    lv_obj_set_height(ui_ButtonDiscon, 80);
    lv_obj_set_x(ui_ButtonDiscon, 370);
    lv_obj_set_y(ui_ButtonDiscon, 24);
    lv_obj_set_style_radius(ui_ButtonDiscon, 20, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_ButtonDiscon, lv_color_hex(0x404040), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_ButtonDiscon, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_width(ui_ButtonDiscon, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_spread(ui_ButtonDiscon, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_ImageDiscon = lv_img_create(ui_ButtonDiscon);
    lv_img_set_src(ui_ImageDiscon, &ui_img_discon_png);
    lv_obj_set_width(ui_ImageDiscon, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_ImageDiscon, LV_SIZE_CONTENT);
    lv_obj_set_align(ui_ImageDiscon, LV_ALIGN_CENTER);
    ...
    }
    • 与 WiFi 扫描按键设置类似
  2. ui_SrceenWpa.c 中添加连接按键事件回调函数。

    lv_obj_add_event_cb(ui_ButtonDiscon, ui_event_ButtonDiscon, LV_EVENT_ALL, NULL);
  3. ui.c 中实现事件回调函数。

    void ui_event_ButtonDiscon(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t *obj = lv_event_get_target(e);
    if (event_code == LV_EVENT_RELEASED)
    {
    lv_textarea_set_text(ui_TextAreaSSID,"");
    lv_textarea_set_text(ui_TextAreaPW,"");
    disconnect_wifi("wlan0");
    }
    }
    • ui_event_ButtonDiscon 函数:当断开连接按钮被释放时,清空SSID和密码文本区域的内容,并调用 disconnect_wifi 函数来断开指定接口(如"wlan0")的Wi-Fi连接。
  4. ui_custom.c 中实现 WiFi 断开逻辑。

    int wifi_disconnect(const char *interface) {
    char command[128];
    snprintf(command, sizeof(command), "wpa_cli -i %s disconnect", interface);
    int ret = system(command);
    if (ret == -1) {
    perror("system");
    return -1;
    }

    snprintf(command, sizeof(command), "ifconfig %s 0.0.0.0", interface);
    ret = system(command);
    if (ret == -1) {
    perror("system");
    return -1;
    }

    return 0;
    }
    • wifi_disconnect 函数:构建并执行一系列命令来断开指定网络接口的Wi-Fi连接,首先通过 wpa_cli 命令断开连接,然后使用 ifconfig 命令将该接口的IP地址设置为0.0.0.0,以此确保接口完全断开网络连接。如果任一命令执行失败,则返回错误代码-1;否则,成功断开连接后返回0。此过程保证了Wi-Fi接口能够正确地与当前网络断开连接。

4.6 返回主界面功能

本节介绍如何通过按键返回主界面。

  1. 创建主界面返回按键。

    #include "../ui.h"

    void ui_ScreenWpa_screen_init(void)
    {
    ...
    ui_ButtonBack = lv_btn_create(ui_PanelBtn);
    lv_obj_set_width(ui_ButtonBack, 150);
    lv_obj_set_height(ui_ButtonBack, 80);
    lv_obj_set_x(ui_ButtonBack, 257);
    lv_obj_set_y(ui_ButtonBack, 0);
    lv_obj_set_align(ui_ButtonBack, LV_ALIGN_CENTER);
    lv_obj_set_style_radius(ui_ButtonBack, 20, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_ButtonBack, lv_color_hex(0x404040), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_ButtonBack, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_width(ui_ButtonBack, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_spread(ui_ButtonBack, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_ImageBack = lv_img_create(ui_ButtonBack);
    lv_img_set_src(ui_ImageBack, &ui_img_back_png);
    lv_obj_set_width(ui_ImageBack, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_ImageBack, LV_SIZE_CONTENT);
    lv_obj_set_align(ui_ImageBack, LV_ALIGN_CENTER);
    ...
    }
    • 与 WiFi 扫描按键设置类似
  2. ui_SrceenWpa.c 中添加主界面返回按键事件。

    lv_obj_add_event_cb(ui_ButtonBack, ui_event_ButtonBack, LV_EVENT_ALL, NULL);
  3. ui.c 中实现事件回调函数。

    void ui_event_ButtonBack(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED) {
    _ui_screen_change(&ui_ScreenMain, LV_SCR_LOAD_ANIM_FADE_ON, 0, 0, &ui_ScreenMain_screen_init);
    }
    }

    代码步骤说明

    • 屏幕切换:如果检测到点击事件,则调用 _ui_screen_change 函数来执行屏幕切换操作。

      _ui_screen_change() 函数接受多个参数:

      • 第一个参数是要切换到的目标屏幕指针(在这个例子中是 &ui_ScreenMain)。
      • 第二个参数是屏幕切换动画类型(在这里使用了 LV_SCR_LOAD_ANIM_FADE_ON,表示淡入动画)。
      • 第三和第四个参数通常是时间延迟和速度等选项,在此例中都设置为 0
      • 最后一个参数是一个指向初始化函数的指针(&ui_ScreenMain_screen_init),用于在屏幕加载之前或之后进行必要的初始化操作。
  4. 至此,按键功能完成,运行图像如下:

5.副界面

副界面示例程序如下:lvgl_project_86panel_music.tar.gz

5.1 继电器控制功能

本节介绍如何通过按键控制 GPIO 电平通断继电器。

  1. 创建 GPIO 电平切换功能按键

    #include "../ui.h"

    void ui_ScreenPlayer_screen_init(void)
    {
    ...
    ui_PanelRelay1 = lv_obj_create(ui_ScreenPlayer);
    lv_obj_set_width(ui_PanelRelay1, 320);
    lv_obj_set_height(ui_PanelRelay1, 160);
    lv_obj_set_x(ui_PanelRelay1, 30);
    lv_obj_set_y(ui_PanelRelay1, 530);
    lv_obj_set_style_radius(ui_PanelRelay1, 20, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_PanelRelay1, lv_color_hex(0xEC6900), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_PanelRelay1, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_color(ui_PanelRelay1, lv_color_hex(0xF6AC05), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(ui_PanelRelay1, LV_GRAD_DIR_VER, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_width(ui_PanelRelay1, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    ui_LabelRelay1 = lv_label_create(ui_PanelRelay1);
    lv_obj_set_width(ui_LabelRelay1, LV_SIZE_CONTENT);
    lv_obj_set_height(ui_LabelRelay1, LV_SIZE_CONTENT);
    lv_obj_set_align(ui_LabelRelay1, LV_ALIGN_CENTER);
    lv_label_set_text(ui_LabelRelay1, "Relay 1");
    lv_obj_set_style_text_color(ui_LabelRelay1, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_LabelRelay1, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_LabelRelay1, &lv_font_montserrat_48, LV_PART_MAIN | LV_STATE_DEFAULT);
    ...
    }

    代码步骤说明

    • **创建面板:**使用 lv_obj_create() 函数在指定的父容器 ui_ScreenPlayer 中创建一个新的面板对象 ui_PanelRelay1。并设置尺寸和位置以及样式。
    • 创建标签:使用 lv_obj_create() 函数在指定的父容器 ui_PanelRelay1 中创建一个居中的标签 ui_LabelRelay1。并设置尺寸和位置以及样式。
  2. ui_SrceenPlayer.c 中添加按键事件回调函数。

    lv_obj_add_event_cb(ui_PanelRelay1, ui_event_PanelRelay1, LV_EVENT_ALL, NULL);
  3. ui.c 中实现事件回调函数。

    void ui_event_PanelRelay1(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED) {
    if(lv_obj_get_style_bg_grad_dir(ui_PanelRelay1,LV_PART_MAIN) == LV_GRAD_DIR_VER)
    {
    set_gpio(32,0);
    lv_obj_set_style_bg_color(ui_PanelRelay1, lv_color_hex(0x696969), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(ui_PanelRelay1, LV_GRAD_DIR_NONE, LV_PART_MAIN | LV_STATE_DEFAULT);
    }
    else
    {
    set_gpio(32,1);
    lv_obj_set_style_bg_color(ui_PanelRelay1, lv_color_hex(0xEC6900), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_color(ui_PanelRelay1, lv_color_hex(0xF6AC05), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(ui_PanelRelay1, LV_GRAD_DIR_VER, LV_PART_MAIN | LV_STATE_DEFAULT);
    }
    }
    }

    int set_gpio(int gpio_pin, int val)
    {
    int len;
    char buff[10];
    char filename[64];
    int ret,result;

    memset(filename, 0x0, sizeof(filename));
    sprintf(filename, "/sys/class/gpio/gpio%d/value", gpio_pin);
    FILE *value_file = fopen(filename, "w");
    if (value_file == NULL)
    {
    ret = gpio_export(gpio_pin);
    if (ret < 0)
    {
    return ret;
    }
    else
    {
    result = gpio_out_direction(gpio_pin);
    if(result < 0)
    return result;
    if (value_file == NULL)
    return ret;
    }
    }
    memset(buff, 0x0, sizeof(buff));
    len = sprintf(buff, "%s", val ? "1" : "0");
    fprintf(value_file,buff);
    if (ret != len)
    {
    fclose(value_file);
    return ret;
    }
    fclose(value_file);
    return 0;
    }

    代码步骤说明

    • ui_event_PanelRelay1 函数:当面板 ui_PanelRelay1 被点击时,检查标签 ui_LabelStatus1 的文本内容。如果当前文本是"ON",则调用 set_gpio 函数将GPIO引脚32的状态设置为0(关闭状态),并将标签文本更新为"OFF";反之,若文本是"OFF",则将GPIO引脚32的状态设置为1(开启状态),并将标签文本更新为"ON"。
    • set_gpio 函数:首先构建目标GPIO值文件的路径,尝试打开该文件以写入新的GPIO状态值(0或1)。如果文件不存在,则先通过 gpio_export 函数导出GPIO引脚,并使用 gpio_out_direction 函数设置GPIO的方向为输出模式。然后再次尝试打开GPIO值文件并写入新状态值。如果在任何步骤中发生错误(例如文件无法打开或写入失败),则返回相应的错误代码;否则,在成功写入新状态后返回0,确保GPIO状态被正确设置。此过程保证了能够根据用户交互动态地控制GPIO引脚的状态,并同步更新UI上的状态显示。
  4. 对于另一继电器控制也是如此,运行图像如下:

5.2 音乐播放器 UI 组件及事件

本节包含图片按键、滑动条、滚轮列表的使用以及 MPV 相关函数 的调用

  1. 创建图片按键,一共有音乐播放/暂停图片按键,上一首音乐图片按键,下一首音乐图片按键,音乐列表图片按键,音乐播放模式按键。下面以音乐列表图片按键为例。

    void ui_ScreenPlayer_screen_init(void)
    {
    ...
    ui_ImgButtonList = lv_imgbtn_create(ui_PanelMusicPlayer);
    lv_imgbtn_set_src(ui_ImgButtonList, LV_IMGBTN_STATE_RELEASED, NULL, &ui_img_icon_music_list_png, NULL);
    lv_imgbtn_set_src(ui_ImgButtonList, LV_IMGBTN_STATE_PRESSED, NULL, &ui_img_icon_music_list_png, NULL);
    lv_imgbtn_set_src(ui_ImgButtonList, LV_IMGBTN_STATE_DISABLED, NULL, &ui_img_icon_music_list_png, NULL);
    lv_imgbtn_set_src(ui_ImgButtonList, LV_IMGBTN_STATE_CHECKED_PRESSED, NULL, &ui_img_icon_music_list_png, NULL);
    lv_imgbtn_set_src(ui_ImgButtonList, LV_IMGBTN_STATE_CHECKED_RELEASED, NULL, &ui_img_icon_music_list_png, NULL);
    lv_imgbtn_set_src(ui_ImgButtonList, LV_IMGBTN_STATE_CHECKED_DISABLED, NULL, &ui_img_icon_music_list_png, NULL);
    lv_obj_set_height(ui_ImgButtonList, 50);
    lv_obj_set_width(ui_ImgButtonList, LV_SIZE_CONTENT);
    lv_obj_set_x(ui_ImgButtonList, 411);
    lv_obj_set_y(ui_ImgButtonList, 296);
    ...
    }

    代码步骤说明

    • ui_ScreenPlayer_screen_init 函数:在初始化屏幕时,创建了一个图像按钮 ui_ImgButtonList 并将其添加到名为 ui_PanelMusicPlayer 的父容器中。为该图像按钮设置了不同状态(如释放、按下、禁用等)下的图标源,所有状态下均使用相同的图标资源 &ui_img_icon_music_list_png。设置了按钮的高度为50像素,宽度根据内容自动调整,并将其放置在坐标(411, 296)处。这种配置允许创建一个视觉上一致的图像按钮,其外观不会随交互状态变化而改变,确保了用户界面的简洁性和一致性。此过程展示了如何通过设置不同的状态图像来定义图像按钮的行为和外观,同时根据需要精确地控制其尺寸和位置。
  2. 创建音量滑动条。

    void ui_ScreenPlayer_screen_init(void)
    {
    ...
    ui_SliderMusic = lv_slider_create(ui_PanelMusicPlayer);
    lv_slider_set_value(ui_SliderMusic, 65, LV_ANIM_OFF);
    if(lv_slider_get_mode(ui_SliderMusic) == LV_SLIDER_MODE_RANGE) lv_slider_set_left_value(ui_SliderMusic, 0, LV_ANIM_OFF);
    lv_obj_set_width(ui_SliderMusic, 500);
    lv_obj_set_height(ui_SliderMusic, 10);
    lv_obj_set_x(ui_SliderMusic, 80);
    lv_obj_set_y(ui_SliderMusic, 421);
    lv_obj_set_style_bg_color(ui_SliderMusic, lv_color_hex(0x333232), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_SliderMusic, 255, LV_PART_MAIN | LV_STATE_DEFAULT);

    lv_obj_set_style_bg_color(ui_SliderMusic, lv_color_hex(0x7D7D7D), LV_PART_INDICATOR | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_SliderMusic, 255, LV_PART_INDICATOR | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_color(ui_SliderMusic, lv_color_hex(0xC4C4C4), LV_PART_INDICATOR | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(ui_SliderMusic, LV_GRAD_DIR_HOR, LV_PART_INDICATOR | LV_STATE_DEFAULT);

    lv_obj_set_style_bg_color(ui_SliderMusic, lv_color_hex(0xC4C4C4), LV_PART_KNOB | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_SliderMusic, 255, LV_PART_KNOB | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_left(ui_SliderMusic, 12, LV_PART_KNOB | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_right(ui_SliderMusic, 12, LV_PART_KNOB | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_top(ui_SliderMusic, 12, LV_PART_KNOB | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_bottom(ui_SliderMusic, 12, LV_PART_KNOB | LV_STATE_DEFAULT);
    ...
    }

    代码步骤说明

    • 创建滑块控件:使用 lv_slider_create() 函数在指定的父容器 ui_PanelMusicPlayer 中创建一个新的滑块控件 ui_SliderMusic
    • 设置初始值:通过 lv_slider_set_value() 方法将滑块的值设置为65,并关闭动画效果(LV_ANIM_OFF)。如果滑块模式是范围模式(LV_SLIDER_MODE_RANGE),则使用 lv_slider_set_left_value() 方法将左侧值设置为0,同样关闭动画效果。
    • 调整尺寸和位置
      • 设置滑块的宽度为500像素,高度为10像素。
      • 设置滑块的位置,X坐标为80像素,Y坐标为421像素,以确定其在父容器中的具体位置。
    • 最后,按需设置样式配置。
  3. 创建音乐选择列表。

    void ui_ScreenPlayer_screen_init(void)
    {
    ...
    ui_PanelBoard = lv_obj_create(ui_ScreenPlayer);
    lv_obj_set_width(ui_PanelBoard, 720);
    lv_obj_set_height(ui_PanelBoard, 720);
    lv_obj_set_align(ui_PanelBoard, LV_ALIGN_CENTER);
    lv_obj_clear_flag(ui_PanelBoard, LV_OBJ_FLAG_SCROLLABLE); /// Flags
    lv_obj_set_style_bg_color(ui_PanelBoard, lv_color_hex(0x0D0D0D), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_PanelBoard, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(ui_PanelBoard, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(ui_PanelBoard, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_left(ui_PanelBoard, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_right(ui_PanelBoard, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_top(ui_PanelBoard, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_pad_bottom(ui_PanelBoard, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_add_flag(ui_PanelBoard, LV_OBJ_FLAG_HIDDEN); /// Flags

    ui_RollerMusic = lv_roller_create(ui_PanelBoard);
    lv_roller_set_options(ui_RollerMusic,
    "1.Music List click to Play 001.mp3\n2.Music List click to Play 001.mp3\n3.Music List click to Play 001.mp3\n4.Music List click to Play 001.mp3\n5.Music List click to Play 001.mp3\n6.Music List click to Play 001.mp3",
    LV_ROLLER_MODE_NORMAL);
    lv_obj_set_width(ui_RollerMusic, 720);
    lv_obj_set_height(ui_RollerMusic, 576);
    lv_obj_set_x(ui_RollerMusic, 0);
    lv_obj_set_y(ui_RollerMusic, 144);
    lv_obj_add_flag(ui_RollerMusic, LV_OBJ_FLAG_HIDDEN);
    lv_obj_set_style_text_color(ui_RollerMusic, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_RollerMusic, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(ui_RollerMusic, &lv_font_montserrat_30, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_radius(ui_RollerMusic, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(ui_RollerMusic, lv_color_hex(0x1F1F1F), LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(ui_RollerMusic, 255, LV_PART_MAIN | LV_STATE_DEFAULT);
    lv_obj_set_style_border_width(ui_RollerMusic, 0, LV_PART_MAIN | LV_STATE_DEFAULT);

    lv_obj_set_style_text_color(ui_RollerMusic, lv_color_hex(0xFFA300), LV_PART_SELECTED | LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(ui_RollerMusic, 255, LV_PART_SELECTED | LV_STATE_DEFAULT);
    ...
    }

    代码步骤说明

    • 创建滚轮控件:使用 lv_roller_create() 函数在指定的父容器 ui_PanelBoard 中创建一个新的滚轮控件 ui_RollerMusic
    • 设置选项:通过 lv_roller_set_options() 方法为滚轮设置了多个选项,每个选项代表一个音乐列表项,使用换行符 \n 分隔。这里所有选项文本相同,仅为示例目的。
    • 调整尺寸和位置
      • 设置滚轮的宽度为720像素,高度为576像素。
      • 设置滚轮的位置,X坐标为0像素,Y坐标为144像素,以确定其在父容器中的具体位置。
    • 隐藏标志:添加 LV_OBJ_FLAG_HIDDEN 标志,使滚轮初始状态下不可见,直到需要显示时再取消此标志。
    • 最后,按需设置样式配置。
  4. ui_SrceenPlayer.c 中添加各个控件事件。

    void ui_ScreenPlayer_screen_init(void)
    {
    ...
    lv_obj_add_event_cb(ui_ImgButtonPrev, ui_event_ImgButtonPrev, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ImgButtonNext, ui_event_ImgButtonNext, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_SliderMusic, ui_event_SliderMusic, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ImgButtonList, ui_event_ImgButtonList, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ImagePlay, ui_event_ImagePlay, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ImageMusicMode, ui_event_ImageMusicMode, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_RollerMusic, ui_event_RollerMusic, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ImageMusicClose, ui_event_ImageMusicClose, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_ScreenPlayer, ui_event_ScreenPlayer, LV_EVENT_ALL, NULL);
    lv_obj_add_event_cb(ui_PanelBoard, ui_event_PanelBoard, LV_EVENT_ALL, NULL);
    ...
    }
  5. ui.c 中实现事件回调函数。

    void ui_event_ImgButtonPrev(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED && playing_music_node != NULL) {
    if(!CycleFlag)
    {
    playing_music_node = playing_music_node->prev;
    lv_roller_set_selected(ui_RollerMusic, playing_music_node->id, LV_ANIM_OFF);
    music_set_pos(playing_music_node->id);
    }
    else
    {
    music_set_pos(playing_music_node->id);
    }

    }
    }
    void ui_event_ImgButtonNext(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_PRESSED && playing_music_node != NULL) {
    if(!CycleFlag)
    {
    playing_music_node = playing_music_node->next;
    lv_roller_set_selected(ui_RollerMusic, playing_music_node->id, LV_ANIM_OFF);
    music_set_pos(playing_music_node->id);
    }
    else
    {
    music_set_pos(playing_music_node->id);
    }
    }
    }
    void ui_event_SliderMusic(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_VALUE_CHANGED) {
    int volume = lv_slider_get_value(target);
    music_set_volume(volume);
    }
    }
    void ui_event_ImgButtonList(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_RELEASED) {
    // lv_obj_clear_flag(ui_RollerMusic, LV_OBJ_FLAG_HIDDEN);
    lv_obj_clear_flag(ui_PanelBoard, LV_OBJ_FLAG_HIDDEN);
    GestureFlag = false;
    }
    }
    void ui_event_ImagePlay(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_PRESSED) {
    lv_state_t btn_state = lv_obj_get_state(target);
    if(btn_state &= LV_STATE_CHECKED)
    {
    music_pause(1);
    }
    else
    {
    music_pause(0);
    }
    }
    }
    void ui_event_ImageMusicMode(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_PRESSED) {
    lv_state_t btn_state = lv_obj_get_state(target);
    if(btn_state &= LV_STATE_CHECKED)
    {
    music_set_mode(0);
    CycleFlag = false;
    }
    else
    {
    music_set_mode(1);
    CycleFlag = true;
    }
    }
    }
    void ui_event_RollerMusic(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_VALUE_CHANGED && playing_music_node != NULL) {
    char buf[32];
    lv_roller_get_selected_str(target, buf, sizeof(buf));

    while(strcmp(playing_music_node->filename, buf))
    {
    playing_music_node = playing_music_node->next;
    }
    music_set_pos(playing_music_node->id);
    }
    }

    void ui_event_ImageMusicClose(lv_event_t *e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED)
    {
    lv_obj_add_flag(ui_RollerMusic,LV_OBJ_FLAG_HIDDEN);
    GestureFlag = true;
    }
    }

    void ui_event_PanelBoard(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_CLICKED)
    {
    lv_obj_add_flag(ui_PanelBoard,LV_OBJ_FLAG_HIDDEN);
    GestureFlag = true;
    }
    }

    代码步骤说明

    • ui_event_ImgButtonNext()ui_event_ImgButtonPrev() 分别处理下一曲和上一曲按钮的点击事件。
    • ui_event_SliderMusic() 处理音量滑块的变化事件,调整音乐播放的音量。
    • ui_event_ImgButtonList() 处理列表按钮的点击事件,用于显示或隐藏音乐选择滚轮。
    • ui_event_ImgPlay() 处理播放/暂停按钮的点击事件。
    • ui_event_ImageMusicMode() 处理音乐播放模式按钮的点击事件,切换循环播放模式和顺序播放模式。
    • ui_event_RollerMusic() 处理音乐选择滚轮的选择变化事件,更新当前播放的音乐文件。
    • ui_event_ImageMusicClose() 通过点击图标处理音乐列表的关闭事件,隐藏当前列表。
    • ui_event_PanelBoard() 通过点击列表外的界面处理音乐列表的关闭事件,隐藏当前列表。
  6. 至此,音乐播放器 UI 组件完成,运行图像如下:

5.3 音乐播放器逻辑功能

本节主要介绍 MPV 相关函数的具体实现。

  1. 上一节介绍了 UI 组件的函数调用,以下介绍音乐播放器逻辑函数的具体实现,在 ui.c 中,添加 music_player_thread_init() 函数,以便进入副界面时启动 mpv 工具。

    int music_player_thread_init()
    {
    pid = vfork();
    if (pid == 0) // child thread
    {
    char cmd[256];
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    execlp("mpv", "mpv", "--quiet", "--no-terminal", "--no-video", "--idle=yes", "--term-status-msg=", "--input-ipc-server=/tmp/mpvsocket", NULL);
    return 0;
    }
    else if (pid > 0) // parent thread
    {

    sleep(1);
    close(0);

    act.sa_handler = sigaction_exit_handler;
    sigfillset(&act.sa_mask);
    act.sa_flags = SA_RESTART; /* don't fiddle with EINTR */
    sigaction(SIGUSR1, &act, NULL);
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "/tmp/mpvsocket");

    fd_mpv = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd_mpv == -1)
    {
    perror("Create socket failed\n");
    return -1;
    }
    sleep(0.1);
    // socket connect
    if (connect(fd_mpv, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
    perror("Cannot connect to socket \n");
    return -1;
    }
    }
    else
    {
    perror("fork error:\n");
    return -1;
    }
    }

    void custom_init()
    {
    system("mpv 2>&1 >/dev/null");
    music_player_thread_init();
    }

    ///////////////////// SCREENS ////////////////////

    void ui_init(void)
    {
    lv_disp_t * dispp = lv_disp_get_default();
    lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED),false, LV_FONT_DEFAULT);
    lv_disp_set_theme(dispp, theme);
    custom_init();
    ui_ScreenMain_screen_init();
    ui_ScreenWpa_screen_init();
    ui_ScreenPlayer_screen_init();
    ui____initial_actions0 = lv_obj_create(NULL);
    lv_disp_load_scr(ui_ScreenPlayer);
    }

    代码步骤说明

    • 使用 vfork() 创建一个新的子进程。
    • 子进程操作时,使用 prctl() 函数设置父进程的死亡信号,如果父进程终止,子进程将立即被强制终止。使用 execlp() 函数执行当前进程映像为 mpv 命令行媒体播放器,并传递一系列参数以配置其行为。
    • 父进程操作时,执行 sleep(1) 确保子进程有足够的时间初始化,后使用 close(0) 关闭标准输入释放资源。设置信号处理函数 sigaction_exit_handler 处理 SIGUSR1 信号。配置 Unix 域套接字地址结构 addr,并创建一个 Unix 域套接字用于连接到 mpv 的 IPC 服务器。
  2. 设置界面初始化时的 mpv 控制命令函数,如暂停/继续播放、设置音乐列表、调整音量和切换循环模式。

    void music_pause(int sta)
    {
    char cmd[256];
    sprintf(cmd, "{ \"command\": [\"set_property\", \"pause\",%s] }\n", sta ? "true" : "false");
    //printf("%s\n", cmd);
    write(fd_mpv, cmd, strlen(cmd));
    }

    void music_set_pos(int music_id)
    {
    int read_flag = 0;
    char cmd[256];
    sprintf(cmd, "{ \"command\": [\"set_property\", \"playlist-pos\", %d] }\n",music_id);
    //printf("%s\n", cmd);
    write(fd_mpv, cmd, strlen(cmd));
    // set name
    lv_label_set_text(ui_LabelMusicPlayer, playing_music_node->filename);
    }

    void music_set_volume(int volume)
    {
    char cmd[256];
    sprintf(cmd, "{ \"command\": [\"set_property\", \"volume\", %d] }\n",volume);
    //printf("%s\n", cmd);
    write(fd_mpv, cmd, strlen(cmd));
    }

    void music_set_mode(int mode)
    {
    char cmd[256];
    sprintf(cmd, "{ \"command\": [\"set_property\", \"loop\",%s] }\n", mode ? "true" : "false");
    //printf("%s\n", cmd);
    write(fd_mpv, cmd, strlen(cmd));
    }

    代码步骤说明

    • music_pause(int sta) 使用 sprintf 构造一个 JSON 命令字符串,指示 mpv 暂停 ("true") 或恢复 ("false") 播放。
    • music_set_pos(int music_id) 使用 sprintf 构造一个 JSON 命令字符串,指定 "playlist-pos" 属性和目标 music_id
    • music_set_volume(int volume) 使用 sprintf 构造一个 JSON 命令字符串,指定 "volume" 属性和新的音量值。
    • music_set_mode(int mode)使用 sprintf 构造一个 JSON 命令字符串,指示 mpv 是否启用循环播放 ("true" 或 "false")。
  3. 音乐扫描列表,此函数主要是扫描指定目录下的所有 mp3 文件,将这些文件信息填写到链表中。

    int music_scan_list(char* mp3_string)
    {
    DIR *dir;
    struct dirent *entry;
    int id_num = 0;
    int i;

    init_music_node_list(&head);
    dir = opendir(MUSIC_DIR_PATH);
    if (dir == NULL) {
    perror("opendir error: ");
    lv_label_set_text(ui_LabelMusicList, "Music List(0)");
    return -1;
    }

    // Read files from music dir
    while ((entry = readdir(dir)) != NULL) {
    if (entry->d_type == DT_REG) { // common file
    // check .mp3
    char *ext = strrchr(entry->d_name, '.');
    if (ext != NULL && strcmp(ext, ".mp3") == 0) {
    // insert Music Node
    insert_music_node(&head, entry->d_name,id_num);
    id_num++;
    //add to mpv list
    char cmd[256];
    sprintf(cmd, "{ \"command\": [\"loadfile\", \"/music/%s\",\"append\"] }\n",entry->d_name);
    // printf("%s\n", cmd);
    write(fd_mpv, cmd, strlen(cmd));
    }
    }
    }
    closedir(dir);

    // Create roller str
    mp3_string[0] = '\0';
    if (head != NULL) {
    struct Music_Node* current = head;
    playing_music_node = head;
    for(i = 0;i < id_num;i++) {
    if(i != 0)
    strcat(mp3_string,"\n");
    strcat(mp3_string, current->filename);
    // printf("%s\n", current->filename);
    current = current->next;
    }
    }

    char MusicList[20];
    snprintf(MusicList,sizeof(MusicList),"Music List(%d)",id_num);
    lv_label_set_text(ui_LabelMusicList,MusicList);
    // printf("idnum:%d\n",id_num);

    return 0;
    }

代码步骤说明

  • 使用 init_music_node_list(&head); 初始化音乐节点链表的头指针。
  • 使用 opendir(MUSIC_DIR_PATH); 打开指定的音乐文件目录,若返回参数为空,则证明无此目录。
  • 循环调用 readdir(dir) 遍历目录中的文件,如果是 mp3 文件,则分配 id_num 并将文件信息插入到节点链表中,使用 sprintf 格式化命令,指示 mpv 加载该文件并将其追加到播放列表中。
  • 最后将获取到的 id_num 变量格式化到字符数组中,用于显示当前列表 mp3 文件个数。

5.4 返回主界面功能

本节介绍如何通过屏幕上滑返回主界面。

  1. ui_SrceenPlayer.c 中添加主界面返回事件。

    lv_obj_add_event_cb(ui_ScreenPlayer, ui_event_ScreenPlayer, LV_EVENT_ALL, NULL);
  2. ui.c 中实现事件回调函数。

    void ui_event_ScreenPlayer(lv_event_t * e)
    {
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_GESTURE && lv_indev_get_gesture_dir(lv_indev_get_act()) == LV_DIR_TOP && GestureFlag == true) {
    lv_indev_wait_release(lv_indev_get_act());
    _ui_screen_change(&ui_ScreenMain, LV_SCR_LOAD_ANIM_MOVE_TOP, 300, 0, &ui_ScreenMain_screen_init);
    }
    }

    代码步骤说明

    • 获取事件代码和目标对象:使用 lv_event_get_code(e) 获取事件类型,使用 lv_event_get_target(e) 获取触发事件的对象。
    • 手势处理:检查手势方向是否为上滑,同时检查全局标志 GestureFlag 是否为 true 。如果条件都满足,则执行以下操作:
      • 调用 lv_indev_wait_release(lv_indev_get_act()) 等待当前输入设备释放,确保在手势结束前不会触发其他事件。
      • 调用 _ui_screen_change(&ui_ScreenMain, LV_SCR_LOAD_ANIM_MOVE_TOP, 300, 0, &ui_ScreenMain_screen_init) 来切换到主屏幕,并使用从底部向上移动的动画效果。
    • 点击处理:如果事件代码是 LV_EVENT_CLICKED,则将 GestureFlag 设置为 true。这表示用户已经点击了屏幕,允许后续的手势识别。

6.编译运行

  1. 执行 make 指令,将生成可执行文件到 /build/bin 目录下。

    make
  2. 将可执行文件上传到 Luckfox Pico 上(可使用 adb ssh等方式),板端进入所上传的目录运行。

    # 在 Luckfox Pico 板端运行,<Demo Target> 是部署文件夹中的可执行程序
    chmod a+x <Demo Target>
    ./<Demo Target>