Flipper Zero JS 应用开发:GUI 图标模块(gui/icon)中 getBuiltin 与 loadFxbm 的用法及底层实现解析
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
本文围绕 Flipper Zero 固件 JS 应用框架中的gui/icon模块(模块名gui__icon),讲解如何通过getBuiltin()获取固件内置图标、通过loadFxbm()从 SD 卡加载.fxbm精灵图标,并解析两种图标在 C 侧的实现、.fxbm文件格式、IconData返回类型以及 Widget/Menu/ButtonPanel 等视图对图标的消费方式。读完后你可以在自己的 JS 应用中正确加载并摆放图标,并理解其内存生命周期。
模块定位与加载方式
gui/icon是 JS SDK GUI 子模块之一,其职责按官方文档 js_gui__icon.md 的概括是:获取并加载图标,供 Dialog 等 GUI 视图使用(参见 js_gui__dialog.md)。
图标本身不是"视图",而是一种可被视图引用的图形资源:它不直接出现在屏幕上,而是作为 Widget 元素、Menu 条目、ButtonPanel 按钮等组件的属性传入。模块在 JS 侧的加载顺序遵循 GUI 模块的依赖约定(先event_loop,再gui,最后具体子模块):
let eventLoop = require("event_loop"); let gui = require("gui"); let icon = require("gui/icon");仓库中完整的实战示例脚本是 gui.js,其中大量演示了图标在各视图中的用法(详见后文"典型使用示例"一节)。
API 一:getBuiltin(icon) —— 获取固件内置图标
参数与返回值
- 参数
icon:图标名称(字符串) - 返回:一个
IconData对象 - 行为:按名称查找固件内嵌的位图资源并返回其句柄;找不到时抛出错误。
TypeScript 声明见 icon.d.ts:
export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px" | "off_19x20" | "off_hover_19x20" | "power_19x20" | "power_hover_19x20" | "Settings_14"; export declare function getBuiltin(icon: BuiltinIcon): IconData;当前可用的内置图标清单
文档中提到"目前仅DolphinWait_59x54和js_script_10px可用",而从源码结构看,当前固件支持的内置图标实际上已经扩充到 7 个。C 侧实现 icon.c 中的注册表为:
| 图标名称 | 尺寸/类型 | 用途示例 |
|---|---|---|
DolphinWait_59x54 | 静态位图 | 等待画面的海豚动画 |
js_script_10px | 10px 小图标 | JS 脚本标识 |
off_19x20/off_hover_19x20 | 19x20 位图 | 电源键"关闭"常态/高亮态 |
power_19x20/power_hover_19x20 | 19x20 位图 | 电源键"开启"常态/高亮态 |
Settings_14 | 动画图标 | 设置齿轮旋转动画 |
注意两点实现细节:
- 静态图标通过
ICON_DEF宏展开为固件assets中编译进去的I_<name>符号,而Settings_14通过ANIM_ICON_DEF宏映射到A_<name>符号——它是多帧动画图标,这也是 gui.js 中它能直接用作 Menu 图标并呈现旋转效果的原因。 - 查找是简单的线性
strcmp遍历(icon.c#L61-L68),名称不存在时返回MJS_BAD_ARGS_ERROR,错误信息为no such built-in icon。
返回值通过mjs_mk_foreign包装成 JS "foreign" 对象,内部实际是一个指向 CIcon结构体指针——即IconData本质上是一个不透明符号(nominal type),只能传给接受它的视图 API,不能直接操作其像素。
API 二:loadFxbm(path) —— 从文件加载 .fxbm 图标
参数与返回值
- 参数
path:.fxbm文件路径(相对或绝对路径,如"/ext/demo.fxbm") - 返回:
IconData对象 - 生命周期:文档明确说明"脚本退出时会自动卸载",这一行为在源码中有直接对应(详见"生命周期与内存管理"一节)。
.fxbm全称为 "XBM Flipper sprite",格式源自flipperzero-game-engine项目。从 icon.c 的解析逻辑可以确认其文件布局:
struct { uint32_t size; // 紧随其后的数据总大小(含 width/height 两个字段的值) uint32_t width; // 位图宽度(像素) uint32_t height; // 位图高度(像素) } fxbm_header; // 之后是 frame_size = size - 8 字节的未压缩位图像素数据即:4 字节总大小、4 字节宽、4 字节高,其余全部是单帧未压缩的 1-bit 位图数据。加载函数会依次校验:文件能否以只读方式打开 → 头部 12 字节是否完整读出 → 剩余像素数据是否完整读出,任一环节失败都会报could not load .fxbm icon。
解析成功后,函数将位图包装为固件 GUI 子系统可直接渲染的Icon结构(见 icon_i.h:width、height、frame_count、frame_rate、frames),其中frame_count固定为 1、frame_rate为 1,即.fxbm只承载单帧静态精灵。
IconData 与固件 Icon 结构的衔接
C 侧有一个精巧的内存布局:固件的Icon结构要求一个 frames 数组,JS 侧的实现用FxbmIconWrapper在一次malloc中把Icon、frames 指针数组和像素数据放在一起(icon.c#L36-L43),并借助CompressHeader的首字节is_compressed = false标记告知 GUI 渲染层该帧是未压缩数据。这样做的好处是整块内存可一次性free,避免多处分配/释放。
JS 类型定义上,IconData声明为symbol & { "__tag__": "icon" }(icon.d.ts#L6-L7),注释坦承这是"用 hack 方式引入名义类型"——__tag__属性并不真实存在,仅用于让 TypeScript 在编译期区分图标与其他值。
典型使用示例:图标在视图中的三种消费方式
以下示例均摘自仓库示例脚本 gui.js,覆盖了当前 SDK 中三种接收IconData的组件形态。
1. Widget 视图的 icon 元素
Widget 是自定义布局的画布,{ element: "icon" }元素通过iconData字段接收图标,支持绝对坐标定位:
let cuteDolphinWithWatch = icon.getBuiltin("DolphinWait_59x54"); let jsLogo = icon.getBuiltin("js_script_10px"); let stopwatchWidgetElements = [ { element: "string", x: 67, y: 44, align: "bl", font: "big_numbers", text: "00 00" }, { element: "rect", x: 64, y: 27, w: 28, h: 20, radius: 3, fill: false }, { element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch }, { element: "icon", x: 64, y: 13, iconData: jsLogo }, { element: "button", button: "right", text: "Back" }, ]; let stopwatchWidget = widget.makeWith({}, stopwatchWidgetElements);类型约束见 widget.d.ts#L45:IconElement = { element: "icon", iconData: IconData } & Position。C 侧对应处理在 widget.c#L203-L211:取出iconData字段、校验其为 foreign 对象后,解出const Icon*指针交给widget_add_icon_element渲染。注意iconData字段名与 Menu/ButtonPanel 中的icon不同。
2. Menu 视图的条目图标
Menu 条目由"图标 + 文本"组成(menu.d.ts#L32:Child = { icon: IconData, label: string }),示例中用Settings_14动画图标:
let settingsIcon = icon.getBuiltin("Settings_14"); let menu = menuView.makeWith({}, [ { label: "One", icon: settingsIcon }, { label: "Two", icon: settingsIcon }, { label: "three", icon: settingsIcon }, ]);3. ButtonPanel 视图的按钮图标(常态/高亮两态)
ButtonPanel 的按钮元素支持icon(常态)与iconSelected(高亮/选中态)两个图标,这对"off/power"内置图标正是为此设计的(button_panel.d.ts#L38):
let offIcons = [icon.getBuiltin("off_19x20"), icon.getBuiltin("off_hover_19x20")]; let powerIcons = [icon.getBuiltin("power_19x20"), icon.getBuiltin("power_hover_19x20")]; let buttonPanel = buttonPanelView.makeWith({ matrixSizeX: 2, matrixSizeY: 2, }, [ { type: "button", x: 0, y: 0, matrixX: 0, matrixY: 0, icon: offIcons[0], iconSelected: offIcons[1] }, { type: "button", x: 30, y: 30, matrixX: 1, matrixY: 1, icon: powerIcons[0], iconSelected: powerIcons[1] }, ]);除上述三类外,Dialog 等视图同样可以接收IconData(原文档即以 Dialog 为主要消费场景之一,对应类型声明位于 fz-sdk/gui 目录)。
生命周期与内存管理(源码级)
模块以插件(plugin)形式挂载,描述符名称为gui__icon(icon.c#L145-L150)。创建实例时初始化一个FxbmIconWrapperList(基于 mlib 的链表),所有loadFxbm分配的FxbmIconWrapper都会push_back进该列表(icon.c#L117-L119)。
脚本退出(模块销毁)时,js_gui_icon_destroy遍历链表逐个free并清理(icon.c#L135-L143)。这正是文档所说".fxbm图标会在脚本退出时自动卸载"的实现保障:开发者无需手动释放loadFxbm的返回值,但也意味着图标数据只在本次脚本运行期间有效,不应跨脚本复用。
而getBuiltin返回的图标指向固件内置资源区(assets编译产物),不占用该列表,无释放负担。
版本与可用性说明
根据类型声明中的@version标注(icon.d.ts):
getBuiltin:JS SDK 0.2 引入,需额外特性"gui-widget";自 JS SDK 1.0 起为基线功能(无需额外声明特性)。loadFxbm:JS SDK 0.3 引入,需额外特性"gui-widget-extras";自 JS SDK 1.0 起为基线功能。- 内置图标集合会随固件版本扩充,本文所列 7 个名称以当前仓库 icon.c#L22-L30 注册表为准;使用时若名称拼写错误或固件版本不含该图标,会收到
no such built-in icon错误。
小结
gui/icon模块用两个 API 覆盖了 JS 应用的图标需求:getBuiltin零成本获取固件内置位图/动画图标(线性查找注册表,失败即抛错),loadFxbm读取 12 字节头部 + 单帧 1-bit 位图的.fxbm精灵文件并自动管理其生命周期。两者都返回不透明的IconData,可直接作为 Widgeticon元素(iconData字段)、Menu 条目或 ButtonPanel 按钮(icon/iconSelected字段)的图标源。仓库内最完整的调用示范见 gui.js,实现细节则以 icon.c 为权威参考。
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考