Flipper Zero JavaScript SDK 开发指南:基于 @flipperdevices/fz-sdk 构建与运行 Flipper Zero JS 应用
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
导读
本文以 Flipper Zero 固件仓库中的官方 JS SDK 开发包(@flipperdevices/fz-sdk)为线索,系统讲解如何用交互式脚手架创建 Flipper Zero JavaScript 应用、理解其构建上传流水线与配置文件,并掌握 SDK 版本兼容检查机制和内置 API。读完本文,你将能够独立完成一个 Flipper Zero JS 应用的初始化、编译、烧录运行与兼容性验证,并理解其底层实现原理。
一、@flipperdevices/fz-sdk 是什么
@flipperdevices/fz-sdk是 Flipper Zero 官方为 JavaScript 应用开发提供的工具与类型声明包(Type declarations and documentation for native JS modules available on Flipper Zero)。它位于仓库的 applications/system/js_app/packages/fz-sdk 目录下,主要包含三部分能力:
- TypeScript 类型声明(typings):以
*.d.ts文件形式描述 Flipper Zero 固件内置(native)JS 模块的完整 API 表面,供开发者在 PC 上获得代码补全与类型检查; - CLI 工具(sdk.js):提供
build与upload两个命令,负责把 TypeScript 源码编译为 Flipper Zero 上 mJS 引擎可执行的脚本,并通过串口上传到设备; - 文档生成配置:基于 TypeDoc 将类型声明中的 JSDoc 注释渲染为 API 文档。
从 package.json 可以看到,该包依赖esbuild(打包压缩)、esbuild-plugin-tsc(TypeScript 编译)、typedoc(文档生成)、serialport(串口上传)等工具链组件,版本为1.0.0,License 为 GPL-3.0-only。
它对应的"解释器实现"位于仓库固件侧:applications/system/js_app目录下的js_app.c、js_thread.c以及modules/中各原生模块(js_gui.c、js_gpio.c、js_storage.c等),这些 C 模块就是 JS API 在固件端的实际落地实现。
二、快速开始:交互式脚手架与一键运行
2.1 创建应用
官方推荐的起步方式是使用交互式向导创建应用骨架:
npx @flipperdevices/create-fz-app@latest运行后向导会在当前目录生成一个 Flipper Zero JS 应用工程(默认目录名如my-flip-app)。脚手架的模板文件位于仓库 applications/system/js_app/packages/create-fz-app/template 下,包含:
index.ts:应用入口源码,演示了事件循环 + GUI 视图的基本用法;package.json:定义build/start脚本与依赖;tsconfig.json:TypeScript 编译配置;fz-sdk.config.json5:SDK 构建与上传配置。
2.2 构建并运行
cd my-flip-app npm startnpm start会依次执行模板 package.json 中定义的两条脚本:
"build": "tsc && node node_modules/@flipperdevices/fz-sdk/sdk.js build", "start": "npm run build && node node_modules/@flipperdevices/fz-sdk/sdk.js upload"即:先用tsc把 TypeScript 编译到dist/,再调用sdk.js的build命令完成 mJS 兼容转译,最后通过sdk.js upload将产物经串口上传到 Flipper Zero 并立即运行。官方文档同时说明,你完全可以使用pnpm或yarn替代npm,流程不变。
2.3 向导生成的入口模板解读
模板 index.ts 本身就是一个最小可运行的 Flipper Zero JS 应用,值得逐段拆解:
// 注意导入顺序:eventLoop 必须先于 gui 导入,gui 必须先于任何 gui 子模块导入 import * as eventLoop from "@flipperdevices/fz-sdk/event_loop"; import * as gui from "@flipperdevices/fz-sdk/gui"; import * as dialog from "@flipperdevices/fz-sdk/gui/dialog"; const views = { dialog: dialog.makeWith({ header: "Hello from <app_name>", text: "Check out index.ts and\nchange something :)", center: "Gonna do that!", }), }; // 按下中间键退出应用 eventLoop.subscribe(views.dialog.input, (_sub, button, eventLoop) => { if (button === "center") eventLoop.stop(); }, eventLoop); // 按下返回键退出应用 eventLoop.subscribe(gui.viewDispatcher.navigation, (_sub, _item, eventLoop) => { eventLoop.stop(); }, eventLoop); // 切换到对话框视图并启动事件循环 gui.viewDispatcher.switchTo(views.dialog); eventLoop.run();这段代码集中体现了 Flipper Zero JS 编程的三个核心概念:事件循环(event_loop)、视图工厂(makeWith)与视图调度器(viewDispatcher),其 API 语义在类型声明 event_loop/index.d.ts 与 gui/index.d.ts 中有完整定义。
三、sdk.js 构建与上传流水线原理
sdk.js是 SDK 包的命令行入口(shebang#!/usr/bin/env node),支持build与upload两个子命令,从源码 sdk.js 可以还原其完整工作流程。
3.1 build:转译为 mJS 兼容子集
Flipper Zero 上的 mJS 引擎并不支持完整的现代 JavaScript 语法,因此build命令借助 esbuild 的supported选项显式禁用了一批语法特性,将其转译为 mJS 可执行的兼容代码。被禁用的特性包括(节选):
supported: { "array-spread": false, "arrow": false, "async-await": false, "class": false, "destructuring": false, "optional-chain": false, "template-literal": false, "for-of": false, // ... 完整清单见 sdk.js }同时@flipperdevices/fz-sdk/*被声明为external,意味着源码中的import * as gui from "@flipperdevices/fz-sdk/gui"会保留为运行时的require("gui")调用,与固件内建模块一一对应。
构建产物默认写入dist/index.js(由config.output决定),并会在文件头部拼接let exports = {};前缀以适配 mJS 的执行模型。
3.2 upload:串口自动发现与命令行协议
upload命令实现了从 PC 到设备的完整烧录运行链路:
- 设备发现:通过
SerialPort.list()枚举串口,优先匹配serialNumber以flip_开头的设备;若为空(部分 Windows 驱动不报告序列号),则回退为按 STM32 VCP 的 VID:PID(0483:5740)过滤。多台设备时会弹出交互选择; - 串口连接:以
230400波特率打开串口; - CLI 协议交互:依次向 Flipper Zero 的 CLI 发送
storage remove <output>(删除旧文件)、storage write_chunk <output> <size>(分块写入脚本)、js <output>(启动脚本执行),并以">: "、"Ready"、"Running"、"Script done!"等输出标记同步状态; - 退出清理:进程退出时发送
\x03中断脚本。
也就是说,npm start的"上传即运行"本质上是驱动了设备端 CLI 的存储与 JS 命令,相关 CLI 实现在固件侧可对应到applications/services/cli与applications/system/js_app/js_app.c。
四、fz-sdk.config.json5 配置详解
构建与上传的行为由工程根目录下的fz-sdk.config.json5控制。模板默认配置如下(完整模板):
{ build: { // 编译产物路径 output: "dist/<app_name>.js", // 是否压缩代码(以可读性和错误信息清晰度为代价减小体积) minify: false, // 设为 false 可关闭自动插入的 SDK 版本检查(见下文第五、六节) enforceSdkVersion: true, }, upload: { // 上传源文件;若无额外后处理,应与 build.output 一致 input: "dist/<app_name>.js", // 设备端存放路径,默认是脚本应用目录 output: "/ext/apps/Scripts/<app_name>.js", }, }各配置项说明:
| 配置项 | 作用 | 注意事项 |
|---|---|---|
build.output | 编译产物输出路径 | 通常放在dist/下 |
build.minify | 是否启用 esbuild 压缩 | 开启后体积更小,但报错信息更难读 |
build.enforceSdkVersion | 是否自动插入checkSdkCompatibility调用 | 默认true;关闭需自行做版本检查 |
upload.input | 上传的源文件 | 无后处理时应等于build.output |
upload.output | 设备端保存路径 | 默认/ext/apps/Scripts/目录,与设备上"脚本"应用的浏览入口一致 |
五、版本兼容机制:SDK 版本与额外特性集
这是 SDK 设计中最关键的部分,原文档(README 的 Versioning 一节)给出了明确的版本语义:
- 版本对齐:
@flipperdevices/fz-sdk每个发布版本的主版本号(major)和次版本号(minor)与它面向的 Flipper Zero JS SDK 版本一致,并遵循 semver 语义。例如,用 SDK 版本0.1.0编译的应用,与0.1…1.0(不含1.0)之间的 JS SDK 版本兼容。 - 破坏性变更规则:major 版本在引入破坏性变更时递增(即需要开发者修改应用的变更),minor 版本在引入新的非破坏性特性时递增。由于官方已采用 TypeScript 类型声明,是否属于破坏性变更依据 semver-ts 标准判定,其核心是"no new red squiggles"(编译期不产生新的类型错误)。
- 每个 API 的版本史:类型声明中每个 API 的 JSDoc 注释都记录了其引入/修改的版本,例如 global.d.ts 中大量出现
@version Added in JS SDK 0.1、@version Added in JS SDK 0.2, extra feature "gui-widget"、@version Baseline since JS SDK 1.0等标记。
此外,SDK 定义了额外特性集(extra feature set)概念:每个 major 版本对应一组在部分固件发行版中存在、但并未进入上游的"额外特性"。各发行版之间可以互相移植这些特性;当某个特性被移植进上游固件后,在下一次 JS SDK major 版本发布时,它会被声明为基线特性(baseline feature),不再被视为"额外特性"。当前仓库中的 JS SDK 即为 v1.0(global.d.ts的 JSDoc 中明确标注 "You're looking at JS SDK v1.0")。
结论是:在使用任何特性之前,必须先检查运行脚本的解释器是否支持它,否则应用的跨固件可移植性将大打折扣。
六、兼容性检查 API 详解
针对不同的使用场景,global.d.ts 提供了五个兼容性检查函数(均为全局可用,无需导入):
| 函数 | 签名 | 用途 |
|---|---|---|
sdkCompatibilityStatus | (expectedMajor, expectedMinor) => "compatible" \| "firmwareTooOld" \| "firmwareTooNew" | 需要详细的兼容性状态时使用 |
isSdkCompatible | (expectedMajor, expectedMinor) => boolean | 需要布尔形式判断时使用 |
checkSdkCompatibility | (expectedMajor, expectedMinor) => void \| never | 脚本绝对无法在不兼容的解释器上运行时使用;不兼容时会询问用户是否继续 |
doesSdkSupport | (features: string[]) => boolean | 查询指定额外特性是否被支持(布尔形式) |
checkSdkFeatures | (features: string[]) => void \| never | 同上,但不支持时询问用户是否继续运行 |
其中sdkCompatibilityStatus的三态语义为:
"compatible":脚本与固件 JS SDK 兼容;"firmwareTooOld":期望的 major 大于固件版本,或期望的 minor 大于固件版本;"firmwareTooNew":期望的 major 低于固件版本。
doesSdkSupport/checkSdkFeatures有一个易踩的坑(类型声明中以@warning明确标注):如果被查询的特性如今已被认定为基线特性,函数会返回false(或视为未实现)。因此查询时应只针对"额外特性"列表,基线特性无需也不能通过它们检查。
自动版本强制
在enforceSdkVersion: true(默认)时,build过程会在产物开头自动插入一行checkSdkCompatibility(<major>, <minor>);(见 sdk.js),其中 major/minor 取自@flipperdevices/fz-sdk包自身的package.json版本。如果你已通读文档、确定能用上文的手动检查 API 获得更好控制,可在fz-sdk.config.json5中将该选项设为false。
补充说明:原 README 的 Versioning 一节提到可组合使用
sdkCompatibilityStatus、isSdkCompatible与assertSdkCompatibility三个函数;而在当前仓库的类型声明中,第三个函数的实际名称为checkSdkCompatibility(语义一致:检查失败时询问用户是否继续),并以doesSdkSupport/checkSdkFeatures补充了特性级检查。使用时应以 global.d.ts 的实际声明为准。
七、全局内置 API(global.d.ts 一览)
SDK 除模块化 API 外,还提供了一批全局可用的内置函数与类型,均在 global.d.ts 中声明:
- 基础函数:
delay(ms)(暂停执行)、print(...args)(输出到 GUI 控制台视图)、parseInt(text, base?)(转数字,base 支持 2…16,默认 10)、chr(n)(ASCII 码转单字符,越界返回null)、require(module)(加载原生模块)、load(path, scope?)(加载并执行另一个 JS 文件,结果按会话缓存)、die(message)(携带错误信息退出); - 环境变量:
__dirname(当前脚本所在目录)、__filename(当前脚本文件路径); - 控制台:
console.log / debug / warn / error,分别输出到 UART 日志的[I]/[D]/[W]/[E]级别; - 二进制类型:
ArrayBuffer(含getPtr、byteLength、slice)、RawPointer(不透明指针类型,JS 侧只能持有并原样回传)、Uint8Array/Int8Array/Uint16Array/Int16Array/Uint32Array/Int32Array及ElementType联合类型; - 内建对象子集:
Array(splice/push/length)、String(charCodeAt/at/indexOf/slice/toUpperCase/toLowerCase)、Number.toString(base)等。
标准库的大部分特性尚未实现,该模块只声明了确实实现的部分(JSDoc 中明确说明 "Standard library features are mostly unimplemented")。编写代码时请以本文件声明为唯一依据,不要依赖未声明的 ECMAScript 标准库行为。
八、原生模块与 TypeScript 类型声明
@flipperdevices/fz-sdk的*.d.ts覆盖了固件侧全部原生 JS 模块,与applications/system/js_app/modules/下的 C 实现一一对应:
| SDK 模块(类型声明目录) | 固件侧 C 实现 |
|---|---|
event_loop | js_event_loop.c |
gui及 15 个子模块(dialog、submenu、widget、text_input、number_input、byte_input、file_picker、loading、menu、popup、empty_screen、button_menu、button_panel、vi_list、text_box、icon) | js_gui.c 及各视图 C 文件 |
gpio | js_gpio.c |
storage | js_storage.c |
serial | js_serial.c |
math | js_math.c |
notification | js_notification.c |
badusb | js_badusb.c |
flipper | js_flipper.c |
tests | js_tests.c |
在仓库自带的示例脚本 applications/system/js_app/examples/apps/Scripts 中(gui.js、storage.js、gpio.js、event_loop.js、uart_echo.js、notify.js、math.js、badusb_demo.js等),可以看到这些模块在真实脚本中的用法。
8.1 GUI 体系:View、ViewFactory 与 ViewDispatcher
gui/index.d.ts 系统性地描述了 Flipper Zero 的 GUI 抽象层次:
- Canvas:纯绘图区域,没有抽象层;
- Viewport:指向画布矩形区域的窗口,应用总是通过 viewport 访问画布;
- View:占据整个 viewport 并接管所有输入事件的"全屏设计元素",即 Flipper 术语中的视图。JS 适配器已覆盖
button_menu、button_panel、byte_input、dialog(对应dialog_ex)、empty_screen、file_picker(对应file_browser)、loading、menu、number_input、popup、submenu、text_box、text_input、vi_list(对应variable_item_list)、widget共 15 种视图; - ViewDispatcher:持有应用所需的所有视图并在请求间切换,提供
switchTo、sendCustom、sendTo以及navigation/custom事件源; - SceneManager:视图调度器的可选附加组件,用于复杂导航流程管理,当前版本在 JS 中不可用。
每种视图通过ViewFactory提供make()(默认属性创建)与makeWith(props)(自定义初始属性创建)两个工厂方法,且属性可在创建后用view.set(name, value)修改。GUI 依赖event_loop模块,因此必须先导入event_loop,再导入gui,再导入gui子模块——这一约束在模板index.ts的注释和类型声明中均有明确说明。
8.2 事件循环模型
event_loop/index.d.ts 说明了一个重要事实:Flipper Zero 的 mJS 子系统不支持闭包,因此subscribe(callback, ...extraArgs)允许把外部值作为额外参数传给回调;若回调返回一个与额外参数个数相同的数组,则下次触发时使用新值。示例(定时器):
let timer = eventLoop.timer("periodic", 1000); eventLoop.subscribe(timer, function(_sub, _item, counter, eventLoop) { print("Counter is at:", counter); if(counter === 10) eventLoop.stop(); return [counter + 1, eventLoop]; // 修改下次回调收到的额外参数 }, 0, eventLoop);回调的前两个参数固定为订阅管理器(可cancel())和事件项(无数据的定时器事件为undefined)。
8.3 存储 API 要点
storage/index.d.ts 提供了File/FileInfo/FsInfo等类,以及两组重要枚举:
- 访问模式
AccessMode:"r"(只读)、"w"(只写)、"rw"(读写); - 创建模式
OpenMode:"open_existing"(不存在即失败)、"open_always"(不存在则创建空文件)、"open_append"(打开并把读写指针置到文件末尾,不存在则创建)、"create_new"(已存在即失败)、"create_always"(截断并打开,不存在则创建空文件)。
FileInfo返回path(stat返回完整路径,readDirectory返回文件名)、isDirectory、size与accessTime(UNIX 时间戳)。
九、开发工作流与最佳实践
综合以上内容,一个完整的 Flipper Zero JS 应用开发周期是:
- 初始化:
npx @flipperdevices/create-fz-app@latest生成工程骨架; - 编码:在
index.ts(或按需拆分的多个 TS 文件)中编写逻辑,依赖tsc的类型检查(模板 tsconfig.json 开启checkJs、noLib并显式包含global.d.ts,以保证全局 API 可用); - 构建:
npm run build调用tsc与sdk.js build,产出 mJS 兼容的压缩脚本; - 运行:
npm start自动执行构建 + 串口上传 + 启动,在设备上观察输出(Running…Script done!标记); - 兼容性:保持默认的
enforceSdkVersion: true,或按第六节手动调用sdkCompatibilityStatus/isSdkCompatible/doesSdkSupport做精细控制;关注每个 API JSDoc 中的@version历史以决定脚本可运行的固件范围。
最佳实践要点:在脚本无法运行于不兼容解释器时使用checkSdkCompatibility/checkSdkFeatures(自动询问用户);在脚本能利用多版本能力时使用isSdkCompatible/doesSdkSupport;需要详细状态时使用sdkCompatibilityStatus。谨记"先检查、再使用"的原则,这能确保你的应用在各类 Flipper Zero 固件发行版之间保持可移植性。
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考