拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Flipper Zero JS SDK 的 event_loop 模块详解:回调式编程、定时器与消息队列事件循环

Flipper Zero JS SDK 的 event_loop 模块详解回调式编程、定时器与消息队列事件循环【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware本篇技术指南围绕 Flipper Zero 固件中 JS 子系统的event_loop模块展开。读完本文你将理解该模块显式事件循环的设计动机、掌握subscribe/timer/queue等 API 的完整用法与参数细节并能结合固件源码看懂 JavaScript 回调是如何被 mJS 引擎驱动、如何与底层FuriEventLoop对接的从而为编写 GPIO、GUI 等高级模块的 JS 应用打下基础。为什么 JS 子系统需要显式的事件循环事件循环是多数框架中事件驱动编程的核心。Flipper Zero 的 JS 子系统也不例外——官方文档明确建议在使用 GPIO、GUI 等高级模块之前先熟悉事件循环模块。模块通过标准导入方式引入let eventLoop require(event_loop);如果你写过 JavaScript一定见过回调callback一个函数把另一个函数通常是匿名函数当作参数并在稍后——例如事件发生时或数据就绪时——调用它例如setTimeout(function() { console.log(Hello, World!) }, 1000);许多 JavaScript 引擎内部维护一个事件队列运行时从中取出发生的事件随后调用对应的回调函数。这一切都在一个长期运行的循环中完成因此得名事件循环event loop。典型事件循环的伪代码如下while(loop_is_running()) { if(event_available_in_queue()) { let event fetch_event_from_queue(); let callback get_callback_associated_with(event); if(callback) callback(get_extra_data_for(event)); } else { // avoid wasting CPU time sleep_until_any_event_becomes_available(); } }大多数 JS 运行时把事件循环封装在引擎内部开发者几乎无感。但 Flipper Zero 的 JS 子系统不同事件循环是暴露在 API 上的显式对象需要你自己run()和stop()。这一差异的根源在于固件使用的 mJS 引擎不支持闭包——匿名函数无法访问其参数之外的外部变量。因此模块用额外参数模式替代了闭包这一点在后面的示例中会详细展开。完整示例从 setTimeout 到 run/stop下面这段代码就是上文setTimeout示例在 Flipper Zero JS 子系统里的等价写法与 原文档 一致可直接运行// import module let eventLoop require(event_loop); // create an event source that will fire once 1 second after it has been created let timer eventLoop.timer(oneshot, 1000); // subscribe a callback to the event source eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) { print(Hello, World!); eventLoop.stop(); }, eventLoop); // notice this extra argument. well come back to this later // run the loop until it is stopped eventLoop.run(); // the previous line will only finish executing once .stop() is called, hence // the following line will execute only after Hello, World! is printed print(Stopped);逐行解读eventLoop.timer(oneshot, 1000)创建一个事件源创建后 1 秒触发一次oneshot表示单次触发。eventLoop.subscribe(timer, callback, eventLoop)把回调挂到这个事件源上。注意第三个参数eventLoop本身——这是额外参数会被原样传给回调。eventLoop.run()阻塞运行事件循环直到有人调用stop()。回调执行eventLoop.stop()后run()返回最后一行print(Stopped)才会在Hello, World!打印之后执行。无闭包环境下的状态保持额外参数模式固件使用的 mJS 引擎不支持闭包回调函数无法捕获外层的counter、eventLoop等变量。解决办法是让subscribe携带额外参数作为跨回调的状态载体并允许回调返回一个数组来修改这些参数供下一次调用使用// this timer will fire every second let timer eventLoop.timer(periodic, 1000); eventLoop.subscribe(timer, function(_subscription, _item, counter, eventLoop) { print(Counter is at:, counter); if(counter 10) eventLoop.stop(); // modify the extra arguments that will be passed to us the next time return [counter 1, eventLoop]; }, 0, eventLoop);这里有两个额外参数初始值0和eventLoop。因为额外参数个数为 2如果回调返回的不是长度为 2 的数组参数将保持原值进入下一次调用只有返回长度匹配的数组才会被替换。仓库自带的 示例脚本 还展示了一个只用一个额外参数的变体并用subscription.cancel()单独停掉某个定时器而不终止整个循环// count up to 5 with a delay of 100ms between increments eventLoop.subscribe(eventLoop.timer(periodic, 100), function (subscription, _item, counter) { print(Counter two:, counter); if (counter 5) subscription.cancel(); return [counter 1]; }, 0);回调函数的完整签名每次事件触发时回调收到的参数依次为SubscriptionManager订阅管理器可通过.cancel()取消该订阅事件 item携带额外数据的事件会传入具体数据定时器事件没有额外数据恒为undefined你传给subscribe的额外参数个数与传入时一致回调可返回等长数组来更新它们。SDK 的 TypeScript 类型定义event_loop/index.d.ts把这个签名形式化为export type CallbackItem, Args extends Lit[] (subscription: Subscription, item: Item, ...args: Args) Args | undefined | void;API 参考run()export function run(): void | never;运行事件循环直到被stop()停止。注意它是阻塞调用脚本主线程会停在run()这一行期间只有已订阅的回调能执行run()返回后后续代码才会继续。stop()export function stop(): void;停止事件循环。通常在某个回调内部调用用于结束整个脚本脚本正常退出后JS 线程结束资源统一释放。subscribe()export function subscribeItem, Args extends Lit[]( contract: ContractItem, callback: CallbackItem, Args, ...args: Args ): Subscription;把函数订阅到某个事件源上。参数contract事件源标识由timer()、queue()返回的Contract对象或其他模块提供的事件源callback事件发生时调用的函数...args额外参数会追加在系统参数之后传给回调。回调至少收到 2 个参数订阅管理器、事件 item再加上你传入的额外参数。回调可以返回一个与额外参数个数相同的数组来更新状态其他返回值一律被丢弃。返回值一个Subscription对象SubscriptionManagerSubscription.cancel()取消订阅之后的事件不再触发该回调。警告每个事件源只允许挂一个回调one callback per event source。timer()export function timer(mode: oneshot | periodic, interval: number): Contract;产生一个以固定间隔触发的事件源可像其他事件一样被订阅。参数modeoneshot单次触发或periodic周期触发intervaloneshot下的超时时间 /periodic下的周期单位为毫秒。返回值一个Contract对象即subscribe第一个参数所期望的事件源标识。从源码看interval会被校验必须为正数否则抛出 timer interval must be a positive number 参数错误见 js_event_loop.c随后通过furi_ms_to_ticks()把毫秒换算成系统 tickjs_event_loop.c最终交给底层FuriEventLoopTimer。queue()export function queueT(length: number): QueueT;创建一个可用于交换消息的队列。参数length队列允许容纳的最大条目数。返回值一个Queue对象Queue.send(message)把任意类型的message追加到队尾Queue.input一个Contract事件源订阅它即可在有新消息到达时从队头取出条目item 参数就是消息本身。一个典型的队列用法let eventLoop require(event_loop); let q eventLoop.queue(4); eventLoop.subscribe(q.input, function(subscription, message) { print(got:, message); }); q.send(ping); q.send({ n: 42 }); eventLoop.run();源码级剖析回调、契约与底层事件循环JS 回调的桥接机制JS 模块的核心实现在 js_event_loop.c。文件顶部定义了SYSTEM_ARGS 2表示回调中前两个系统参数订阅管理器与事件 item不能被修改js_event_loop.c。真正的事件处理函数js_event_loop_callback_generic做了三件事js_event_loop.c通过mjs_apply以保存的实参数组调用 JS 回调若回调抛出异常、或检测到 JS 线程收到ThreadEventStop标志例如用户按键退出脚本就调用furi_event_loop_stop()停止循环检查返回值只有当返回值是长度等于额外参数个数的数组时才逐个用mjs_array_get取出新值替换保存的实参供下一次回调使用——这正是文档中返回等长数组才能修改状态描述的底层实现。subscribe()的实现js_event_loop.c根据契约contract的object_type分三条路径Timer调用furi_event_loop_timer_alloc()创建底层定时器并furi_event_loop_timer_start()回调直接是js_event_loop_callback_genericSemaphore走furi_event_loop_subscribe_semaphore()事件触发时默认行为是先furi_semaphore_acquire()消费信号量见 js_event_loop.cQueue走furi_event_loop_subscribe_message_queue()由契约自带的 transformer 把队列中的 JS 值取出传给回调。每个订阅都会被登记到模块内部的SubscriptionArray中脚本退出时由js_event_loop_destroy()统一反订阅并释放保证资源不泄漏。契约Contract其他模块如何接入事件循环js_event_loop.h 是整个 JS 模块体系的集成接口。它定义了JsEventLoopContract结构js_event_loop.h其他 JS 模块如 GPIO、GUI通过返回一个指向该结构的 foreign 指针值即 JS 侧的Contract把自己的事件源交给event_loop模块统一调度。契约分两类定时器契约object_type JsEventLoopObjectTypeTimer时timer字段有效包含定时器类型和 tick 间隔非定时器契约non_timer.event指定订阅的事件方向如队列的FuriEventLoopEventInnon_timer.transformer是一个 C 函数指针负责把底层对象转换成传给 JS 回调的 item 值若为 NULL事件循环会按默认行为处理如取信号量/互斥锁。头文件注释还说明了其他模块接入的两条路径js_event_loop.h一是通过契约让 JS 程序员把事件源传给event_loop适合用户关心的事件二是模块创建时直接通过js_event_loop_get_loop()拿到FuriEventLoop实例自行操作适合幕后事件。两种情况下对象的创建、反订阅与释放都由模块自己负责。以队列模块自身为例queue(length)就是用furi_message_queue_alloc(length, sizeof(mjs_val_t*))建一个存 JS 值指针的消息队列并绑定js_event_loop_queue_transformer作为转换器js_event_loop.c。底层 FuriEventLoopsleep_until 的真相JS 模块的run()最终落到 furi_event_loop_run()。其主循环与文档中的伪代码一一对应while(true) { const TickType_t ticks_to_sleep MIN(furi_event_loop_get_timer_wait_time(instance), furi_event_loop_get_tick_wait_time(instance)); uint32_t flags 0; BaseType_t ret xTaskNotifyWaitIndexed( FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, 0, FuriEventLoopFlagAll, flags, ticks_to_sleep); // ... 按 Stop/Event/Timer/Pending/ThreadFlag 标志位分发处理 }这里xTaskNotifyWaitIndexed就是伪代码里的sleep_until_any_event_becomes_available()线程睡眠到下一个定时器到期或下一个 tick 到期为止任何被订阅对象消息队列、流缓冲、信号量等状态变化时会通过furi_event_loop_notify()以 task notify 位唤醒它furi_event_loop_notify 还区分了任务上下文与中断上下文两种通知方式。事件对象被唤醒后进入 waiting list 逐个处理level-triggered 事件若处理后条件仍成立比如队列里还有剩余消息会把 item 重新放回等待列表等下一轮继续触发见 furi_event_loop_process_level_event。需要注意 event_loop.h 中的硬性约束每个线程只允许一个FuriEventLoop实例且所有调用必须来自创建它的同一线程。JS 侧的 JS 线程由 js_thread.c 以 8KB 栈创建脚本文件通过mjs_exec_file在该线程内执行事件循环实例随require(event_loop)在该线程中创建因此天然满足单线程单循环的要求。定时器部分由 event_loop_timer.h 定义furi_event_loop_timer_alloc()创建定时器furi_event_loop_timer_start()以 tick 为单位的间隔启动。JS 的oneshot/periodic模式分别映射为FuriEventLoopTimerTypeOnce/FuriEventLoopTimerTypePeriodic见 js_event_loop.c 中的枚举变体表。一个容易忽略的细节JS 回调中调用subscription.cancel()对定时器来说是一种延迟操作——由于定时器操作会被推迟处理cancel()只是停止定时器相关内存要等脚本结束时由清理例程统一释放js_event_loop.c。这与取消后立即释放的直觉不同但对外行为一致取消后回调不再触发。实战要点小结显式驱动JS 脚本是单线程顺序执行的run()之前完成所有subscribe脚本会停在run()直到stop()无闭包所有跨回调共享的状态计数、循环句柄等都要作为额外参数传入并在回调中返回等长数组更新返回其他值状态不变单回调约束一个事件源只能挂一个回调重复订阅同一事件源会违反底层约束两种停止粒度subscription.cancel()只停掉某个事件源如某个定时器eventLoop.stop()终止整个循环、结束脚本定时器参数interval单位是毫秒且必须为正数内部换算为系统 tick队列即消息总线queue(input)的 item 参数就是send()发出去的值适合在多个订阅者之间做轻量解耦通信。结合 官方文档、JS SDK 类型定义、示例脚本 与 模块实现你可以从timer的简单延时场景入手逐步过渡到多定时器、队列组合的复杂脚本再进一步理解 GPIO/GUI 等高级模块如何复用同一套事件循环契约体系。【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门