第12章:LVGL Pro CLI 工作流(XML→C)

设计一次,处处生成——把 XML 界面描述"编译"成 C 代码

🔨

本章导师:鲁班

核心方法论:工欲善其事,必先利其器

「手艺人的功夫不在'做',而在'备'——工具趁手,活儿就成了一半。写界面也一样:与其在 C 里一行行摆控件,不如先把界面描述成一张 XML 图纸,再让工具把图纸变成代码。看懂这把'鲁班的刨子',你就从手工时代进入了流水线时代。」

12.1 鲁班的工具箱:认识 lved-cli

打开 README 的「LVGL Pro」一节,官方把设计工具链拆成四件套:Editor(桌面可视化编辑器,所见即所得)、Online Viewer(浏览器里跑编辑器)、Figma Plugin(Figma 设计一键搬进 LVGL Pro)、以及CLI Tool——在 CI/CD 里生成 C 代码、跑 UI 测试的那一件。四件共享同一种 XML 格式。本章主角就是最后这件 CLI,在工作区里对应 LVGL_Pro_CLI-2.0.2-rc1-darwin/ 这个目录。

先拆开这个"工具箱"看看里面有什么。真实目录里只有三样东西:lved-cli.js 是把整套 CLI 打包进单个文件的 Node.js 程序(体积约 18 MB);lvgl-resources.zip(约 16 MB)是生成代码时需要用到的 LVGL 资源包;bin/resvgjs.darwin-arm64.node 是一个原生扩展模块——文件名里的 resvg 是著名的 SVG 栅格化引擎,而 darwin-arm64 直接点明了它的宿主:macOS 的 Apple Silicon。这也是整套工具"按平台分发"的最直接证据。

# lvgl-sample 工作区里的 CLI 工具(真实文件)
LVGL_Pro_CLI-2.0.2-rc1-darwin/
├── lved-cli.js                  # 打包为单文件的 Node CLI(约 18 MB)
├── lvgl-resources.zip            # LVGL 资源包(约 16 MB)
└── bin/
    └── resvgjs.darwin-arm64.node   # resvg 原生扩展(SVG 栅格化,macOS arm64)

运行方式是把子命令参数交给 Node:node lved-cli.js <子命令>。先问一句"它到底能干什么"——执行 node lved-cli.js --help,顶部用法行写着 Usage: lved [options] [command],下面是 7 个顶层命令。逐一读一遍,几乎就是一张完整的"XML→C 流水线"地图:generate 负责翻译,compile 负责编译,validate 负责静态校验,screenshot 负责出图,run-testrun-all-tests 负责 UI 交互回归,compare 负责与参考示例比对生成的代码。

$ node lved-cli.js --help
Usage: lved [options] [command]

LVGL Pro Editor CLI tool

Options:
  -V, --version                                                 output the version number
  -h, --help                                                    display help for command

Commands:
  generate [options] <project-path>                             Generate code from XML files
  compile [options] <project-path>                              Compile LVGL project
  compare [options] <first-project-path> <second-project-path>  Compare generated code with reference examples
  validate [options] <project-path>                             Validate LVGL code
  run-test [options] <project-path> <testing-file>              Run UI interaction tests
  run-all-tests [options] <project-path>                        Run all UI interaction tests
  screenshot [options] <project-path> <screen>                  Take a screenshot of the selected screen
  help [command]                                                display help for command
命令作用关键参数
generate从 XML 工程生成 C 代码--token / --ignore-fonts / --ignore-images
compile编译 LVGL 工程--target editor|cli / --skip-resources
validate静态校验 LVGL 代码-l, --errorlimit
screenshot给指定屏幕截图--out / --delay
run-test运行单个 UI 交互测试--slowdown
run-all-tests运行全部 UI 交互测试
compare与参考示例比对生成代码
鲁班提示

这套 CLI 的用法不必死记——它自带了"说明书":顶层 --help 看有哪些命令,help [command]<command> --help 看某个命令的参数。写脚本之前先跑一次 --help,就是最快的"反编译"。本章所有命令与参数的说法,都以你本地跑出来的真实输出为准。

12.2 一张"图纸":XML 工程结构

