--- name: "packer-ui" description: "包装机界面(packer_ui)设计规范与开发指南。包括布局尺寸、颜色体系、字体使用和后续开发指导。适用于800x480 LCD上的LVGL界面。" --- # Packer UI Design Guide ## 1. 布局总览 屏幕分辨率为 **800x480**,整体划分为左右两栏: ``` +--------------------------------------------------------------+ | +--------------------------+ +--------------------------+ | | | 状态卡片 (左) | | 启动按钮 (右) | | | | x=10, y=10 | | x=580, y=10 | | | | w=560, h=330 | | w=210, h=330 | | | | | | | | | | +----------+----------+ | | "启动" | | | | | 当前重量 | 总包数 | | | | | | | | | | | | | | | | | 0.0 | 0 | | | | | | | | | | | | | | | | | 称重斗.. | 累计成品 | | | | | | | +----------+----------+ | | | | | | 工序 进料 | o 已停止 | | | | | +--------------------------+ +--------------------------+ | | +--------------------------+ +--------------------------+ | | | 目标重量卡片 (左下) | | 复位按钮 (右下) | | | | x=10, y=350 | | x=580, y=350 | | | | w=560, h=120 | | w=210, h=120 | | | | | | | | | | 设计重量 190.0 克/包 | | 复位 | | | | o(-) o(+) | | | | | +--------------------------+ +--------------------------+ | +--------------------------------------------------------------+ ``` ## 2. 容器与位置参数 ### 2.1 顶层容器 | 控件 | 创建函数 | x | y | w | h | 圆角 | 背景色 | |------|---------|---|---|---|---|------|--------| | packer_scr | lv_obj_create | - | - | 800 | 480 | - | COLOR_BG | | card_status | lv_obj_create | 10 | 10 | 560 | 330 | 20 | COLOR_CARD | | card_target | lv_obj_create | 10 | 350 | 560 | 120 | 20 | COLOR_CARD_TARGET | | btn_start | lv_btn_create | 580 | 10 | 210 | 330 | 20 | COLOR_BTN_START | | btn_reset | lv_btn_create | 580 | 350 | 210 | 120 | 20 | COLOR_BTN_DARK | | btn_minus | lv_btn_create | 331 | 30 | 60 | 60 | 30 | COLOR_BTN_ADJ | | btn_plus | lv_btn_create | 463 | 30 | 60 | 60 | 30 | COLOR_BTN_ADJ | ### 2.2 状态卡片内部分布 card_status (560x330) 被垂直分割线分成左右两半: **左半区 -- 当前重量 (宽度 281px)** | 控件 | 文本 | 父级x偏移 | 父级y偏移 | 宽 | 高 | 字体 | 颜色 | |------|------|----------|----------|-----|-----|------|------| | w_title | "当前重量" | 0 | 14 | 281 | 28 | custom_cjk_20 | COLOR_GREEN | | label_current_weight | "0.0" | 0 | 90 | 281 | 84 | custom_cjk_60 | COLOR_TEXT_MAIN | | w_sub | "称重斗实时值" | 0 | 200 | 281 | 20 | custom_cjk_16 | 0x6f9181 | **右半区 -- 总包数 (宽度 278px)** | 控件 | 文本 | 父级x偏移 | 父级y偏移 | 宽 | 高 | 字体 | 颜色 | |------|------|----------|----------|-----|-----|------|------| | c_title | "总包数" | 282 | 14 | 278 | 30 | custom_cjk_20 | COLOR_GREEN | | label_pack_count | "0" | 282 | 90 | 278 | 84 | custom_cjk_60 | COLOR_TEXT_MAIN | | c_sub | "累计成品" | 282 | 200 | 278 | 20 | custom_cjk_16 | 0x6f9181 | **分割线** | 控件 | 位置(x,y) | 尺寸(w,h) | 颜色 | |------|----------|----------|------| | horizontal_line | (40, 240) | (480, 1) | COLOR_BORDER | | vertical_line | (280, 20) | (1, 290) | COLOR_BORDER | **状态指示** | 控件 | 文本 | 父级x偏移 | 父级y偏移 | 宽 | 高 | 圆角 | 颜色 | |------|------|----------|----------|-----|-----|------|------| | status_dot | - | 330 | 270 | 34 | 35 | 17(圆形) | COLOR_GREEN | | label_status | "已停止" | 420 | 280 | - | - | - | COLOR_TEXT_MAIN, font 20px | **工序状态 (左下, 分割线下方)** 标题 "工序" 位于 x=50, y=270, 使用 font 20px, COLOR_GREEN。 当前工序名称紧跟在标题右侧, 使用 font 20px, COLOR_TEXT_MAIN: | 控件 | 文本(初始) | x | y | 字体 | 颜色 | |-----|-----------|----|----|------|------| | proc_title | "工序" | 50 | 270 | custom_cjk_20 | COLOR_GREEN | | label_step | "进料" | 120 | 270 | custom_cjk_20 | COLOR_TEXT_MAIN | 通过 `current_step` (0~5) 索引 `step_name[]` 数组动态切换显示的工序: ```c static const char *step_name[] = {"进料", "称重", "压缩", "打包", "封包", "出料"}; if (current_step < 6) lv_label_set_text(label_step, step_name[current_step]); ``` ### 2.3 目标重量卡内部布局 card_target (560x120) 从左到右: | 控件 | 文本 | 父级x偏移 | 父级y偏移 | 宽/尺寸 | 高 | 字体 | 颜色 | |------|------|----------|----------|---------|-----|------|------| | t_title | "设计重量" | 0 | 50 | 90 | 120 | custom_cjk_20 | COLOR_GREEN | | label_target_val | "190.0" | 100 | 30 | 150 | 120 | custom_cjk_60 | COLOR_TEXT_MAIN | | t_unit | "克/包" | 240 | 50 | - | - | custom_cjk_20 | 0x6f9181 | | btn_minus | "-" | 331 | 30 | 60x60 | - | custom_cjk_60 | COLOR_TEXT_MAIN | | btn_plus | "+" | 463 | 30 | 60x60 | - | custom_cjk_40 | COLOR_TEXT_MAIN | > btn_minus 字符"-"用 font 60px, btn_plus 字符"+"用 font 40px, 视觉上调整到大小接近。 ## 3. 颜色体系 ### 3.1 颜色常量定义 ```c COLOR_BG = 0x161b22 // 深色背景(深蓝黑) COLOR_CARD = 0x000000 // 卡片背景(纯黑) COLOR_CARD_TARGET = 0x080a0e // 目标重量卡片(近乎纯黑) COLOR_BORDER = 0x30363d // 分割线颜色(暗灰) COLOR_TEXT_MAIN = 0xffffff // 主文字(纯白) COLOR_TEXT_SEC = 0x9ab3a5 // 次要文字(薄荷灰) COLOR_BTN_START = 0x06990c // 启动按钮背景(绿色) COLOR_BTN_DARK = 0x000000 // 复位按钮背景(纯黑) COLOR_BTN_ADJ = 0x1e3c28 // 加减按钮背景(深绿) COLOR_GREEN = 0x06990c // 状态灯/绿色指示(绿色) COLOR_RED = 0x990B06 // 红色(深红) COLOR_ADJ_ICON = 0xc4c4c4 // 调节图标辅助色(浅灰) ``` ### 3.2 辅助颜色(直接硬编码) | 用途 | 颜色值 | 说明 | |------|--------|------| | w_sub / c_sub 文字 | 0x6f9181 | 副标题, 茶绿色 | | t_unit "克/包" | 0x6f9181 | 单位标签, 茶绿色 | ### 3.3 状态灯逻辑 状态灯(status_dot)是一个 34x35 的圆形, 通过背景色切换运行/停止: | 状态 | 圆点颜色 | 圆点值 | 文字颜色 | |------|---------|--------|---------| | 运行中 | COLOR_GREEN | 0x06990c | COLOR_TEXT_MAIN | | 已停止 | COLOR_RED | 0x990B06 | COLOR_TEXT_MAIN | 状态灯右侧显示文字 label_status, 内容对应切换: "运行中" / "已停止", 文字始终为白色 (COLOR_TEXT_MAIN)。 ## 4. 字体系统 ### 4.1 字体规格 使用 SimHei(黑体)生成的 LVGL 位图字体, 4bpp 抗锯齿: | 大小 | 字体变量 | 适用场景 | |:----:|----------|----------| | 16px | custom_cjk_16 | 副标题, 辅助说明文字 | | 20px | custom_cjk_20 | 区域标题, 标签, 单位 | | 40px | custom_cjk_40 | 按钮文字, 部分符号(+) | | 60px | custom_cjk_60 | 数值显示, 大字符号(-) | ### 4.2 字体声明 ```c LV_FONT_DECLARE(custom_cjk_16); LV_FONT_DECLARE(custom_cjk_20); LV_FONT_DECLARE(custom_cjk_40); LV_FONT_DECLARE(custom_cjk_60); ``` ## 5. 按钮样式规则 所有按钮统一移除阴影, 轮廓, 边框: ```c // 每个按钮创建后必须执行的样式 lv_obj_set_style_shadow_width(btn, 0, 0); lv_obj_set_style_shadow_opa(btn, LV_OPA_0, 0); lv_obj_set_style_outline_width(btn, 0, 0); lv_obj_set_style_outline_opa(btn, LV_OPA_0, 0); lv_obj_set_style_border_width(btn, 0, 0); lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_NONE, 0); ``` 按钮样式差异(仅背景色和圆角不同): | 按钮 | 圆角 | 背景色 | 文字颜色 | 字体 | |------|------|--------|---------|------| | btn_start | 20 | COLOR_BTN_START (绿) | COLOR_TEXT_MAIN (白) | custom_cjk_40 | | btn_reset | 20 | COLOR_BTN_DARK (黑) | COLOR_TEXT_SEC (灰绿) | custom_cjk_40 | | btn_minus | 30 | COLOR_BTN_ADJ (深绿) | COLOR_TEXT_MAIN (白) | custom_cjk_60 | | btn_plus | 30 | COLOR_BTN_ADJ (深绿) | COLOR_TEXT_MAIN (白) | custom_cjk_40 | ## 6. 卡片通用样式 所有卡片(create_card)默认属性: ```c lv_obj_set_style_border_width(card, 0, 0); // 无边框 lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); // 不可滚动 // 圆角在 create_card 的参数中传入 // 背景色在 create_card 的参数中传入 ``` 状态卡片和目标重量卡片额外设置: ```c lv_obj_set_style_pad_hor(card, 0, LV_PART_MAIN); lv_obj_set_style_pad_ver(card, 0, LV_PART_MAIN); lv_obj_set_style_outline_width(card, 0, LV_PART_MAIN); lv_obj_set_style_border_width(card, 0, LV_PART_MAIN); ``` ## 7. 回调函数 | 回调 | 绑定的控件 | 触发事件 | 行为 | |------|-----------|---------|------| | start_cb | btn_start | LV_EVENT_CLICKED | 设置 is_running=true, 清零 current_weight, 更新显示 | | reset_cb | btn_reset | LV_EVENT_CLICKED | 设置 is_running=false, 清零 weight 和 pack_count, 更新显示 | | minus_cb | btn_minus | LV_EVENT_CLICKED | 运行中忽略, 否则 target_weight -= 10 (下限 10), 更新显示 | | plus_cb | btn_plus | LV_EVENT_CLICKED | 运行中忽略, 否则 target_weight += 10 (上限 2000), 更新显示 | ## 8. 核心数据结构 ```c // 控件指针(文件内 static) static lv_obj_t *label_target_val; // 目标重量值标签 static lv_obj_t *label_current_weight; // 当前重量值标签 static lv_obj_t *label_pack_count; // 总包数值标签 static lv_obj_t *label_status; // 状态文字标签 static lv_obj_t *status_dot; // 状态指示圆点 static lv_obj_t *label_step; // 当前工序标签(进料/称重/压缩/打包/封包/出料) // 状态变量(volatile, 可能被中断/任务修改) static volatile bool is_running = false; // 运行状态 static volatile uint8_t current_step = 0; // 当前工序索引(0~5) static volatile uint32_t target_weight = 190; // 目标重量 (单位: 0.1g) static volatile uint32_t current_weight = 230; // 当前重量 (单位: 0.1g) static volatile uint32_t pack_count = 2340; // 总包数 ``` ## 9. update_display() 刷新规则 每次状态变更后调用 `update_display()`: ```c static void update_display(void) { char tmp[32]; // 1. 刷新当前重量(保留一位小数) snprintf(tmp, sizeof(tmp), "%lu.%lu", (unsigned long)(current_weight / 10), (unsigned long)(current_weight % 10)); lv_label_set_text(label_current_weight, tmp); // 2. 刷新总包数(整数) snprintf(tmp, sizeof(tmp), "%lu", (unsigned long)pack_count); lv_label_set_text(label_pack_count, tmp); // 3. 刷新状态灯颜色 lv_obj_set_style_bg_color(status_dot, is_running ? COLOR_GREEN : COLOR_RED, 0); // 4. 刷新状态文字 lv_label_set_text(label_status, is_running ? "运行中" : "已停止"); } ``` > `update_display()` 由外部任务循环调用, 也由 start_cb/reset_cb 等回调触发。 ## 10. 后续开发指南 ### 10.1 新增控件原则 - **所有坐标都以 800x480 为基准**, 不要超出范围 - **使用 `create_card()` 辅助函数**创建新的卡片容器, 统一无边框无滚动 - **新按钮必须执行 "无边框三部曲"**: shadow=0, outline=0, border=0 - **新文字标签必须指定字体**, 不要依赖默认字体(默认字体不含中文) ### 10.2 颜色使用 - **标题一律用 COLOR_GREEN**, 内容/数值一律用 **COLOR_TEXT_MAIN** (白色) - **优先使用已定义的 COLOR_xxx 常量**, 不要随意硬编码新颜色 - 如需新增颜色: 在 `set_colors()` 中添加新常量, 不要在代码中直接写 `lv_color_hex()` - 副标题文字绿色调使用 `0x6f9181`, 与 COLOR_TEXT_SEC (0x9ab3a5) 区分 - COLOR_TEXT_SEC 目前仅用于复位按钮文字(白色改为后已不再使用, 保留以备后续) ### 10.3 字体使用 - **标题/说明**: custom_cjk_20 - **数值**: custom_cjk_60 - **辅助文字**: custom_cjk_16 - **按钮**: custom_cjk_40 - 如需新增汉字: 使用 [gen_font](../gen_font/SKILL.md) 技能重新生成字体 ### 10.4 添加新页面 如需添加新页面(如设置页, 历史记录页), 建议步骤: 1. 新建 `xxx_ui.cpp` / `xxx_ui.h`(参考 packer_ui 结构) 2. 遵循相同的卡片布局风格: 深色背景, 圆角卡片, 绿黑配色 3. 在新文件的 `xxx_ui_create()` 中完成创建 4. 在 `lv_scr_load()` 切换页面时, 先用 `lv_obj_del()` 删除旧页面控件(可选) 5. 在 `main.cpp` 或任务流程中调用新页面的 create 函数 ### 10.5 动画与过渡 当前界面为静态布局, 如需添加过渡效果: ```c // 示例: 按钮按下缩放动画 lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, btn); lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_scale); lv_anim_set_values(&a, 256, 280); // LV_IMG_ZOOM_NONE=256 lv_anim_set_time(&a, 100); lv_anim_start(&a); ``` ### 10.6 国际化和单位 - 所有 UI 文字为中文硬编码 - 重量显示格式: `"190.0"`(一位小数), 重量内部以 0.1g 为单位存储 - 如需支持多语言, 建议将所有字符串抽取到常量表中 ### 10.7 触摸事件 当前按钮使用 `LV_EVENT_CLICKED` 事件, 如需区分按下/释放: ```c lv_obj_add_event_cb(btn, my_cb, LV_EVENT_PRESSED, NULL); lv_obj_add_event_cb(btn, my_cb, LV_EVENT_RELEASED, NULL); static void my_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_PRESSED) { // 按下时改变按钮样式 } else if (code == LV_EVENT_RELEASED) { // 释放时恢复 } } ``` ### 10.8 键盘/物理按键支持 LVGL 默认支持键盘焦点导航。焦点指示器为按钮的 outline 样式。如需自定义焦点样式: ```c // 自定义焦点时的边框颜色 lv_obj_set_style_outline_color(btn, lv_color_hex(0x58a6ff), LV_STATE_FOCUSED); lv_obj_set_style_outline_width(btn, 2, LV_STATE_FOCUSED); ``` ## 11. 文件位置 | 文件 | 路径 | |------|------| | UI 源码 | [app/tasks/packer_ui.cpp](file:///d:/20_AI/LSPi/app/tasks/packer_ui.cpp) | | UI 头文件 | [app/tasks/packer_ui.h](file:///d:/20_AI/LSPi/app/tasks/packer_ui.h) | | 调试日志 | [app/tasks/debug_log.h](file:///d:/20_AI/LSPi/app/tasks/debug_log.h) | | 字体生成脚本 | [regen_font_new.py](file:///d:/20_AI/LSPi/regen_font_new.py) | ## 12. 界面逻辑 1. 当前重量数值跟随着称称重变化 2. 总包数数值跟包装好的包数变化 3. 状态灯颜色跟运行状态变化 4. 状态文字跟运行状态变化 5. 当前工序标签跟当前工序变化 6. 增加按键点击时,设计重量会增加1g,最大值90g 7. 减小按键点击时,设计重量会减少1g,最小值1g 8. 复位按键点击时,发出机器复位的指令,所有工序停止,并且各种结构归零复位。 9. 运行按键点击时,开始运行,并开始进行包装。并且启动按键变成红色,文字变成停止,状态灯变绿,文字变为运行中,再次点击时,按键变为绿色言,文字变为启动,状态灯变红,文字变为已停止。