Skip to content

Commit 2e21f65

Browse files
yingtao450claude
andcommitted
docs: add ai_ui architecture guide and photo album example
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8a25464 commit 2e21f65

11 files changed

Lines changed: 2214 additions & 0 deletions

File tree

docs/ai_ui_skill.md

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
---
2+
name: ai_ui_skill
3+
description: AI UI component architecture, patterns, conventions and implementation guide for the wechat-style embedded LVGL UI system
4+
type: reference
5+
originSessionId: fa30025d-8321-410e-b472-b1c0ce3fe12e
6+
---
7+
# AI UI Component Skill Reference
8+
9+
## 1. Architecture Overview
10+
11+
```
12+
ai_ui/
13+
include/
14+
ai_ui_manage.h # 核心接口定义(所有 INTFS 结构体、枚举、API 声明)
15+
ai_ui_page.h # 页面管理器(栈式导航)
16+
ai_ui_camera.h # Camera 管理层
17+
ai_ui_image_album.h # Album 管理层
18+
src/
19+
ai_ui_manage.c # 核心调度:消息队列 + 回调分发 + action 队列
20+
ai_ui_page.c # 页面栈管理(open/close)
21+
ai_ui_camera.c # Camera 管理逻辑
22+
ai_ui_image_album.c # Album 管理逻辑
23+
ai_ui_stream_text.c # 流式文本显示
24+
ai_ui_icon_font.c # 字体管理
25+
wechat/ # WeChat 风格 UI 实现
26+
ai_ui_chat_wechat.c # 主入口:LVGL 初始化、screen、container、status bar
27+
ai_ui_wechat_chat.c # Chat 子页面:消息、流式、链接、图片查看、附件栏
28+
ai_ui_wechat_camera.c # Camera 子页面:预览画布、快门、缩略图、关闭
29+
ai_ui_wechat_album.c # Album 子页面:单图查看、全部缩略图、选择模式
30+
ai_ui_wechat_common.h # wechat 内部共享声明
31+
chatbot/ # Chatbot 风格 UI(独立变体)
32+
oled/ # OLED 风格 UI(独立变体)
33+
```
34+
35+
## 2. 四层接口体系
36+
37+
管理层通过四个独立的 INTFS 结构体委托显示回调,各 UI 变体独立注册:
38+
39+
| 结构体 | 职责 | 注册函数 |
40+
|--------|------|----------|
41+
| `AI_UI_INTFS_T` | 全局:init、emotion、status、notification、wifi、chat_mode | `ai_ui_register()` |
42+
| `AI_UI_CHAT_INTFS_T` | Chat:open/close、user_msg、ai_msg、stream、image、link、attach | `ai_ui_chat_register()` |
43+
| `AI_UI_CAMERA_INTFS_T` | Camera:open、yuv_flush、thumbnail_jpeg、close | `ai_ui_camera_register()` |
44+
| `AI_UI_ALBUM_INTFS_T` | Album:open、image、all_thumb_list、select_thumb_list、close | `ai_ui_image_album_register()` |
45+
46+
**Wechat 注册模式**(ai_ui_chat_wechat.c):
47+
```c
48+
// 主入口注册全局接口
49+
ai_ui_register(&intfs);
50+
// 各子页面独立注册
51+
ai_ui_wechat_chat_register(); // → ai_ui_chat_register()
52+
ai_ui_wechat_camera_register(); // → ai_ui_camera_register()
53+
ai_ui_wechat_album_register(); // → ai_ui_image_album_register()
54+
```
55+
56+
## 3. 消息调度机制
57+
58+
`ai_ui_manage.c` 维护两个队列:
59+
60+
- **UI 消息队列** (`sg_ui_queue_hdl`):外部调用 `ai_ui_disp_msg()` / `ai_ui_disp_msg_sync()` 投递,`__ai_chat_ui_task` 线程消费,通过 `__ui_disp_msg_handle()` 分发到对应的 INTFS 回调
61+
- **Action 队列** (`sg_action_queue_hdl`):UI 层调用 `ai_ui_notify_action()` 投递用户操作事件(拍照、翻页、删除等),`__ai_ui_action_task` 线程消费
62+
63+
同步版本 `ai_ui_disp_msg_sync()` 通过二值信号量阻塞调用者直到 UI 线程处理完毕。**不能在 UI 线程(ai_ui task)中调用,否则死锁。**
64+
65+
## 4. 页面管理器
66+
67+
`ai_ui_page.h/.c` 提供栈式页面导航:
68+
69+
```c
70+
typedef enum {
71+
AI_UI_PAGE_CHAT,
72+
AI_UI_PAGE_CAMERA,
73+
AI_UI_PAGE_ALBUM_VIEW,
74+
AI_UI_PAGE_ALBUM_ALL,
75+
AI_UI_PAGE_ALBUM_SELECT,
76+
} AI_UI_PAGE_E;
77+
78+
ai_ui_page_open(AI_UI_PAGE_CAMERA, NULL); // push + 调 open 回调
79+
ai_ui_page_close(); // pop + 调 close 回调
80+
```
81+
82+
页面的 open/close 回调在 `ai_ui_manage.c``__page_register_all()` 中注册。
83+
84+
## 5. LVGL 层级结构(Wechat)
85+
86+
```
87+
lv_scr_act()
88+
└── screen (LV_HOR_RES x LV_VER_RES, bg=0xF0F0F0)
89+
├── container (LV_HOR_RES x LV_VER_RES)
90+
│ ├── status_bar (LV_HOR_RES x 40, green)
91+
│ │ ├── mode_label (LEFT)
92+
│ │ ├── status_label (CENTER)
93+
│ │ ├── emotion_label (LEFT of status)
94+
│ │ ├── notification_label (CENTER, hidden)
95+
│ │ └── network_label (RIGHT)
96+
│ ├── chat.content (滚动消息区)
97+
│ ├── chat.picture (图片查看, hidden)
98+
│ └── chat.attach_bar (附件缩略图, hidden)
99+
├── camera.page (全屏, hidden) ← parent=screen,覆盖 status bar
100+
│ ├── preview_canvas (lv_canvas)
101+
│ └── ctrl_bar (底部控制栏, 可隐藏)
102+
└── album pages (全屏, hidden) ← parent=screen,覆盖 status bar
103+
├── view_page
104+
├── all_page
105+
└── select_page
106+
```
107+
108+
**关键决策**
109+
- chat 子页面 parent = `container`(与 status bar 共存)
110+
- camera/album 子页面 parent = `screen`(全屏覆盖,包括状态栏)
111+
112+
## 6. 图像处理模式
113+
114+
### 6.1 JPEG 缩略图(推荐方式)
115+
116+
使用 `tal_image_jpeg_scale_rgb565()` 一步完成解码+缩放:
117+
118+
```c
119+
TAL_IMAGE_JPEG_SCALE_IN_T scale_in = {0};
120+
scale_in.method = TAL_IMAGE_SCALE_MTH_NEAREST;
121+
scale_in.mode = TAL_IMAGE_SCALE_MODE_SIZE;
122+
scale_in.data = jpeg_data;
123+
scale_in.size = jpeg_len;
124+
scale_in.out_width = THUMB_SIZE;
125+
scale_in.out_height = THUMB_SIZE;
126+
127+
TAL_IMAGE_SCALE_OUT_T scale_out = {0};
128+
tal_image_jpeg_scale_rgb565(&scale_in, &scale_out);
129+
130+
// scale_out.buf 由 API 内部分配,需用 tal_image_scale_buf_free() 释放
131+
// 如需长期持有(canvas buffer),先 memcpy 到自己的 buffer
132+
uint8_t *thumb_buf = Malloc(THUMB_SIZE * THUMB_SIZE * 2);
133+
memcpy(thumb_buf, scale_out.buf, THUMB_SIZE * THUMB_SIZE * 2);
134+
tal_image_scale_buf_free(&scale_out);
135+
```
136+
137+
### 6.2 JPEG 原图显示
138+
139+
直接解码不缩放:
140+
141+
```c
142+
TAL_IMAGE_JPEG_INFO_T info = {0};
143+
tal_image_jpeg_get_info(data, len, &info);
144+
145+
uint32_t buf_size = (uint32_t)info.width * info.height * 2;
146+
uint8_t *rgb565_buf = Malloc(buf_size);
147+
148+
TAL_IMAGE_JPEG_OUTPUT_T out = {0};
149+
out.out_buf = rgb565_buf; out.out_buf_size = buf_size;
150+
out.out_width = info.width; out.out_height = info.height;
151+
tal_image_jpeg_decode_rgb565(data, len, &out);
152+
153+
lv_canvas_set_buffer(canvas, rgb565_buf, info.width, info.height, LV_IMG_CF_TRUE_COLOR);
154+
```
155+
156+
### 6.3 Camera YUV 预览(双缓冲)
157+
158+
```c
159+
// 1. 在 disp lock 外分配新 buffer 并转换
160+
uint8_t *rgb565_buf = CAMERA_UI_MALLOC(w * h * 2);
161+
TAL_IMAGE_YUV422_TO_RGB_T conv = { .in_buf=yuv, .in_width=w, .in_height=h,
162+
.out_buf=rgb565_buf, .out_width=w, .out_height=h };
163+
tal_image_convert_yuv422_to_rgb565(&conv);
164+
165+
// 2. Lock → 设置 canvas → 释放旧 buffer → Unlock
166+
lv_vendor_disp_lock();
167+
lv_canvas_set_buffer(canvas, rgb565_buf, w, h, LV_IMG_CF_TRUE_COLOR);
168+
if (old_buf) CAMERA_UI_FREE(old_buf);
169+
preview_buf = rgb565_buf;
170+
lv_vendor_disp_unlock();
171+
```
172+
173+
## 7. 内存管理约定
174+
175+
| 场景 | 分配 | 释放 |
176+
|------|------|------|
177+
| 通用 UI (chat) | `Malloc()` / `Free()` | `Free()` |
178+
| Camera (可能用 PSRAM) | `CAMERA_UI_MALLOC` / `CAMERA_UI_FREE` | `CAMERA_UI_FREE` |
179+
| Album | `tal_malloc()` / `tal_free()` | `tal_free()` |
180+
| Scale API 输出 | API 内部分配 | `tal_image_scale_buf_free()` |
181+
182+
**Canvas buffer 生命周期**:canvas 引用外部 buffer,必须在 buffer 释放前更新或删除 canvas。存储的 buffer 指针(如 `attach_bufs[]`、`preview_buf`、`view_buf`、`thumb_buf`)需在对象删除或页面关闭时释放。
183+
184+
## 8. 常用 LVGL 模式
185+
186+
### 8.1 线程安全
187+
188+
所有 LVGL API 调用必须包裹在 `lv_vendor_disp_lock()` / `lv_vendor_disp_unlock()` 之间。耗时操作(图像解码、转换)放在 lock 外。
189+
190+
### 8.2 Event 回调 user_data 内存泄漏防护
191+
192+
当 LVGL 对象绑定了动态分配的 user_data 时,必须同时注册 `LV_EVENT_DELETE` 回调来释放:
193+
194+
```c
195+
UI_LINK_CB_DATA_T *link_data = Malloc(sizeof(UI_LINK_CB_DATA_T));
196+
lv_obj_add_event_cb(label, __link_click_event_cb, LV_EVENT_CLICKED, link_data);
197+
lv_obj_add_event_cb(label, __link_delete_event_cb, LV_EVENT_DELETE, link_data);
198+
// __link_delete_event_cb 中 Free(data)
199+
```
200+
201+
### 8.3 手势识别
202+
203+
**关键机制(LVGL v9)**`indev_gesture()``act_obj`(手指下方对象)开始,沿父级链向上走,直到找到**没有** `LV_OBJ_FLAG_GESTURE_BUBBLE` 的对象,在那个对象上触发 `LV_EVENT_GESTURE`
204+
205+
**所有通过 `lv_obj_create(parent)` 创建的子对象,`LV_OBJ_FLAG_GESTURE_BUBBLE` 默认是开启的。** 因此,如果不做处理,手势会一路冒泡到根屏幕(`lv_obj_create(NULL)`),注册在中间层级的回调永远收不到事件。
206+
207+
**正确用法**:在接收手势的对象上手动清除 `GESTURE_BUBBLE`,让手势在此停止:
208+
209+
```c
210+
/* 清除 GESTURE_BUBBLE,让手势在 view_page 停止,不再往上冒泡 */
211+
lv_obj_clear_flag(view_page, LV_OBJ_FLAG_GESTURE_BUBBLE);
212+
lv_obj_add_flag(view_page, LV_OBJ_FLAG_CLICKABLE);
213+
lv_obj_add_event_cb(view_page, __gesture_cb, LV_EVENT_GESTURE, NULL);
214+
215+
/* 子对象保持默认的 GESTURE_BUBBLE,手势会从子对象冒泡到 view_page */
216+
```
217+
218+
```c
219+
static void __gesture_cb(lv_event_t *e) {
220+
(void)e;
221+
lv_indev_t *indev = lv_indev_active(); /* LVGL v9 API */
222+
if (indev == NULL) return;
223+
lv_dir_t dir = lv_indev_get_gesture_dir(indev);
224+
if (dir == LV_DIR_LEFT) { /* 左滑 → 下一张 */ }
225+
if (dir == LV_DIR_RIGHT) { /* 右滑 → 上一张 */ }
226+
}
227+
```
228+
229+
**lv_canvas 的特殊性**`lv_canvas_create()` 内部会清除 `LV_OBJ_FLAG_CLICKABLE`,所以触摸会穿透 canvas 到其父对象(`view_page`)。父对象成为 `act_obj`,手势直接在父对象上触发,无需额外处理 canvas。
230+
231+
**常见踩坑**
232+
- `LV_OBJ_FLAG_SCROLLABLE` 影响的是滚动冲突(v8 的问题),不是手势冒泡(v9 的问题),清 SCROLLABLE 解决不了手势不触发
233+
- `lv_indev_get_act()` 在 v9 中通过 `lv_api_map_v8.h` 映射到 `lv_indev_active()`,两者等价,但推荐直接用 `lv_indev_active()`
234+
235+
### 8.4 点击隐藏/显示 UI 叠加层
236+
237+
```c
238+
static void __preview_click_cb(lv_event_t *e) {
239+
if (lv_obj_has_flag(ctrl_bar, LV_OBJ_FLAG_HIDDEN))
240+
lv_obj_clear_flag(ctrl_bar, LV_OBJ_FLAG_HIDDEN);
241+
else
242+
lv_obj_add_flag(ctrl_bar, LV_OBJ_FLAG_HIDDEN);
243+
}
244+
```
245+
246+
## 9. 附件栏(Attach Bar)
247+
248+
- 最多 `MAX_ATTACH_NUM`(4) 张缩略图
249+
- 每张图:container(48x48) + canvas + close button(16x16, 右上角)
250+
- buffer 存储在 `attach_bufs[]` 数组,通过 `lv_obj_set_user_data(container, buf)` 关联
251+
- 删除时 shift 数组、释放 buffer、删除 LVGL 对象;全部为空时隐藏 bar
252+
253+
## 10. CMakeLists.txt 条件编译
254+
255+
根据 Kconfig 选择只编译对应的 UI 变体:
256+
257+
```cmake
258+
# 公共源文件
259+
aux_source_directory(${COMP_MODULE_PATH}/src COMP_MODULE_SRCS)
260+
261+
# 按 Kconfig 选择 UI 变体
262+
if (CONFIG_ENABLE_AI_CHAT_GUI_WECHAT STREQUAL "y")
263+
file(GLOB WECHAT_SRCS ${COMP_MODULE_PATH}/src/wechat/*.c)
264+
list(APPEND COMP_MODULE_SRCS ${WECHAT_SRCS})
265+
elseif (CONFIG_ENABLE_AI_CHAT_GUI_CHATBOT STREQUAL "y")
266+
...
267+
elseif (CONFIG_ENABLE_AI_CHAT_GUI_OLED STREQUAL "y")
268+
...
269+
endif()
270+
```
271+
272+
Include 路径也按条件添加(如 `src/wechat` 仅在 wechat 模式)。
273+
274+
## 11. 嵌入式平台注意事项
275+
276+
- **不用标准 `<string.h>`**`tal_api.h` 提供 `memcpy`/`memset`/`strlen`/`strcmp`/`snprintf`
277+
- **Clang LSP 误报**`string.h file not found``memcpy undeclared` 等是嵌入式交叉编译环境下的 LSP 误诊,不是真正的编译错误
278+
- **Tuya 宏**`TUYA_CHECK_NULL_RETURN``TUYA_CALL_ERR_RETURN``PR_ERR`/`PR_DEBUG`/`PR_INFO`
279+
- **内存**`Malloc`/`Free`(大写开头)是 Tuya 封装,`tal_malloc`/`tal_free` 是 TAL 层封装
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
##
2+
# @file CMakeLists.txt
3+
# @brief
4+
#/
5+
6+
# APP_PATH
7+
set(APP_PATH ${CMAKE_CURRENT_LIST_DIR})
8+
9+
# APP_NAME
10+
get_filename_component(APP_NAME ${APP_PATH} NAME)
11+
12+
# APP_SRCS
13+
file(GLOB_RECURSE APP_SRCS "${APP_PATH}/src/*.c")
14+
15+
set(APP_MODULE_INC
16+
${APP_PATH}/include
17+
)
18+
19+
########################################
20+
# Target Configure
21+
########################################
22+
add_library(${EXAMPLE_LIB})
23+
24+
target_sources(${EXAMPLE_LIB}
25+
PRIVATE
26+
${APP_SRCS}
27+
)
28+
29+
target_include_directories(${EXAMPLE_LIB}
30+
PRIVATE
31+
${APP_MODULE_INC}
32+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CONFIG_BOARD_CHOICE_T5AI=y
2+
CONFIG_TUYA_T5AI_BOARD_EX_MODULE_35565LCD=y
3+
CONFIG_ENABLE_EX_MODULE_CAMERA=y
4+
CONFIG_ENABLE_LIBLVGL=y
5+
CONFIG_LVGL_ENABLE_TP=y
6+
CONFIG_ENABLE_IMAGE_ALBUM=y
7+
# CONFIG_ENABLE_IMAGE_ALBUM_STORAGE_MEM is not set
8+
CONFIG_ENABLE_IMAGE_ALBUM_STORAGE_SD=y
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CONFIG_BOARD_CHOICE_T5AI=y
2+
CONFIG_TUYA_T5AI_BOARD_EX_MODULE_35565LCD=y
3+
CONFIG_ENABLE_EX_MODULE_CAMERA=y
4+
CONFIG_ENABLE_LIBLVGL=y
5+
CONFIG_LVGL_ENABLE_TP=y
6+
CONFIG_ENABLE_IMAGE_ALBUM=y
7+
CONFIG_ENABLE_IMAGE_ALBUM_STORAGE_MEM=y

0 commit comments

Comments
 (0)