CLI 吃的不是单个文件,而是一个工程目录。仓库里现成的样例就在 lvgl-master/examples/xml_project/,里面恰好四样东西:project.xml(工程与目标设备)、globals.xml(全局数据)、images/(图像素材)、fonts/(字体文件)。把它理解成一张"施工图":项目信息画在 project.xml,全局材料清单记在 globals.xml,素材单独放目录。

先看 project.xml。它很短——声明这份 XML 面向的 LVGL 版本(lvgl_version="9.5.0"),以及一块 320×240 的显示:

<!-- examples/xml_project/project.xml(真实源码,全文) -->
<project lvgl_version="9.5.0">
    <targets>
        <target name="target1">
            <display width="320" height="240" />
        </target>
    </targets>
</project>

注意 lvgl_version="9.5.0" 这个细节:它描述的是这份 XML 面向的版本目标,而仓库里真正编译进固件的运行时是 9.6.0-dev(见第 1 章)。两者相差一个小版本——可以把它理解为"XML 的目标 SDK 版本",与最终链接的运行时版本是两回事,通常不影响生成代码的编译。CLI 工具自身的版本号则从启动横幅里读到:LVGL CLI v2.0.2-rc1

globals.xml 才是"材料清单"的主体,内部按 api / consts / styles / subjects / images / fonts 六块组织。前四块和第 10、11 章都见过:<subjects> 声明可观察数据(subject_value 等),<images><data> 声明图像(带 src_pathcolor_format="argb8888"),<fonts><bin> 声明二进制字体。下面这一段的注释格外有价值——它说图像 img_example_lvgl_logo 的名字特意与 demos/widgets/assets/img_example_lvgl_logo.c 里预置的描述符一致,生成代码直接链接这个既有符号,因此样例不必依赖编辑器的图像转换管线。

<!-- globals.xml:subjects / images / fonts 三块(真实源码,节选) -->
<subjects>
    <int    name="subject_value"  value="50"  min_value="0" max_value="100"/>
    <int    name="subject_opa"    value="128" min_value="0" max_value="255"/>
    <string name="subject_text"  value="Hello"/>
</subjects>

<images>
    <data name="img_example_lvgl_logo" src_path="images/img_example_lvgl_logo.png"
          color_format="argb8888"/>
</images>

<fonts>
    <bin name="font_example_large" src_path="fonts/Montserrat-Medium.ttf"
         size="32" bpp="4" as_file="false"/>
</fonts>

一个值得品味的属性是字体标签里的 as_file="false":它表示转换后的二进制字体内嵌进生成的 C 数组,而不是落地成一个外部 .bin 文件。这决定了你拿到生成代码后要不要多带一个文件——是"代码即资源"还是"资源随行"。图像那边 color_format="argb8888" 则是输出像素格式,与第 10 章讲的图像解码一一对应。

注意

不要把 project.xml 里的 lvgl_version="9.5.0" 和仓库运行时版本 9.6.0-dev 混为一谈:前者是这份 XML 面向的版本目标,后者是 lvgl-master/ 源码的实际版本。生成出来的 C 代码最终链接的是后者。

12.3 许可与 lved generate:XML 到 C 的翻译

核心命令是 generate。跑 node lved-cli.js generate --help,真实输出显示它接收一个必填参数 <project-path>(工程目录),并有四个选项:--token(许可令牌,说明文字写明"Can also be set with LVGL_CLI_TOKEN")、--ignore-fonts(跳过字体转换)、--ignore-images(跳过图像转换)、--skip-resources(跳过 LVGL resources 的拷贝与校验,默认 false)。

$ node lved-cli.js generate --help
Usage: lved generate [options] <project-path>

Generate code from XML files

Arguments:
  project-path             Path to the project directory

Options:
  --token <license-token>  License token for running CLI. Can also be set with
                           LVGL_CLI_TOKEN.
  --ignore-fonts           Skip font conversion during code generation
  --ignore-images          Skip image conversion during code generation
  --skip-resources         Skip copying and checking LVGL resources during setup
                           (default: false)
  -h, --help               display help for command

