动态应用指南
动态应用介绍
动态应用基于 RT-Thread 动态模块加载功能,以动态库形式在设备运行时加载,文件通过文件系统存储。支持三类应用:动态表盘(wf)、动态应用(app)、动态息屏显示(aod)。
预置类型: 分为内置(出厂预置,不可删除)和外置(可通过手机 APP 或串口推送 / 删除,灵活扩展)。
PC调试: 支持PC模拟器调试(模拟器中与主代码一起编译,非动态加载模式),可以通过模拟器调试验证代码逻辑以及显示效果。
目录结构
动态 app、wf、aod 的目录结构一致,均包含资源文件(图标、素材等)和代码(业务逻辑),具体结构可参考图示。
注意:resource 资源目录规则参考图片资源的位置
hcpu配置
动态应用需通过宏定义配置,具体配置项(如动态 app/wf/aod 的使能宏)可参考图示。
数据存储
动态应用的数据通过以下接口读写,数据与应用绑定,删除应用时框架自动清理关联数据:
读取
size_t app_nvm_read(const char* key_name, const void* data, size_t length);
存储
rt_err_t app_nvm_write(const char* key_name, const void* data, size_t length);
删除
rt_err_t app_nvm_del(const char* key_name);
注意:key_name必须和动态应用的id保持一致(app/aod/wf),数据删除是由框架处理。
Butterfli编译依赖
Solution 通过 Butterfli 工具进行编译,工具会根据当前板级工程 HCPU 的.config配置,自动判断是否装入动态应用。判断逻辑如下:
若动态应用根目录存在readme.ini文件,工具会读取文件中定义的分辨率和依赖宏,与.config中的配置比对,匹配则装入
若readme.ini文件不存在,工具默认该动态应用与当前配置兼容,直接装入
readme.ini 的核心配置项说明如下:
配置项 |
说明 |
---|---|
RESOLUTION_SET |
定义支持的分辨率(如1024x600),RES_NUM=0表示支持所有分辨率 |
MACRO_DEPEND |
通用依赖宏(板级和模拟器共享),如APP_DLMODULE_APP_USED。 |
BOARD_MACRO_DEPEND |
板级依专用赖宏(如硬件相关宏USING_EZIPA_DEC),无依赖可省略 |
SIMU_MACRO_DEPEND |
模拟器专用依赖宏,无依赖,则设置MACRO_NUM=0 |
例1:
骰子游戏的根目录为_dynamic_app\c\app\game_dice,目录结构如下图
readme.ini说明如下图
例2:
[RESOLUTION_SET]
RES_NUM=5 #支持分辨率的数量,后续的以RES_0开始依次命名,如果RES_NUM是0,表示支持所有分辨率
RES_0=1024x600
RES_1=390x450
RES_2=480x480
RES_3=454x454
RES_4=800x480
[BOARD_MACRO_DEPEND]
MACRO_NUM=9 #支持宏依赖的数量,后续的以MACRO_0开始依次命名
MACRO_0=PKG_USING_WEBCLIENT
MACRO_1=PKG_LIB_OPUS
MACRO_2=RT_USING_LWIP212
MACRO_3=RT_LWIP_DNS
MACRO_4=BT_FINSH_PAN
MACRO_5=RT_LWIP_USING_WEBSOCKET
MACRO_6=PKG_USING_MBEDTLS_USE_ALL_CERTS
MACRO_7=PKG_USING_MBEDTLS_USER_CERTS
MACRO_8=RT_LWIP_REASSEMBLY_FRAG
[MACRO_DEPEND]
MACRO_NUM=2
MACRO_0=APP_DLMODULE_APP_USED
MACRO_1=XIAOZHI_SUPPORT
[SIMU_MACRO_DEPEND]
MACRO_NUM=0
动态app编写
申明类型
在代码首行定义#define DYN_APP,接着"#include “app_module.h”。
#define _MODULE_NAME_ "dyn_plane" // 与app ID一致
#include "app_module.h"
注意: _MODULE_NAME_在文件系统分目录方案中,将作为该应用访问资源的子目录路径。参见app_module.h
#if defined (MODULE_BUILT_IN)
#define MODULE_NAME_PATH BUILT_IN_APP_PATH MODULE_SUB_PATH _MODULE_NAME_ "/"
#else
#define MODULE_NAME_PATH APP_DYNAMIC_APP_PATH MODULE_SUB_PATH _MODULE_NAME_ "/"
#endif
#define GET_IMG_EZIP MODULE_NAME_PATH "ezip/" _MODULE_NAME_ "_"
#define GET_IMG_GIF MODULE_NAME_PATH "gif/" _MODULE_NAME_ "_"
#define GET_IMG_H264 MODULE_NAME_PATH "H264/" _MODULE_NAME_ "_"
#define GET_IMG_JPG MODULE_NAME_PATH "jpg/" _MODULE_NAME_ "_"
注册应用
使用APPLICATION_REGISTER宏注册,参数如下:
APPLICATION_REGISTER(key_str, img, app_name, ptr_size)
key_str : 应用显示标题的多语言字符串id
img : 应用缩略图名称, 动态应用中时此参数无用
app_name : 应用名字,字符串,框架调度该应用时使用,需要保证唯一性
ptr_size : 应用全局内存大小,该内存由框架申请释放,页面可以直接使用
子页面创建
通过gui_app_create_page_for_app_ext接口显式创建
int gui_app_create_page_for_app_ext(const char *app_id, const char *pg_id,gui_page_msg_cb_t handler, void *user_data, uint32_t mem_size)
多风格图标
动态应用支持多风格图标,图标名和放置目录需要遵循如下:
默认图标是放置在thumbnails下的名为"tn.png"的图标。
其他风格图标是放置images目录下的"tnx.png"的图标,x为style的值,如支持style 2的风格,则是"tn2.png"。
提供如下接口获取图标:
const char* dynamic_app_get_icon(const char* app_name, uint8_t style);
注意: style为1时,使用的thumbnails下的名为"tn.png"的图标,其他值则是images目录下的"tnx.png"图标,如果"tnx.png"不存在,会使用默认的"tn.png"
初始化和反初始化
初始化(模块首次运行时调用):DLMODULE_INIT_DEF(init_func)。注;即使多次打开module,也只会调用一次
反初始化(应用删除时调用):DLMODULE_DEINIT_DEF(cleanup_func)。如播放本地音乐时,动态音乐app删除时需要在反初始化时停止本地音乐播放。
初始化注册:
//init_func: module_init_func_t类型
DLMODULE_INIT_DEF(init_func)
反初始化注册:
//cleanup_func: module_cleanup_func_t 类型
DLMODULE_DEINIT_DEF(cleanup_func)
初始化和反初始化接口在需要时注册,一个app只能注册和反注册一次。
删除接口
动态应用删除包括节点删除和资源删除。先删除节点,再删除资源
/*删除节点*/
rt_err_t dynamic_app_list_del_app(const char* id);
/*删除动态应用code、资源*/
int32_t dynamic_app_del_app(const char* id);
主app示例
#define DYN_APP
#include "global.h"
/*_MODULE_NAME_需要和动态应用id保持一致,用以模拟器调试使用 */
#define _MODULE_NAME_ "dyn_plane"
#include "app_module.h"
static plane_play_t *p_lane_play = NULL;
static void on_start(void)
{
/*从框架获取申请的内存,必须要注册的时候填入需要的内存大小*/
p_lane_play = APP_GET_PAGE_MEM_PTR;
RT_ASSERT(p_lane_play);
/*do something*/
}
static void on_resume(void)
{
/*do something*/
}
static void on_pause(void)
{
/*do something*/
}
static void on_stop(void)
{
/*do something*/
/*指针置空,内存由框架释放*/
p_lane_play = NULL;
}
APPLICATION_REGISTER(app_get_strid(key_plane, "Plane"), NULL, "dyn_plane", sizeof(plane_play_t))
弹窗创建
动态app支持弹窗功能,使用注册宏’POPUP_REGISTER_EXT(id, priopoty, lifetime, mem_size, val)'注册。每个app最大支持8个弹窗(val 0 ~ 7)。
弹窗示例
#define DYN_APP
#include "global.h"
#include "popup_fwk.h"
/*_MODULE_NAME_需要和动态应用id保持一致,用以模拟器调试使用 */
#define _MODULE_NAME_ "dyn_plane"
#include "app_module.h"
static plane_popup_t *p_lane_pop = NULL;
static void on_start(void* param)
{
p_lane_pop = POPUP_GET_NODE_MEM_PTR;
RT_ASSERT(p_plane_pop);
}
static void on_refr(void* param)
{
/*do something*/
}
static void on_stop(void* param)
{
/*do something*/
/*指针置空,内存由框架释放*/
p_plane_pop = NULL;
}
POPUP_REGISTER_EXT("dyn_plane", 5, 3000, sizeof(plane_popup_t), 0);
动态wf编写
申明wf类型
C代码需要定义DYN_WF,需要放置在include"app_modul.h"前,建议放到C代码第一行,对于wf注册接口和用到资源(包含了app_modul.h)的C文件,都需要定义DYN_WF。
头文件和_MODULE_NAME_定义
应用注册的C文件必须要先定义’_MODULE_NAME_', '_MODULE_NAME_'的值就是动态wf的id,然后再include"app_modul.h"。
wf注册接口
动态wf注册和内置wf注册接口相同,使用’WF_REGISTER(priority, id, name, thumb_img, ptr_size)'。
WF_REGISTER(priority, id, name, thumb_img, ptr_size)
priority : 表盘优先级,表盘在选择时按照优先级从高(数值小)到低排列,内置表盘的顺序可以动态调整,修改管理链表(”wf_list_get()“)中的优先级即可。
id : 表盘名字,字符串,框架调度该应用时使用,需要保证唯一性
name : 表盘显示标题,多语言字符串id,仅作显示用
thumb_img : 表盘缩略图,编辑时使用
ptr_size : 表盘全局内存大小,该内存由框架申请释放,页面可以直接使用,通过‘WF_GET_NODE_MEM_PTR’获取
删除接口
动态wf删除包括节点删除和资源删除两步,先删除节点,再删除资源
/*删除节点*/
rt_err_t dynamic_wf_del_node(const char* id);
/*删除动态应用code、资源*/
int32_t dynamic_wf_del_app(const char* id);
动态wf示例
#define DYN_WF
#include "global.h"
/*_MODULE_NAME_需要和动态wf id保持一致,用以模拟器调试使用 */
#define _MODULE_NAME_ "wf_simple"
#include "app_module.h"
static clock_simple_t *p_clk_simple = NULL;
static rt_int32_t on_init(void *param)
{
/*从框架获取申请的内存,必须要注册的时候填入需要的内存大小*/
p_clk_simple = WF_GET_NODE_MEM_PTR;
RT_ASSERT(p_clk_simple);
/*do something*/
return RT_EOK;
}
static rt_int32_t on_resume(void *param)
{
/*do something*/
return RT_EOK;
}
static rt_int32_t on_pause(void *param)
{
/*do something*/
return RT_EOK;
}
static rt_int32_t on_stop(void *param)
{
/*do something*/
/*指针置空,内存由框架释放*/
p_lane_play = NULL;
return RT_EOK;
}
WF_REGISTER(3, "wf_simple", app_get_strid(key_clk_simple, "Simple"), tn, sizeof(clock_simple_t));
动态aod编写
申明aod类型
C代码需要定义DYN_AOD,需要放置在include"app_modul.h"前,建议放到C代码第一行,对于wf注册接口和用到资源(包含了app_modul.h)的C文件,都需要定义DYN_WF。
头文件和_MODULE_NAME_定义
应用注册的C文件必须要先定义’_MODULE_NAME_', '_MODULE_NAME_'的值就是动态aod的id,然后再include"app_modul.h"。
aod注册接口
动态wf注册和内置aod注册接口相同,使用’AOD_REGISTER(priority, id, key_str, thumb_img)'。
AOD_REGISTER(priority, id, key_str, thumb_img)
priority : AOD优先级, 未使用,预留
id : AOD名字,字符串,框架调度该应用时使用,需要保证唯一性
key_str : AOD显示标题,多语言字符串id
thumb_img : AOD缩略图, 动态aod中无用,框架自动使用tn.png
删除接口
动态wf删除包括节点删除和资源删除两步,先删除节点,再删除资源
/*删除节点*/
rt_err_t dynamic_aod_del_node(const char* id);
/*删除动态应用code、资源*/
int32_t dynamic_aod_del_app(const char* id);
动态aod示例
#define DYN_AOD
#include "global.h"
#define _MODULE_NAME_ "suntrack"
#include "app_module.h"
static aod_suntrack_t *p_aod_suntrack = NULL;
static void on_start(void)
{
p_aod_suntrack = app_calloc(1, sizeof(aod_suntrack_t));
RT_ASSERT(p_aod_suntrack);
/*do something*/
}
static void on_resume(void)
{
/*do something*/
}
static void on_pause(void)
{
/*do something*/
}
static void on_stop(void)
{
/*do something*/
if(p_aod_suntrack)
{
app_free(p_aod_suntrack);
p_aod_suntrack = NULL;
}
}
AOD_REGISTER(0, "suntrack", app_get_strid(key_aod_suntrack, "Aod_suntrack"), NULL)
注意事项
多语言问题
动态应用的多语言是独立的,目前主代码和动态应用不能相互访问多语言,不能用app_get_str_from_id去获取对方多语言信息。
注册ID
动态应用的注册ID必须和动态应用目录,‘_MODULE_NAME_’(模拟器或者内置编译使用)宏定义相同。
图标名称
动态app/aod/wf图标(缩略图)必须固定为tn.png。
接口限制
由于目前HCPU主代码和动态应用代码编译工具链不一致,少量接口/宏有限制,已知如下:
dfs_file_stat
S_ISDIR()
INIT_APP_EXPORT
INIT_PRE_APP_EXPORT
INIT_ENV_EXPORT
INIT_COMPONENT_EXPORT
INIT_DEVICE_EXPORT
INIT_PREV_EXPORT
INIT_BOARD_EXPORT
线程问题
由于动态应用退出时会关闭module,释放所有资源,其他线程不知道动态应用关闭,所以不建议在其他线程使用动态应用的函数,变量等资源。如天气应用会联网获取天气信息,获取的处理会比较耗时,一般会将获取天气的处理放到其他线程里操作,需要注意获取成功的回调里的数据存储或者接口调用都不能用动态天气的接口,只能将这些操作放到主代码里,天气应用通过数据订阅的方式获取主代码的天气数据或者轮询获取天气存储数据。
调试建议
由于动态应用调试相较于内置应用(和code编译到一起)相对麻烦,所以建议开发一个动态应用的流程是:内置方式模拟器调试UI–>内置方式调试板级相关–>内置改动态—>调试验证完成。
UI相关
对于UI部分,建议优先在模拟器上调试,待模拟器调试完成后再到板级调试。
板级相关
UI部分调试完成后,对于依赖板级相关,按照内置应用方式调试板级
内置改动态
内置板级调试完成后需要改成外置动态,参考上面动态app编写流程。
动态改内置
有时候需要将动态应用改为内置应用,一般需要修改以下内容:
移除或者注释所有源文件.c和.h中DYN_APP宏,这通常是放到文件的头位置,并将src下的代码放置到内置代码application目录中,同时要增加Sconscript脚本连接编译。
将动态应用resource目录thumbnails以及images目录下的资源放到内置资源的images/xxx/目录下,在注册函数APPLICATION_REGISTER中需要对应修改资源名称。
将resource目录下的其他资源以合适的方式放到内置工程的资源目录中
死机分析
动态应用发生死机时,使用Trace32恢复现场分析时需要额外加载动态应用的符号表才能看到完整的调用栈。butterfli编译动态应用完成后,会在文件系统路径中生成一个zip_output_symbolbak目录,里面的动态应用内会带有xxx.so.nostrip的文件(xxx为动态应用名),恢复现场时需要用到此符号表来恢复现场。具体操作如下:
获取module地址
1.1) 模块地址可由串口日志,搜素"open module"关键字,最后一个关键字后的就是module地址。
1.2) 固件中保存了最后打开的模块地址,也是可以用来debug。
cur_app_module 最后打开的动态应用。
cur_wf_module 最后打开的动态表盘。
cur_aod_module 最后打开的动态AOD。获取entry函数地址 将module转换成(struct rt_dlmodule *)类型,内部的entry_addr就是函数的地址
加载符号表 使用“D.Load wf_simple.so.nostrip entry_addr /nocode /noclear”加载动态符号表
如在下图代码的210行往0地址赋值,当执行到这里会触发hardfault
使用脚本恢复得到的现场如下图,只能看到触发异常的指令地址是0x60373996,但并不知道在表盘代码的哪个函数里出错,也无法和C代码对应起来
由于动态加载的代码的执行地址经过了重定位,因此恢复现场的时候需要先加载符号表进行地址重定位。以上图为例,
通过(struct rt_dlmodule *) cur_app_module,就可以找到结构变量成员entry_addr的值为0x6036dc38
通过该地址,使用命令“D.Load sport.so.nostrip 0x6036dc38 /nocode /noclear”加载动态模块的符号表 通过以上两个步骤,就可以看到下图中完整的调用栈,从而看到触发死机的断言是在on_start函数里。
死机dump