OpenVela 之 UI 应用开发
在嵌入式开发领域,打造一个美观且功能完善的 UI 应用是提升用户体验的关键。本文将以 OpenVela 为例,详细介绍如何开发一个简单的音乐播放器 UI 应用,涵盖从环境搭建到代码实现、编译运行的全过程。
一、前提条件
在开始开发之前,需要完成以下准备工作:
- 搭建开发环境,具体步骤可参考 OpenVela 开发环境的前置安装教程。
- 下载 OpenVela 源码,可参照 OpenVela 快速入门 相关指引。
- 从 music_player 获取示例代码,该代码将作为本教程开发的基础。
二、前置概念
为了顺利完成开发任务,建议先了解以下基础知识和工具:
Makefile
:这是构建自动化工具 make 使用的配置文件,主要用于定义项目的编译规则和依赖管理,熟悉其基础概念与使用方式很有必要。Kconfig
:在 Linux 内核及嵌入式开发中常用的配置系统,能帮助开发者灵活地定义和选择软件配置选项,了解其基本原理和用法有助于项目配置。LVGL
:一款开源的嵌入式图形库,广泛用于开发高性能的用户界面,可参考 LVGL 官方文档学习其使用方法。
对这些概念的基本理解,能让开发过程更加高效。
三、项目简介
本文主要介绍如何在 OpenVela 中编写一个简单的音乐播放器(music_player
)。该音乐播放器具备播放、暂停、切换歌曲、显示歌曲信息、调节音量等基本功能,通过这个实例,能让开发者掌握 OpenVela 中 UI 应用开发的核心流程和方法。
四、项目结构
项目的代码和资源组织有序,便于管理和开发。以下是 music_player
项目的目录结构和文件组成说明。
1. 目录结构
项目的核心目录和文件结构如下:
packages/demos/music_player
├── res
│ ├── fonts
│ │ ├── MiSans-Normal.ttf
│ │ └── MiSans-Semibold.ttf
│ ├── icons
│ │ ├── album_picture.png
│ │ ├── audio.png
│ │ ├── music.png
│ │ ├── mute.png
│ │ ├── next.png
│ │ ├── nocover.png
│ │ ├── pause.png
│ │ ├── play.png
│ │ ├── playlist.png
│ │ └── previous.png
│ ├── musics
│ │ ├── manifest.json
│ │ ├── UnamedRhythm.png
│ │ └── UnamedRhythm.wav
│ └── config.json
├── audio_ctl.c
├── audio_ctl.h
├── Kconfig
├── Make.defs
├── Makefile
├── music_player.c
├── music_player.h
├── music_player_main.c
├── wifi.c
└── wifi.h
2. 文件组成
各目录与文件的作用如下:
-
res
:资源目录,包含项目运行所需的静态资源文件。fonts
:字体文件目录,存放应用使用的字体,如 MiSans-Normal.ttf、MiSans-Semibold.ttf。icons
:图标文件目录,包含界面显示所需的各种图标,像 album_picture.png、play.png 等。musics
:音乐资源目录,包含音频文件和相应的配置信息,有 manifest.json、UnamedRhythm.wav 等。config.json
:全局配置文件,存储项目的配置参数。
-
audio_ctl.c
/audio_ctl.h
:音频控制模块,负责实现音频相关功能,如音频输入、输出及音量调节等。 -
wifi.c
/wifi.h
:Wi-Fi 控制模块,实现 Wi-Fi 的连接管理、初始化等功能。 -
music_player.c
/music_player.h
:音乐播放器的核心逻辑,定义和实现音乐播放的主要功能。 -
music_player_main.c
:程序入口文件,负责初始化音乐播放器并启动主要运行逻辑。 -
Kconfig
、Make.defs
、Makefile
:构建系统文件。Kconfig
用于定义项目的配置信息和构建选项;Make.defs
包含编译相关的变量定义和依赖项规则;Makefile
定义项目的构建过程和依赖管理。
五、核心设计详解
音乐播放器的核心设计包括数据结构、UI 结构和业务逻辑,三者相互配合实现完整功能。
1. UI 结构设计
播放器的 UI 采用分组方式组织,层次清晰,便于维护。整体结构如下:
TIME GROUP:时间和日期显示区域
PLAYER GROUP:播放器核心区域├── ALBUM GROUP:专辑信息(封面、名称、艺术家)├── PROGRESS GROUP:播放进度(当前时间/总时长、进度条)└── CONTROL GROUP:控制按钮(上一首、播放/暂停、下一首等)
TOP Layer:顶层界面├── VOLUME BAR:音量控制条└── PLAYLIST GROUP:播放列表(包含歌曲列表及标题)
2. 数据结构设计
为了管理应用状态和资源,设计了三个核心数据结构:
- 应用配置(struct conf_s):存储环境参数(如 Wi-Fi 配置),敏感信息(如
ssid
、psk
)需通过安全方式加载(避免明文存储)。
struct conf_s {
#if WIFI_ENABLEDwifi_conf_t wifi;
#endif
};
- 如果启用 Wi-Fi 功能(
WIFI_ENABLED
宏定义),将允许配置 Wi-Fi 的ssid
和psk
。 - 避免在代码中硬编码
ssid
和psk
,确保配置敏感信息时引用外部加密存储或动态加载机制。
- 运行时状态(struct ctx_s):记录动态信息,包括当前播放状态(播放 / 暂停 / 停止)、音量、播放进度、音频控制句柄等。
// 唱片信息
typedef struct _album_info_t { const char* name; // 专辑名称 const char* artist; // 艺术家 char path[LV_FS_MAX_PATH_LENGTH]; // 音频文件路径 char cover[LV_FS_MAX_PATH_LENGTH]; // 专辑封面路径 uint64_t total_time; // 总时长(单位:毫秒) lv_color_t color; // 专辑主题颜色
} album_info_t; // 唱片状态切换
typedef enum _switch_album_mode_t { SWITCH_ALBUM_MODE_PREV, // 切换到上一张 SWITCH_ALBUM_MODE_NEXT, // 切换到下一张
} switch_album_mode_t; // 播放状态
typedef enum _play_status_t {PLAY_STATUS_STOP, // 播放停止 PLAY_STATUS_PLAY, // 正在播放 PLAY_STATUS_PAUSE, // 暂停播放
} play_status_t;// 播放器运行时的状态信息
struct ctx_s { bool resource_healthy_check; // 系统资源检查 album_info_t* current_album; // 当前播放的专辑信息 lv_obj_t* current_album_related_obj; // 关联到专辑的 UI 对象 uint16_t volume; // 当前音量 play_status_t play_status_prev; // 上一次播放状态 play_status_t play_status; // 当前播放状态 uint64_t current_time; // 当前播放时长 struct { lv_timer_t* volume_bar_countdown; // 音量条自动隐藏计时器 lv_timer_t* playback_progress_update; // 播放进度更新计时器 } timers; audioctl_s* audioctl; // 音频控制句柄,用于音频操作
};
- 资源组件(struct resource_s):管理所有
ui
控件、fonts
字体、styles
样式和images
图片资源,统一维护界面元素和数据关联。
struct resource_s {struct {lv_obj_t* time; // 时间显示lv_obj_t* date; // 日期显示lv_obj_t* player_group; // 播放器容器 lv_obj_t* volume_bar; // 音量条 lv_obj_t* volume_bar_indic; // 音量指示器 lv_obj_t* audio; // 音频对象 lv_obj_t* playlist_base; // 播放列表基础区域 lv_obj_t* album_cover; // 专辑封面 lv_obj_t* album_name; // 专辑名称 lv_obj_t* album_artist; // 艺术家名称 lv_obj_t* play_btn; // 播放键 lv_obj_t* playback_group; // 播放进度容器 lv_obj_t* playback_progress; // 播放进度条 lv_span_t* playback_current_time; // 当前播放时间 lv_span_t* playback_total_time; // 总时长 lv_obj_t* playlist; // 播放列表对象 } ui; struct { struct { lv_font_t* normal; } size_16; struct { lv_font_t* bold; } size_22; struct { lv_font_t* normal; } size_24; struct { lv_font_t* normal; } size_28; struct { lv_font_t* bold; } size_60; } fonts; struct { lv_style_t button_default; // 按钮默认样式 lv_style_t button_pressed; // 按钮按下样式 lv_style_transition_dsc_t button_transition_dsc; // 按钮过渡效果 lv_style_transition_dsc_t transition_dsc; // 通用过渡效果 } styles; struct { const char* playlist; // 播放列表图标路径 const char* previous; // 上一首图标路径 const char* play; // 播放图标路径 const char* pause; // 暂停图标路径 const char* next; // 下一首图标路径 const char* audio; // 音频图标路径 const char* mute; // 静音图标路径 const char* music; // 音乐图标路径 const char* nocover; // 无封面占位图标路径 } images; album_info_t* albums; // 所有专辑信息 uint8_t album_count; // 专辑数量
};
3. 业务逻辑设计
主启动流程
应用启动过程由 app_create
函数主导,步骤如下:
- 初始化资源和运行时状态结构体。
- 读取配置文件(如 Wi-Fi 信息、音乐列表)。
- 检查资源完整性(字体、图标、音频文件)。
- 创建主界面和顶层控件(如音量条、播放列表)。
- 初始化播放状态(默认停止),加载第一个专辑并设置默认音量。
- 启动后台任务(如时间更新、播放进度刷新)。
播放状态机
播放状态的切换是核心逻辑,由 app_refresh_play_status
函数实现,根据状态(停止 / 播放 / 暂停)更新 UI 和音频控制:
- 停止状态:显示「播放」图标,暂停进度计时器,释放音频资源。
- 播放状态:显示「暂停」图标,启动进度计时器,初始化并启动音频播放。
- 暂停状态:显示「播放」图标,暂停进度计时器,暂停音频播放。
static void app_refresh_play_status(void){if (C.timers.playback_progress_update == NULL) {C.timers.playback_progress_update = lv_timer_create(app_playback_progress_update_timer_cb, 1000, NULL);}switch (C.play_status) { case PLAY_STATUS_STOP: // 停止播放状态处理 lv_image_set_src(R.ui.play_btn, R.images.play); // 更新播放按钮图标为“播放” lv_timer_pause(C.timers.playback_progress_update); // 暂停计时器 if (C.audioctl) { audio_ctl_stop(C.audioctl); // 停止音频播放 audio_ctl_uninit_nxaudio(C.audioctl); // 释放音频控制器资源 C.audioctl = NULL; // 清空音频控制器句柄 } break; case PLAY_STATUS_PLAY: // 播放状态处理 lv_image_set_src(R.ui.play_btn, R.images.pause); // 更新播放按钮图标为“暂停” lv_timer_resume(C.timers.playback_progress_update); // 恢复计时器 if (C.play_status_prev == PLAY_STATUS_PAUSE) { audio_ctl_resume(C.audioctl); // 恢复音频播放 } else if (C.play_status_prev == PLAY_STATUS_STOP) { C.audioctl = audio_ctl_init_nxaudio(C.current_album->path); // 初始化音频控制器 audio_ctl_start(C.audioctl); // 开始播放音频 } break; case PLAY_STATUS_PAUSE: // 暂停播放状态处理 lv_image_set_src(R.ui.play_btn, R.images.play); // 更新播放按钮图标为“播放” lv_timer_pause(C.timers.playback_progress_update); // 暂停计时器 audio_ctl_pause(C.audioctl); // 暂停音频播放 break; default: break; }
}
六、接口设计
1. 初始化函数
初始化函数负责在应用启动时执行资源配置、界面创建和配置文件加载等任务。以下是主要函数接口:
/* Init functions */
static void read_configs(void);
static bool init_resource(void);
static void reload_music_config(void);
static void app_create_error_page(void);
static void app_create_main_page(void);
static void app_create_top_layer(void);
2. 定时器启动函数
定时器控制任务用于启动后台进程,支持动态更新界面功能,例如时间显示、播放进度更新。
/* Timer starting functions */
static void app_start_updating_date_time(void);
3. 专辑操作接口
专辑操作是音乐播放器的核心功能,支持专辑排序、切换和播放相关处理。
/* Album operations */
static int32_t app_get_album_index(album_info_t* album);
static void app_switch_to_album(int index);
4. 播放器状态接口
播放状态接口用于设置播放器的运行状态,如播放、暂停、改变音量或播放时间等。以下提供的接口实现了这些功能:
/* Album operations */
static void app_set_play_status(play_status_t status);
static void app_set_playback_time(uint32_t current_time);
static void app_set_volume(uint16_t volume);
5. UI 刷新功能接口
UI 刷新接口负责动态更新界面组件,如专辑信息、播放状态、音量条和播放进度的实时显示。
/* UI refresh functions */
static void app_refresh_album_info(void);
static void app_refresh_date_time(void);
static void app_refresh_play_status(void);
static void app_refresh_playback_progress(void);
static void app_refresh_playlist(void);
static void app_refresh_volume_bar(void);
static void app_refresh_volume_countdown_timer(void);
6. 事件处理接口
事件处理是用户交互的重要组成部分,负责对按钮、播放列表、音量条等的事件进行处理:
/* Event handler functions */
static void app_audio_event_handler(lv_event_t* e);
static void app_play_status_event_handler(lv_event_t* e);
static void app_playlist_btn_event_handler(lv_event_t* e);
static void app_playlist_event_handler(lv_event_t* e);
static void app_switch_album_event_handler(lv_event_t* e);
static void app_volume_bar_event_handler(lv_event_t* e);
static void app_playback_progress_bar_event_handler(lv_event_t* e);
7. 定时器回调函数接口
定时器相关的回调函数用于在固定时间间隔内触发任务
/* Timer callback functions */
static void app_refresh_date_time_timer_cb(lv_timer_t* timer);
static void app_playback_progress_update_timer_cb(lv_timer_t* timer);
static void app_volume_bar_countdown_timer_cb(lv_timer_t* timer);
七、编写项目配置文件
- 配置编译系统配置文件目的是针对目录下的所有源代码,将其编译成可执行产物。
- 增加了新的应用程序,对应的应用程序需要有新的配置项来来决定是否启用应用程序、分配多少栈、进程执行额优先级以及应用的名字等信息。
- 为了新增音乐播放器,需要更新编译系统的配置文件,包括 Kconfig、Makefile 和 Make.defs 文件。
Kconfig 文件
以下为新增应用项目的 Kconfig 文件,用于启用功能及定义音乐播放器数据路径:
config LVX_USE_DEMO_MUSIC_PLAYERbool "Music Player"default nif LVX_USE_DEMO_MUSIC_PLAYERconfig LVX_MUSIC_PLAYER_DATA_ROOTstring "Music Player Data Root"default "/sdcard"
endif
Makefile 文件
Makefile
控制应用的编译规则及资源。
include $(APPDIR)/Make.defsifeq ($(CONFIG_LVX_USE_DEMO_MUSIC_PLAYER), y)
PROGNAME = music_player
PRIORITY = 100
STACKSIZE = 32768
MODULE = $(CONFIG_LVX_USE_DEMO_MUSIC_PLAYER)CSRCS = music_player.c audio_ctl.c wifi.c
MAINSRC = music_player_main.c
endifinclude $(APPDIR)/Application.mk
Make.defs 文件
Make.defs
文件将新增的音乐播放器模块加入到系统构建。
ifneq ($(CONFIG_LVX_USE_DEMO_MUSIC_PLAYER),)
CONFIGURED_APPS += $(APPDIR)/packages/demos/music_player
endif
八、编译运行
1. 配置项目
切换到 openvela 仓库的根目录,执行如下命令来配置音乐播放器。 模拟器配置文件(defconfig)在
vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap/ 目录下,使用
build.sh 配置和编译开发板的代码。vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap menuconfig
- build.sh:编译脚本,用来配置和编译 openvela 代码
- vendor/openvela/boards/vela/configs/*:配置路径
- menuconfig:打开 menuconfig 页面,修改项目代码的配置。
执行后出现如下界面:
按下 / 键逐个搜索修改如下配置:
LVX_USE_DEMO_MUSIC_PLAYER=y
LVX_MUSIC_PLAYER_DATA_ROOT="/data"
以LVX_USE_DEMO_MUSIC_PLAYER为例进行操作,其余配置方式相同。
- 输入待搜索的配置 LVX_USE_DEMO_MUSIC_PLAYER,支持模糊搜索,例如 music_player,找到对应的配置,按回车键进入该配置。
- 按下空格键,[ ] 中出现 * 表示打开该配置。
- 将 LVX_MUSIC_PLAYER_DATA_ROOT 设置为 /data,修改后按下回车键保存当前配置项。
- 按下 Q 键,弹出如下退出保存界面。
- 按下字母Y 键保存配置,退出修改配置页面。
2. 编译项目
- 切换到 openvela 仓库的根目录,在终端内依次执行如下命令:
# 清理构建产物
./build.sh vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap distclean -j8# 开始构建
./build.sh vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap -j8
- 成功执行后,将得到以下文件:
./nuttx
├── vela_ap.elf
├── vela_ap.bin
3. 启动模拟器并推送资源
- 切换到 openvela 仓库的根目录,启动模拟器:
./emulator.sh vela
- 使用模拟器支持的 ADB 将资源推送到设备,在 openvela 仓库的根目录下打开一个新的终端,输入 adb push 后跟文件路径,即可将资源传输到相应位置。
# 安装adb
sudo apt install android-tools-adb# 推送资源
adb push apps/packages/demos/music_player/res /data/
4. 启动音乐播放器
在模拟器的终端环境 openvela-ap> 中输入如下命令:
music_player &
5. 退出 Demo
关闭模拟器退出 Demo