--token 背后是一道"许可门禁":LVGL Pro 不是无条件使用的。README「License」一节说得很清楚——Community 与 Evaluation 两个级别对非商业用途免费(学习、爱好、评估工具都算),商业使用则需要付费许可。我在没有 token 的情况下实际跑过一次 generate,程序先打出一幅 ASCII 艺术横幅,然后直截了当地拒绝:

$ node lved-cli.js generate /tmp/xml_project_test   # 未提供 token
# (先打印 ASCII 艺术横幅,已省略)
LVGL CLI v2.0.2-rc1

[ERROR] No token provided. Use --token <license-token> or set LVGL_CLI_TOKEN to run the CLI.

于是使用流程就被拆成了两半:要么每次命令行传 --token <token>,要么把 token 写进环境变量 LVGL_CLI_TOKEN(更推荐,理由见下面的提示)。token 验过后,generate 才会真正开工:读 project.xmlglobals.xml,把 <subjects> 变成 lv_subject_t 与初始化调用,把 <images>/<fonts> 变成描述符与字体资源,把屏幕上的控件树变成一串 lv_*_create()——也就是第 11 章末尾预告过的"XML 与 C 是同一副面孔"。

两个"跳过"选项值得单独解释。--ignore-fonts / --ignore-images 分别跳过字体与图像的转换环节——如果你的工程暂时不想带资源、只想先把控件骨架跑起来,或者在 CI 里已经有预转换好的资源、不想每次重复转换,就加上它们。--skip-resources 则跳过 12.1 里那个 lvgl-resources.zip 的拷贝与校验环节——本地已经初始化过资源、只想增量生成时,省掉这步能省不少时间。

鲁班提示

命令行的 --token 会出现在你的 shell 历史、进程列表和 CI 日志里,等于把许可密钥到处撒。在 CI/CD 里请优先用环境变量:在流水线配置里设置 LVGL_CLI_TOKEN,脚本里只写 lved generate <dir>。README 那句 "Can also be set with LVGL_CLI_TOKEN" 就是给自动化场景留的口子。

12.4 编译、校验、截图与测试:四条"验收"命令

生成只是第一步,工程要"过验收"还得靠另外几条命令。compile 负责把工程编译起来,真实参数里有一个值得一提的 --target <target>:可取值 editorcli(默认 cli),也就是"按编辑器环境编译"还是"按命令行环境编译"。它在调试"编辑器里能跑、CLI 里报错"这类环境差异时很有用,还附带 --skip-resources

$ node lved-cli.js compile --help
Usage: lved compile [options] <project-path>

Compile LVGL project

Arguments:
  project-path             Path to the project directory

Options:
  --target <target>        Set the target platform for compiling (editor or cli)
                           (default: "cli")
  --token <license-token>  License token for running CLI. Can also be set with
                           LVGL_CLI_TOKEN.
  --skip-resources         Skip copying and checking LVGL resources during setup
                           (default: false)
  -h, --help               display help for command

validate 做静态校验,-l, --errorlimit <amount> 控制最多显示多少条错误——这个参数意味着它可能一次性吐出一长串诊断,限制条数是为了不刷屏。screenshot 更有意思:它给指定的屏幕截图,第二个参数 <screen> 是"相对工程目录的屏幕路径"——因为一个工程可以有多个屏幕,必须指明截哪一块;--out 指定输出文件名,--delay 指定截图前等多少毫秒(等界面"安定"下来再截)。

$ node lved-cli.js screenshot --help
Usage: lved screenshot [options] <project-path> <screen>

Take a screenshot of the selected screen

Arguments:
  project-path             Path to the project directory
  screen                   The relative path of the screen to capture screenshot

Options:
  --token <license-token>  License token for running CLI. Can also be set with
                           LVGL_CLI_TOKEN.
  --out <out-file>         File name for the screenshot
  --delay <delay>          Delay in milliseconds before taking the screenshot
                           (default: 0)
  -h, --help               display help for command

交互回归则由 run-testrun-all-tests 承接。run-test <project-path> <testing-file> 运行单个 UI 交互测试文件(相对工程目录),--slowdown <number> 可以人为放慢执行速度(0 为最快),方便在测试太快、肉眼跟不上时调试;run-all-tests 则是把全部交互测试跑一遍。此外 validate 的 help 明确给出 -l, --errorlimitcompare 的 help 则要求两个工程路径(生成代码与参考示例各一)。

