LVGL 消息机制(lv_msg)实战指南:发布/订阅模式在 X-TRACK 模拟器中的应用
发布时间:2026/10/4 14:12:05 锦皓数字建站
实战指南:发布/订阅模式在 X-TRACK 模拟器中的应用`)
智能硬件嵌入式硬件开发【免费下载链接】X-TRACKA GPS bicycle speedometer that supports offline maps and track recording项目地址https://gitcode.com/gh_mirrors/xt/X-TRACK点击查看免费下载导读lv_msg是 LVGL 提供的一套经典的发布/订阅Publisher-Subscriber消息机制它允许不同模块、不同控件之间通过消息 ID解耦通信而无需互相持有对方的引用。本指南以 X-TRACK 仓库自带的 LVGL 源码Software/X-Track/Simulator/LVGL.Simulator/lvgl为基准完整讲解消息 ID 的设计、消息发送、订阅回调、对象订阅以及退订等全部 API 用法并深入源码分析其底层实现与官方示例帮助读者在嵌入式 UI 或多页面应用中写出低耦合、易维护的模块间通信代码。说明本仓库内置的 LVGL 版本源码位于Software/X-Track/Simulator/LVGL.Simulator/lvgllv_msg模块实现在src/extra/others/msg/lv_msg.c与lv_msg.hX-Track 模拟器工程配置位于 Software/X-Track/Simulator/LVGL.Simulator/lv_conf.h其中第 731 行定义了#define LV_USE_MSG 0默认关闭可按需开启。一、lv_msg 是什么经典发布/订阅模式Messaginglv_msg是 LVGL 对经典 发布/订阅模式Publish–subscribe pattern的完整实现。其核心思想是发布者Publisher只负责发出消息它不需要知道有多少订阅者、订阅者是谁订阅者Subscriber只负责监听消息它不需要知道消息从哪来、由谁发出双方唯一的耦合点是消息 IDMessage ID和载荷payload的数据格式约定。这种模式特别适合 LVGL 这类控件繁多、页面切换频繁的 UI 场景多个控件之间同步状态如滑块值实时显示到标签上、模块之间解耦通信如登录状态变更通知多个界面元素时无需建立全局变量或层层传递回调。从源码结构看lv_msg模块在src/extra/others/msg/目录下其公开头文件 lv_msg.h 完整声明了全部 API并以#if LV_USE_MSG作为模块开关——只有开启该宏后相关代码才会被编译进固件。二、消息 ID发布者与订阅者的共同约定在使用lv_msg之前发布者和订阅者双方必须先约定好消息 ID。在lv_msg中消息 ID 就是简单的uint32_t整数。例如#define MSG_DOOR_OPENED 1 #define MSG_DOOR_CLOSED 2 #define MSG_USER_NAME_CHANGED 100 #define MSG_USER_AVATAR_CHANGED 101消息 ID 的编排完全由开发者自行决定LVGL 不做任何限制源码中lv_msg_send仅以uint32_t msg_id参数接收内部不做 ID 范围校验。除了消息 ID双方还必须约定载荷payload的数据格式。以上面的例子来说MSG_DOOR_OPENED和MSG_DOOR_CLOSED没有载荷发送时传NULL即可MSG_USER_NAME_CHANGED的载荷可以是一个const char *携带新的用户名MSG_USER_AVATAR_CHANGED的载荷可以是一个const void *指向新的头像图片资源。从实现上看消息载荷在 lv_msg.h 中被定义为const void * payloadPointer to the data of the message即发送方只需传一个指针LVGL 不会拷贝数据也不会替调用方管理生命周期——载荷指向的数据在回调执行期间必须保持有效。三、发送消息lv_msg_send消息通过lv_msg_send(msg_id, payload)发送例如lv_msg_send(MSG_USER_DOOR_OPENED, NULL); /*无载荷消息*/ lv_msg_send(MSG_USER_NAME_CHANGED, John Smith); /*带字符串载荷*/函数原型为void lv_msg_send(uint32_t msg_id, const void * payload)。结合 lv_msg.c 的实现可以看到其内部流程在栈上构造一个lv_msg_t消息对象lv_memset_00清零填入id msg_id、payload payload调用静态函数notify(m)遍历订阅链表。因此lv_msg_send是一个同步调用发送消息后会立即按顺序通知当前所有匹配的订阅者而不是投递到队列异步处理。这一点在实时性要求高的嵌入式场景中通常是有利的通知立刻生效但也意味着回调中不应做耗时操作。四、订阅消息lv_msg_subscribe 与订阅回调使用lv_msg_subscribe(msg_id, callback, user_data)订阅指定 ID 的消息void * s1; s1 lv_msg_subscribe(MSG_USER_DOOR_OPENED, some_callback, NULL);回调函数的签名如下static void user_name_subscriber_cb(void * s, lv_msg_t * m) { /*s: 订阅对象可用于退订*/ /*m: 消息对象包含 msg_id、payload 以及订阅时设置的 user_data*/ ...do something... }其中s是订阅对象subscribe object即lv_msg_subscribe的返回值在回调中可用于后续退订m是消息对象包含消息 ID、载荷与订阅时传入的user_data。从lv_msg_t中可以取出以下数据lv_msg_get_id(m)—— 获取消息 IDuint32_tlv_msg_get_payload(m)—— 获取载荷指针const void *lv_msg_get_user_data(m)—— 获取订阅时设置的 user_datavoid *。注意事项基于源码的补充本仓库内置 LVGL 版本中订阅函数的实际名称是lv_msg_subsribe源码 lv_msg.h 中拼写为lv_msg_subsribe缺少字母 c官方文档中则写作lv_msg_subscribe。在调用时请以当前仓库头文件的实际函数名为准以免产生编译错误。lv_msg_t的结构体定义见 lv_msg.h如下typedef struct { uint32_t id; /*消息 ID*/ void * user_data; /*订阅时设置的 user_data*/ void * _priv_data; /*内部使用对象订阅时用于保存 obj*/ const void * payload; /*消息载荷指针*/ } lv_msg_t;五、订阅与对象绑定lv_msg_subscribe_obj LV_EVENT_MSG_RECEIVED在 LVGL 中最常见的需求是某个控件关心某条消息。为此lv_msg提供了更便捷的对象订阅接口lv_msg_subscribe_obj(msg_id, obj, user_data)同样地仓库中该函数实际名为lv_msg_subsribe_obj见 lv_msg.h。当有匹配msg_id的新消息被发布时LVGL会向该对象发送一个LV_EVENT_MSG_RECEIVED事件。用法示例lv_obj_add_event_cb(user_name_label, user_name_label_event_cb, LV_EVENT_MSG_RECEIVED, NULL); lv_msg_subsribe_obj(MSG_USER_NAME_CHANGED, user_name_label, NULL); ... void user_name_label_event_cb(lv_event_t * e) { lv_obj_t * label lv_event_get_target(e); lv_msg_t * m lv_event_get_msg(e); lv_label_set_text(label, lv_msg_get_payload(m)); }事件回调中通过lv_event_get_msg(e)取回消息对象再配合lv_msg_get_payload(m)读取数据。对象订阅的内部机制见 lv_msg.c值得注意lv_msg_subsribe_obj内部先调用lv_msg_subsribe注册一个统一的内部回调obj_notify_cb并把user_data透传订阅对象本身存放在sub_dsc_t结构体的_priv_data字段中该字段注释即写明 Internal: used only store obj in lv_obj_subscribe当消息到达时obj_notify_cb通过lv_event_send(m-_priv_data, LV_EVENT_MSG_RECEIVED, m)把消息作为事件参数投递到对象首次订阅时还会给对象注册一个LV_EVENT_DELETE事件回调obj_delete_event_cb当对象被删除时自动退订所有与该对象相关的订阅避免悬垂指针——这正是推荐用对象订阅代替手动管理退订的底层原因。六、退订lv_msg_unsubscribelv_msg_subscribe以及lv_msg_subscribe_obj返回一个订阅对象指针可用来退订void * s1; s1 lv_msg_subscribe(MSG_USER_DOOR_OPENED, some_callback, NULL); ... lv_msg_unsubscribe(s1);源码中lv_msg_unsubscribelv_msg.c通过_lv_ll_remove将该订阅节点从全局订阅链表subs_ll中摘除并释放其内存。需要注意的是退订后该指针即失效不能再次使用如果对象订阅是通过lv_msg_subsribe_obj建立的通常无需手动退订——对象删除时会自动清理见上一节的obj_delete_event_cb实现它遍历整条订阅链表删除所有_priv_data obj的订阅。七、从订阅链表到事件分发lv_msg 底层实现剖析结合 lv_msg.c 源码可以完整还原lv_msg的运行机制1. 全局订阅表static lv_ll_t subs_ll是一条链表每个节点为sub_dsc_ttypedef struct { uint32_t msg_id; /*订阅的消息 ID*/ lv_msg_subscribe_cb_t callback; /*订阅回调*/ void * user_data; /*订阅时设置的 user_data*/ void * _priv_data; /*内部对象订阅时存放 obj*/ } sub_dsc_t;2. 模块初始化lv_msg_init注册事件代码LV_EVENT_MSG_RECEIVED lv_event_register_id()并初始化订阅链表。在 LVGL 中该函数由框架内部自动调用无需用户手动初始化。3. 消息分发核心notifylv_msg.cstatic void notify(lv_msg_t * m) { sub_dsc_t * s; _LV_LL_READ(subs_ll, s) { if(s-msg_id m-id s-callback) { m-user_data s-user_data; m-_priv_data s-_priv_data; s-callback(s, m); } } }关键点在于notify线性遍历整条订阅链表逐个匹配msg_id为每个匹配的订阅者临时填充user_data和_priv_data后再调用回调。这也意味着每条消息的复杂度为 O(订阅总数)多个订阅者按订阅先后顺序依次收到通知同一消息 ID 可被多个订阅者监听回调中修改订阅表如发送其他消息、退订时需注意链表遍历的安全性obj_delete_event_cb中即专门注释说明要先保存 next 节点再退订。八、官方示例精读三种典型应用场景仓库在examples/others/msg/下提供了三个可直接编译运行的官方示例均以#if LV_USE_MSG ... LV_BUILD_EXAMPLES作为编译条件分别演示了不同层次的用法。8.1 滑块 → 标签最简对象订阅lv_example_msg_1源码 lv_example_msg_1.c 演示了滑块值变化 → 标签实时显示的完整链路#define MSG_NEW_TEMPERATURE 1 /*滑块变化时发送消息载荷为滑块值指针*/ static void slider_event_cb(lv_event_t * e) { lv_obj_t * slider lv_event_get_target(e); int32_t v lv_slider_get_value(slider); lv_msg_send(MSG_NEW_TEMPERATURE, v); } /*标签订阅消息并通过 user_data 传递格式化字符串%d °C*/ lv_msg_subsribe_obj(MSG_NEW_TEMPERATURE, label, %d °C); static void label_event_cb(lv_event_t * e) { lv_obj_t * label lv_event_get_target(e); lv_msg_t * m lv_event_get_msg(e); const char * fmt lv_msg_get_user_data(m); const int32_t * v lv_msg_get_payload(m); lv_label_set_text_fmt(label, fmt, *v); }这个例子展示了user_data的典型用法订阅时把格式化字符串%d °C作为 user_data 传入回调中取出后配合载荷完成lv_label_set_text_fmt格式化显示。8.2 PIN 登录界面状态机式消息流lv_example_msg_2源码 lv_example_msg_2.c 实现了一个无需任何全局变量的 PIN 登录界面文本框、登出按钮、状态提示标签、启动按钮等多个对象订阅同一条消息 ID登录成功/失败/登出时通过消息驱动所有相关控件同步状态#define MSG_LOGIN_ATTEMPT 1 #define MSG_LOG_OUT 2 #define MSG_LOGIN_ERROR 3 #define MSG_LOGIN_OK 4 /*登录校验比较输入与 user_data 中的预期密码然后转发 LOGIN_OK / LOGIN_ERROR*/ static void auth_manager(void * s, lv_msg_t * m) { const char * pin_act lv_msg_get_payload(m); const char * pin_expexted lv_msg_get_user_data(m); if(strcmp(pin_act, pin_expexted) 0) lv_msg_send(MSG_LOGIN_OK, NULL); else lv_msg_send(MSG_LOGIN_ERROR, Incorrect PIN); }各控件的事件回调中统一用lv_msg_get_id(m)的 switch-case 区分消息类型再执行相应 UI 更新清空输入、禁用/启用按钮、变色提示等。这个示例是一条消息广播给多个订阅者的绝佳参考——在 X-Track 这类多页面、多状态提示的应用中非常实用。8.3 中央状态 请求-更新模式lv_example_msg_3源码 lv_example_msg_3.c 展示了更进阶的架构系统状态只存于一处value_handler内的static int32_t value所有控件通过消息与之交互增减按钮发送MSG_INC/MSG_DEC滑块发送MSG_SET载荷为新值指针value_handler是唯一的状态持有者状态变化后统一广播MSG_UPDATE新创建的控件不知道当前状态因此启动时发送MSG_UPDATE_REQUEST由value_handler响应并立即回发MSG_UPDATE完成初始状态同步。这种请求-更新模式MSG_UPDATE_REQUEST/MSG_UPDATE成对出现让后加入的 UI 元素也能自动获取当前状态是构建可动态增删控件的界面的理想模式。九、在 X-Track 模拟器中使用 lv_msg 的前提要在 X-Track 模拟器工程中使用lv_msg需要确保两处配置LVGL 配置在 lv_conf.h第 731 行附近将LV_USE_MSG置为1#define LV_USE_MSG 0 /*改为 1 以启用消息模块*/LVGL 的src/lv_conf_internal.h第 2240 行附近会优先采用用户配置文件中已定义的LV_USE_MSG因此以lv_conf.h的值为准。编译参与lv_msg.c属于src/extra/others/模块通过 src/extra/lv_extra.c 与 src/extra/others/lv_others.h 接入构建开启LV_USE_MSG后其初始化与注册逻辑会随 LVGL 初始化流程一并完成应用代码直接调用lv_msg_send/lv_msg_subsribe_obj等 API 即可。开启后开发者即可在 X-Track 模拟器的多页面 UI如USER/Pages/下的 Dialplate、LiveMap、SystemInfos 等页面之间用消息机制解耦地传递 GPS 数据、系统状态等变化通知替代全局变量或页面间直接调用。十、API 速查表以下为lv_msg模块在本仓库源码中的全部公开 API 一览声明见 lv_msg.hAPI作用lv_msg_send(msg_id, payload)发送一条消息同步通知所有匹配订阅者lv_msg_subsribe(msg_id, cb, user_data)订阅消息返回订阅对象指针lv_msg_subsribe_obj(msg_id, obj, user_data)让对象订阅消息收到后触发LV_EVENT_MSG_RECEIVED事件lv_msg_unsubscribe(s)按订阅对象指针退订lv_msg_get_id(m)从消息对象取消息 IDlv_msg_get_payload(m)从消息对象取载荷指针lv_msg_get_user_data(m)从消息对象取订阅时设置的 user_datalv_event_get_msg(e)在LV_EVENT_MSG_RECEIVED事件回调中取消息对象LV_EVENT_MSG_RECEIVED对象订阅专用的接收事件代码小结lv_msg用一套极其轻量的 API 在 LVGL 中实现了完整的发布/订阅模式消息 ID 负责解耦、payload 负责传数据、user_data 负责携带上下文对象订阅机制则让控件与消息系统无缝衔接并自动管理生命周期。结合examples/others/msg/下的三个官方示例单对象同步、多对象广播、中央状态请求-更新开发者完全可以在不引入全局变量的前提下构建出模块化、可扩展的 LVGL 界面通信体系——这正是 X-Track 这类集多页面、多数据源于一体的嵌入式 UI 应用所急需的架构基础。更多细节可直接阅读仓库中的模块头文件 lv_msg.h 与实现 lv_msg.c以及三个示例源码 lv_example_msg_1.c、lv_example_msg_2.c、lv_example_msg_3.c。赞分享智能硬件嵌入式硬件开发【免费下载链接】X-TRACKA GPS bicycle speedometer that supports offline maps and track recording项目地址https://gitcode.com/gh_mirrors/xt/X-TRACK点击查看免费下载相关推荐Go Patterns 发布-订阅Publish/Subscribe消息模式实战解析Go Patterns 发布 订阅Publish/Subscribe消息模式实战解析 发布 订阅Publish/Subscribe是 go patter示例工程教程文档终极指南微服务消息模式对比——发布订阅与消息队列的核心差异与应用场景终极指南微服务消息模式对比——发布订阅与消息队列的核心差异与应用场景 在微服务架构中消息模式是实现服务间高效通信的关键技术。本文将深入解析两种最常用的消息模文档知识库后端DownloadBot代码架构解析Go语言实现的分布式机器人DownloadBot代码架构解析Go语言实现的分布式机器人 DownloadBot是一款基于Go语言开发的分布式跨平台Telegram机器人能够控制Ari上一篇实战解析高效处理纽约市出租车与网约车大数据的完整指南下一篇GameAssist AI游戏助手3大核心技术揭秘让你的游戏体验全面升级创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。