数据刷新¶
1. 概述¶
思澈 Solution 提供两类 UI 数据刷新方式:主动定时轮询和被动数据订阅。两种方式均基于 LVGL 和平台实时数据库(app_db),开发时只需按数据更新特性选择合适方案即可。
固定频率变化的数据:优先使用主动刷新;
事件驱动变化的数据:优先使用被动刷新;
页面首次显示或从休眠恢复时:建议补一次主动读取,确保首帧数据正确。
2. 数据刷新机制¶
主动刷新:通过定时器周期性读取数据并更新 UI;
被动刷新:数据变化时由底层通知页面刷新。
2.1 主动刷新:定时轮询更新¶
2.1.1 实现原理¶
主动刷新基于 LVGL 的定时器 lv_timer 实现,通过周期性轮询数据源获取最新数据并更新 UI,适用于数据更新频率固定、需要持续展示的场景,例如时间、倒计时等。
2.1.2 使用示例¶
下面示例创建一个文本控件用于显示步数,并通过定时器每秒刷新一次:
typedef struct
{
lv_obj_t *label;
lv_timer_t *refr_timer;
} app_step_t;
/* 页面私有数据 */
static app_step_t *p_step = NULL;
/* 定时器回调:刷新步数显示 */
static void refresh_timer_cb(lv_timer_t *timer)
{
step_info_t *step_info = (step_info_t *)app_rt_info_get(RT_STEP);
LV_UNUSED(timer);
if ((p_step == NULL) || (p_step->label == NULL) || (step_info == NULL))
{
return;
}
lv_label_set_text_fmt(p_step->label, "%u", step_info->step);
}
/* 页面启动时初始化 */
static void on_start(void)
{
p_step = app_malloc(sizeof(*p_step));
RT_ASSERT(p_step);
/* 创建文本控件 */
p_step->label = lv_label_create(lv_scr_act());
/* 设置字体和颜色 */
lv_ext_set_local_font(p_step->label, FONT_SMALL, lv_color_make(0xBE, 0xBE, 0xBE));
/* 创建定时器,每秒刷新一次 */
p_step->refr_timer = lv_timer_create(refresh_timer_cb, 1000, NULL);
/* 页面初次显示时先主动刷新一次 */
refresh_timer_cb(NULL);
}
/* 页面恢复时主动刷新 */
static void on_resume(void)
{
refresh_timer_cb(NULL);
}
static void on_pause(void)
{
}
/* 页面退出时释放资源 */
static void on_stop(void)
{
if ((p_step != NULL) && (p_step->refr_timer != NULL))
{
lv_timer_del(p_step->refr_timer);
}
app_free(p_step);
p_step = NULL;
}
2.1.3 使用建议¶
主动刷新适合固定周期变化或只能主动读取的数据,例如时间、倒计时和持续展示的步数等。
注意事项:
定时器周期不宜过短;
页面不可见或进入休眠后应停止定时器;
页面恢复时建议先主动刷新一次。
2.2 被动刷新:数据订阅更新¶
框架提供了完整的数据订阅机制,核心实现见 lv_obj_datasubs.c,接口声明见 lv_obj_datasubs.h。
2.2.1 相关接口¶
接口名称 |
函数原型 |
说明 |
|---|---|---|
|
|
通知所有匹配 |
|
|
为指定控件建立订阅;若相同 |
|
|
取消指定控件在指定 |
|
|
取消某个 |
|
|
取消指定控件绑定的全部订阅 |
接口返回值:成功通常返回 RT_EOK,未命中目标时通常返回 RT_ERROR。
回调类型 lv_obj_datasubs_cb_t 定义如下:
typedef void (*lv_obj_datasubs_cb_t)(struct _lv_obj_t *obj, uint32_t type,
const void *data, uint16_t len,
void *user_data);
参数名 |
类型 |
说明 |
|---|---|---|
|
|
当前收到通知的控件对象 |
|
|
事件类型,需与订阅和通知时使用的 |
|
|
通知携带的数据指针 |
|
|
数据长度 |
|
|
用户扩展参数;若 |
注意:
仅
lv_obj_datasubs_notify支持跨线程调用,其余接口应在 LVGL/UI 线程中调用;订阅匹配条件为
id + type;id长度受MAX_DATASUBS_NAME_LEN限制,当前最大为64字节;回调中不建议直接新增或删除订阅项。
2.2.2 sensor数据刷新链路¶
以 sensor 数据为例,被动刷新链路如下:
底层驱动采集 sensor 原始数据,并上报至 GUI 线程消息队列;
执行
sensors_no_wakeup_msg_process_in_gui_thread_cb回调,完成数据解析与预处理;在回调中调用对应 sensor 类型的 notify 分发函数,例如
sensor_rt_data_step_notify;notify 函数内部完成新旧值比对、无效值过滤,以及可选的单位转换或数据修正;
校验通过后,调用
app_rt_info_notify()按字段发送LV_EVENT_REFRESH事件;已订阅对应字段 key 的页面控件收到事件后执行刷新回调,完成 UI 更新。
完整实现代码参见 sensor_service_ui.c。
2.2.3 sensor notify 的实现特点¶
按字段通知:以步数实时数据为例,
sensor_rt_data_step_notify会分别通知"rt_step"、"rt_step_distance"、"rt_step_calories"、"rt_step_active_type"、"rt_step_freq"、"rt_step_length"和"rt_step_pace"。页面只需订阅自己关心的字段,可减少无效刷新。仅在数值变化时通知:例如
sensor_rt_data_hr_notify()中只有在新值与旧值不同且新值非0时才会触发通知:
if ((hr_db->hr != hr->hr) && (hr->hr != 0))
{
app_rt_info_notify("rt_hr", LV_EVENT_REFRESH, &hr->hr, sizeof(uint8_t), NULL);
}
支持数据修正或单位转换:例如
sensor_rt_data_sport_notify中,距离字段在开启BSP_BLE_SIBLES时会先经过data_unit_convert_distance换算;部分字段上报为0时,还会回填数据库旧值,避免无效值覆盖有效数据。
2.2.4 当前已实现的 sensor 实时 notify¶
基于 sensor_service_ui.c,当前主要实时通知如下:
消息 ID |
处理函数 |
典型 notify key |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.2.5 页面订阅sensor数据示例¶
下面示例演示页面如何订阅步数字段 key "rt_step",并在收到通知后刷新标签:
static void step_refresh_subs_cb(lv_obj_t *obj, uint32_t type, const void *data, uint16_t len, void *user_data)
{
const uint32_t *step = (const uint32_t *)data;
LV_UNUSED(type);
LV_UNUSED(len);
LV_UNUSED(user_data);
if ((obj == NULL) || (step == NULL))
{
return;
}
lv_label_set_text_fmt(obj, "步数:%lu", *step);
}
static lv_obj_t *step_label;
static void on_start(void)
{
step_label = lv_label_create(lv_scr_act());
lv_obj_data_subscribe(step_label, "rt_step", LV_EVENT_REFRESH, step_refresh_subs_cb, NULL);
}
static void on_resume(void)
{
step_info_t *step_info = (step_info_t *)app_rt_info_get(RT_STEP);
if ((step_label != NULL) && (step_info != NULL))
{
lv_label_set_text_fmt(step_label, "步数:%lu", step_info->step);
}
}
static void on_stop(void)
{
if (step_label != NULL)
{
lv_obj_data_unbind(step_label);
step_label = NULL;
}
}
说明:
on_start中订阅使用的id和type必须与通知侧一致;on_resume中主动读取一次,确保页面显示首帧就是最新值;on_stop中调用lv_obj_data_unbind(step_label)解除该对象的全部订阅;页面展示多个字段时,建议每个控件分别订阅对应 key。
3. 不同场景下的刷新策略¶
3.1 唤醒状态下如何刷新¶
系统处于唤醒状态时,主动刷新和被动刷新都可使用:
固定周期变化的数据优先使用主动刷新;
事件驱动型数据优先使用被动刷新;
页面首次进入或恢复显示时,建议补一次主动读取。
3.2 睡眠状态下如何刷新¶
系统进入睡眠后,UI 线程通常会停止工作,主动刷新的定时器和被动刷新的订阅回调都不会继续执行;但 sensor 数据仍会写入 app_db。因此,系统唤醒后应主动读取一次最新数据并刷新页面。
4. 刷新模式选择建议¶
场景 |
推荐方式 |
说明 |
|---|---|---|
时间、倒计时、秒表 |
主动刷新 |
数据按固定频率变化,适合定时器轮询 |
心率、血氧、步数等 sensor 实时变化 |
被动刷新 |
数据由底层事件驱动,适合订阅通知 |
页面首次进入或从后台恢复 |
主动读取 + 被动订阅 |
先保证首帧正确,再接收后续通知 |
系统从睡眠唤醒 |
主动读取 |
睡眠期间 UI 不刷新,需恢复时补一次 |
通常建议以被动刷新作为实时数据更新的主要手段,以主动刷新作为页面初始化、恢复显示和固定周期场景的补充。