$ node lved-cli.js validate --help
Usage: lved validate [options] <project-path>

Validate LVGL code

Arguments:
  project-path               Path to the project directory

Options:
  --token <license-token>    License token for running CLI. Can also be set with
                             LVGL_CLI_TOKEN.
  -l, --errorlimit <amount>  Maximum errors to show
  -h, --help                 display help for command

$ node lved-cli.js run-test --help
Usage: lved run-test [options] <project-path> <testing-file>

Run UI interaction tests

Arguments:
  project-path             Path to the project directory
  testing-file             Path to the testing file relative to the project
                           directory

Options:
  --token <license-token>  License token for running CLI. Can also be set with
                           LVGL_CLI_TOKEN.
  --slowdown <number>      Slow down test execution (0 = fastest) (default: 0)
  -h, --help               display help for command
鲁班提示

把这几条命令串起来,一个"XML→C 的自动化产线"就成型了:改 XML → generate 重新生成 → validate 查语法 → screenshot 出图留档 → run-test 回归交互。尤其 screenshot——它让"界面长什么样"也能进入自动化,视觉回归的门槛一下就被拉下来了。

12.5 工作流串联:从设计到编译进 LVGL

把前面几节拼成一条完整的流水线。① 在 Editor 里可视化搭屏幕(或直接手写 XML),得到一个目录:project.xml + globals.xml + images/ + fonts/;② 用 generate 把它"翻译"成纯 C 代码——注意 README 的原话:Pro "exports plain LVGL C code: the same LVGL you already use, with no extra runtime or dependency",也就是说生成产物和你手写的代码没有任何区别,不会夹带私货;③ 把生成的 C 加进你的编译目标,与 lvgl-master 一起编译、烧录。

# ① 把许可放进环境变量,避免写进 shell 历史 / CI 日志
export LVGL_CLI_TOKEN="你的 token"

# ② 用仓库自带的 XML 样例工程,生成 C 代码
node lved-cli.js generate ../lvgl-master/examples/xml_project

# ③ 编译 → 截图 → 跑全部交互测试
node lved-cli.js compile     ../lvgl-master/examples/xml_project
node lved-cli.js screenshot   ../lvgl-master/examples/xml_project <屏幕相对路径> --out preview.png
node lved-cli.js run-all-tests ../lvgl-master/examples/xml_project

生成代码"长什么样",其实第 11 章已经剧透过了:<subjects> 变成 lv_subject_t + lv_subject_init_*()<images>/<fonts> 变成描述符与字体资源,屏幕上的控件树变成一串 lv_*_create()。再对照 12.2 里 globals.xml 那段真实注释——"生成代码直接链接 img_example_lvgl_logo 这个既有符号"——就能确定:生成代码是普通 LVGL 代码,可以自由地塞进任意已有工程。

/* globals.xml 里 <int name="subject_value" value="50"
       min_value="0" max_value="100"/> 在生成代码里的落点 */
lv_subject_t subject_value;
lv_subject_init_int(&subject_value, 50);

/* <data name="img_example_lvgl_logo" color_format="argb8888"/>
   生成/引用一个图像描述符(样例特意与 demos 预置符号同名) */
LV_IMAGE_DECLARE(img_example_lvgl_logo);

/* 屏幕上的控件树则是一串 lv_*_create() —— 与手写无差别 */
lv_obj_t * btn = lv_button_create(screen);

生成出来的入口怎么接?一般是这样:把生成的 .c/.h 加入编译,#include 对应头文件,在 lv_init()、创建好显示与输入设备之后,调用生成代码暴露的初始化入口(具体函数名以你实际生成的头文件为准),屏幕对象树上就会出现设计好的整棵控件树。自此,设计工具里改一处,重跑一次 generate,界面就"翻译"成了新的 C——不需要你手敲一个像素。

鲁班提示

"生成代码 = 手写代码"是一把双刃剑,也是它的全部价值:既然没差别,你可以随时手改生成的 C 来救急、做只此一次的微调;代价是下次重新 generate 会覆盖手改。所以纪律很重要——能写进 XML 的,就别动生成的 C。把 XML 当成唯一事实来源,生成的代码当"构建产物"。

