数据刷新

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 相关接口

接口名称

函数原型

说明

lv_obj_datasubs_notify

int lv_obj_datasubs_notify(const char *id, uint32_t type, const void *data, uint16_t len, void *user_data)

通知所有匹配 (id, type) 的订阅项;当 id == NULL 时,广播给所有 type 匹配的订阅项;支持跨线程调用

lv_obj_data_subscribe

int lv_obj_data_subscribe(lv_obj_t *obj, const char *id, uint32_t type, lv_obj_datasubs_cb_t cb, void *user_data)

为指定控件建立订阅;若相同 (obj, id, type) 已存在,则只更新回调和 user_data

lv_obj_data_unsubscribe

int lv_obj_data_unsubscribe(lv_obj_t *obj, const char *id, uint32_t type)

取消指定控件在指定 (id, type) 下的单个订阅

lv_obj_data_unsubscribe_all

int lv_obj_data_unsubscribe_all(const char *id)

取消某个 id 下的全部订阅

lv_obj_data_unbind

int lv_obj_data_unbind(lv_obj_t *obj)

取消指定控件绑定的全部订阅

接口返回值:成功通常返回 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);

参数名

类型

说明

obj

lv_obj_t *

当前收到通知的控件对象

type

uint32_t

事件类型,需与订阅和通知时使用的 type 保持一致

data

const void *

通知携带的数据指针

len

uint16_t

数据长度

user_data

void *

用户扩展参数;若 lv_obj_datasubs_notify() 传入非空值,则优先使用该值,否则回落到订阅时传入的 user_data

注意:

  • lv_obj_datasubs_notify 支持跨线程调用,其余接口应在 LVGL/UI 线程中调用;

  • 订阅匹配条件为 id + type

  • id 长度受 MAX_DATASUBS_NAME_LEN 限制,当前最大为 64 字节;

  • 回调中不建议直接新增或删除订阅项。

2.2.2 sensor数据刷新链路

以 sensor 数据为例,被动刷新链路如下:

  1. 底层驱动采集 sensor 原始数据,并上报至 GUI 线程消息队列;

  2. 执行 sensors_no_wakeup_msg_process_in_gui_thread_cb 回调,完成数据解析与预处理;

  3. 在回调中调用对应 sensor 类型的 notify 分发函数,例如 sensor_rt_data_step_notify

  4. notify 函数内部完成新旧值比对、无效值过滤,以及可选的单位转换或数据修正;

  5. 校验通过后,调用 app_rt_info_notify() 按字段发送 LV_EVENT_REFRESH 事件;

  6. 已订阅对应字段 key 的页面控件收到事件后执行刷新回调,完成 UI 更新。

完整实现代码参见 sensor_service_ui.c

2.2.3 sensor notify 的实现特点

  1. 按字段通知:以步数实时数据为例,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"。页面只需订阅自己关心的字段,可减少无效刷新。

  2. 仅在数值变化时通知:例如 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);
}
  1. 支持数据修正或单位转换:例如 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

SENSOR_APP_RT_STEP_INFO_IND

sensor_rt_data_step_notify

"rt_step""rt_step_distance""rt_step_calories"

SENSOR_APP_RT_HR_INFO_IND

sensor_rt_data_hr_notify

"rt_hr""rt_hr_timestamp"

SENSOR_APP_RT_SPO2_INFO_IND

sensor_rt_data_spo2_notify

"rt_spo2"

SENSOR_APP_RT_BP_INFO_IND

sensor_rt_data_bp_notify

"rt_bp_high""rt_bp_low"

SENSOR_APP_RT_TEMP_INFO_IND

sensor_rt_data_temp_notify

"rt_temp"

SENSOR_APP_RT_SPORT_INFO_ID

sensor_rt_data_sport_notify

"rt_sport_step""rt_sport_distance""rt_sport_calories"

SENSOR_APP_RT_GPS_INFO_ID

sensors_no_wakeup_msg_process_in_gui_thread_cb

"gps_info"

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 中订阅使用的 idtype 必须与通知侧一致;

  • on_resume 中主动读取一次,确保页面显示首帧就是最新值;

  • on_stop 中调用 lv_obj_data_unbind(step_label) 解除该对象的全部订阅;

  • 页面展示多个字段时,建议每个控件分别订阅对应 key。

3. 不同场景下的刷新策略

3.1 唤醒状态下如何刷新

系统处于唤醒状态时,主动刷新和被动刷新都可使用:

  • 固定周期变化的数据优先使用主动刷新;

  • 事件驱动型数据优先使用被动刷新;

  • 页面首次进入或恢复显示时,建议补一次主动读取。

3.2 睡眠状态下如何刷新

系统进入睡眠后,UI 线程通常会停止工作,主动刷新的定时器和被动刷新的订阅回调都不会继续执行;但 sensor 数据仍会写入 app_db。因此,系统唤醒后应主动读取一次最新数据并刷新页面。

4. 刷新模式选择建议

场景

推荐方式

说明

时间、倒计时、秒表

主动刷新

数据按固定频率变化,适合定时器轮询

心率、血氧、步数等 sensor 实时变化

被动刷新

数据由底层事件驱动,适合订阅通知

页面首次进入或从后台恢复

主动读取 + 被动订阅

先保证首帧正确,再接收后续通知

系统从睡眠唤醒

主动读取

睡眠期间 UI 不刷新,需恢复时补一次

通常建议以被动刷新作为实时数据更新的主要手段,以主动刷新作为页面初始化、恢复显示和固定周期场景的补充。