章末练习

练习 1:通读帮助,画出命令地图 入门

在工作区里实际运行 node lved-cli.js --help,列出全部 7 个子命令,并为每个子命令写一句"什么时候用它"。接着运行 node lved-cli.js generate --help,写出它的 4 个选项。

提示

顶层命令里,翻译、编译、静态校验、截图、交互测试各有其责;compare 需要两个工程路径,generate 的 4 个选项里有 3 个是"跳过/免做"类。

参考答案

7 个子命令:generate(XML→C 生成)、compile(编译工程)、compare(与参考示例比对生成代码)、validate(静态校验)、run-test(运行单个 UI 交互测试)、run-all-tests(运行全部交互测试)、screenshot(给指定屏幕截图)。generate 的 4 个选项:--token(许可,亦可 LVGL_CLI_TOKEN)、--ignore-fonts(跳过字体转换)、--ignore-images(跳过图像转换)、--skip-resources(跳过资源拷贝与校验)。

练习 2:按需求改一张"图纸" 进阶

复制 examples/xml_project/ 为一个新工程目录,然后把 project.xml 里的显示改成 480×272,并新增一个 target2(分辨率 128×64)。说明:<targets><target><display width height> 三层各表达什么含义?

提示

XML 是树形结构:工程下可挂多个 target,每个 target 声明一块显示。对照 12.2 的真实 project.xml 全文作答。

参考答案

三者是"工程 → 目标 → 屏幕"的树:<project> 是根(带 lvgl_version 目标版本),<targets> 容纳多个目标,<target name="..."> 是一个编译目标,其下的 <display width="480" height="272"/> 声明这块屏的尺寸。改法:复制工程目录后,把 width/height 改成 480/272,再在同一 <targets> 下追加一个 <target name="target2"><display width="128" height="64"/></target>。多 target 意味着一次生成可产出适配多种分辨率屏的代码。

练习 3:读懂三个"跳过"选项 进阶

结合 globals.xml<bin>/<data> 标签与 12.1 的目录内容,解释 --ignore-fonts--ignore-images--skip-resources 各自跳过的是什么环节,并说出"已经在 CI 里预转换好资源、只想增量生成"时应该用哪几个。

提示

<bin> 把 TTF 转成二进制字体、<data> 把 PNG 转成图像描述符;lvgl-resources.zip 是 12.1 里那个约 16 MB 的资源包。

参考答案

--ignore-fonts 跳过字体转换(<bin name="font_example_large" .../> 的 TTF→二进制字体那一步);--ignore-images 跳过图像转换(<data color_format="argb8888"/> 的 PNG→描述符那一步);--skip-resources 跳过 lvgl-resources.zip 的拷贝与校验。增量生成且资源已就绪时,三个都可以加上;只想省时间重跑时至少加 --skip-resources。注意若你的界面真的引用了字体/图像,跳过转换可能让生成代码缺资源,需按实际工程权衡。

练习 4:许可的设计哲学 挑战

CLI 用 --tokenLVGL_CLI_TOKEN 两种方式接受同一份许可。请分析:为什么同时提供两种?各适合什么场景?结合 README 的 License 说明(Community/Evaluation 免费、商业付费),讨论"没有 token 就拒绝执行"这种门禁设计对开发者与厂商分别意味着什么。

提示

想三个点:命令行参数会进 shell 历史与进程列表;环境变量可以由 CI 系统安全注入;门禁让"免费试用"和"商业付费"边界清晰,但也会让第一次上手多一道坎。

参考答案

两种方式服务两种场景:--token 适合临时、本地、一次性使用,直观但会出现在 shell 历史、进程参数与日志里,有泄露风险;LVGL_CLI_TOKEN 适合 CI/CD——由流水线安全注入、不进脚本源码、也不出现在命令行里,README 明确两者等价("Can also be set with LVGL_CLI_TOKEN")。门禁设计上,token 是"免费与付费之间的闸机":无 token 时报 [ERROR] No token provided 并拒绝执行,保障了厂商的商业边界;对开发者则是"先注册再使用"的体验成本。这也解释了为什么教程建议本地实验先申请 Community 许可、把 token 放进环境变量——低成本迈过这道坎,就能用上整条流水线。