mirror of
https://github.com/flipperdevices/flipperzero-firmware.git
synced 2025-12-12 04:41:26 +04:00
[FL-3925, FL-3942, FL-3944] JS features & bugfixes (SDK 0.2) (#4075)
* feat: JS GPIO PWM, JS GUI Widget view; fix: JS EvtLoop stop on request, JS EvtLoop stop on error * fix: f18 build * docs: widget * fix: js unit test * change feature naming Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
@@ -12,4 +12,4 @@ tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"]));
|
||||
|
||||
tests.assert_eq("flipperdevices", flipper.firmwareVendor);
|
||||
tests.assert_eq(0, flipper.jsSdkVersion[0]);
|
||||
tests.assert_eq(1, flipper.jsSdkVersion[1]);
|
||||
tests.assert_eq(2, flipper.jsSdkVersion[1]);
|
||||
|
||||
@@ -110,6 +110,23 @@ App(
|
||||
fap_libs=["assets"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__widget",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_view_widget_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/widget.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_gui__icon",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="js_gui_icon_ep",
|
||||
requires=["js_app"],
|
||||
sources=["modules/js_gui/icon.c"],
|
||||
fap_libs=["assets"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="js_notification",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
|
||||
@@ -3,6 +3,7 @@ let gpio = require("gpio");
|
||||
|
||||
// initialize pins
|
||||
let led = gpio.get("pc3"); // same as `gpio.get(7)`
|
||||
let led2 = gpio.get("pa7"); // same as `gpio.get(2)`
|
||||
let pot = gpio.get("pc0"); // same as `gpio.get(16)`
|
||||
let button = gpio.get("pc1"); // same as `gpio.get(15)`
|
||||
led.init({ direction: "out", outMode: "push_pull" });
|
||||
@@ -16,6 +17,13 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led,
|
||||
return [led, !state];
|
||||
}, led, true);
|
||||
|
||||
// cycle led pwm
|
||||
print("Commencing PWM (PA7)");
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 10), function (_, _item, led2, state) {
|
||||
led2.pwmWrite(10000, state);
|
||||
return [led2, (state + 1) % 101];
|
||||
}, led2, 0);
|
||||
|
||||
// read potentiometer when button is pressed
|
||||
print("Press the button (PC1)");
|
||||
eventLoop.subscribe(button.interrupt(), function (_, _item, pot) {
|
||||
|
||||
@@ -9,8 +9,23 @@ let byteInputView = require("gui/byte_input");
|
||||
let textBoxView = require("gui/text_box");
|
||||
let dialogView = require("gui/dialog");
|
||||
let filePicker = require("gui/file_picker");
|
||||
let widget = require("gui/widget");
|
||||
let icon = require("gui/icon");
|
||||
let flipper = require("flipper");
|
||||
|
||||
// declare clock widget children
|
||||
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: "string", x: 77, y: 22, align: "bl", font: "primary", text: "Stopwatch" },
|
||||
{ element: "frame", x: 64, y: 27, w: 28, h: 20, radius: 3 },
|
||||
{ element: "frame", x: 100, y: 27, w: 28, h: 20, radius: 3 },
|
||||
{ element: "icon", x: 0, y: 5, iconData: cuteDolphinWithWatch },
|
||||
{ element: "icon", x: 64, y: 13, iconData: jsLogo },
|
||||
{ element: "button", button: "right", text: "Back" },
|
||||
];
|
||||
|
||||
// declare view instances
|
||||
let views = {
|
||||
loading: loadingView.make(),
|
||||
@@ -31,6 +46,7 @@ let views = {
|
||||
longText: textBoxView.makeWith({
|
||||
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
|
||||
}),
|
||||
stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements),
|
||||
demos: submenuView.makeWith({
|
||||
header: "Choose a demo",
|
||||
items: [
|
||||
@@ -40,6 +56,7 @@ let views = {
|
||||
"Byte input",
|
||||
"Text box",
|
||||
"File picker",
|
||||
"Widget",
|
||||
"Exit app",
|
||||
],
|
||||
}),
|
||||
@@ -72,6 +89,8 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
|
||||
views.helloDialog.set("center", "Nice!");
|
||||
gui.viewDispatcher.switchTo(views.helloDialog);
|
||||
} else if (index === 6) {
|
||||
gui.viewDispatcher.switchTo(views.stopwatchWidget);
|
||||
} else if (index === 7) {
|
||||
eventLoop.stop();
|
||||
}
|
||||
}, gui, eventLoop, views);
|
||||
@@ -111,6 +130,31 @@ eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views, eventLoop);
|
||||
|
||||
// go to the demo chooser screen when the right key is pressed on the widget screen
|
||||
eventLoop.subscribe(views.stopwatchWidget.button, function (_sub, buttonId, gui, views) {
|
||||
if (buttonId === "right")
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
}, gui, views);
|
||||
|
||||
// count time
|
||||
eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, views, stopwatchWidgetElements, halfSeconds) {
|
||||
let text = (halfSeconds / 2 / 60).toString();
|
||||
if (halfSeconds < 10 * 60 * 2)
|
||||
text = "0" + text;
|
||||
|
||||
text += (halfSeconds % 2 === 0) ? ":" : " ";
|
||||
|
||||
if (((halfSeconds / 2) % 60) < 10)
|
||||
text += "0";
|
||||
text += ((halfSeconds / 2) % 60).toString();
|
||||
|
||||
stopwatchWidgetElements[0].text = text;
|
||||
views.stopwatchWidget.setChildren(stopwatchWidgetElements);
|
||||
|
||||
halfSeconds++;
|
||||
return [views, stopwatchWidgetElements, halfSeconds];
|
||||
}, views, stopwatchWidgetElements, 0);
|
||||
|
||||
// run UI
|
||||
gui.viewDispatcher.switchTo(views.demos);
|
||||
eventLoop.run();
|
||||
|
||||
@@ -267,6 +267,8 @@ void js_check_sdk_compatibility(struct mjs* mjs) {
|
||||
|
||||
static const char* extra_features[] = {
|
||||
"baseline", // dummy "feature"
|
||||
"gpio-pwm",
|
||||
"gui-widget",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
#define JS_SDK_VENDOR "flipperdevices"
|
||||
#define JS_SDK_MAJOR 0
|
||||
#define JS_SDK_MINOR 1
|
||||
#define JS_SDK_MINOR 2
|
||||
|
||||
/**
|
||||
* @brief Returns the foreign pointer in `obj["_"]`
|
||||
@@ -254,6 +254,18 @@ static inline void
|
||||
return; \
|
||||
} while(0)
|
||||
|
||||
/**
|
||||
* @brief Prepends an error, sets the JS return value to `undefined` and returns
|
||||
* a value C function
|
||||
* @warning This macro executes `return;` by design
|
||||
*/
|
||||
#define JS_ERROR_AND_RETURN_VAL(mjs, error_code, ret_val, ...) \
|
||||
do { \
|
||||
mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \
|
||||
mjs_return(mjs, MJS_UNDEFINED); \
|
||||
return ret_val; \
|
||||
} while(0)
|
||||
|
||||
typedef struct JsModules JsModules;
|
||||
|
||||
typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
|
||||
@@ -92,7 +92,7 @@ static void js_console_debug(struct mjs* mjs) {
|
||||
}
|
||||
|
||||
static void js_exit_flag_poll(struct mjs* mjs) {
|
||||
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny, 0);
|
||||
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
|
||||
if(flags & FuriFlagError) {
|
||||
return;
|
||||
}
|
||||
@@ -102,7 +102,8 @@ static void js_exit_flag_poll(struct mjs* mjs) {
|
||||
}
|
||||
|
||||
bool js_delay_with_flags(struct mjs* mjs, uint32_t time) {
|
||||
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny, time);
|
||||
uint32_t flags =
|
||||
furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, time);
|
||||
if(flags & FuriFlagError) {
|
||||
return false;
|
||||
}
|
||||
@@ -124,7 +125,7 @@ uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags_mask, uint32_t timeout) {
|
||||
uint32_t flags = furi_thread_flags_get();
|
||||
furi_check((flags & FuriFlagError) == 0);
|
||||
if(flags == 0) {
|
||||
flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny, timeout);
|
||||
flags = furi_thread_flags_wait(flags_mask, FuriFlagWaitAny | FuriFlagNoClear, timeout);
|
||||
} else {
|
||||
uint32_t state = furi_thread_flags_clear(flags & flags_mask);
|
||||
furi_check((state & FuriFlagError) == 0);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* @brief Context passed to the generic event callback
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* event_loop;
|
||||
JsEventLoopObjectType object_type;
|
||||
|
||||
struct mjs* mjs;
|
||||
@@ -36,11 +37,6 @@ typedef struct {
|
||||
void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition
|
||||
} JsEventLoopSubscription;
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
struct mjs* mjs;
|
||||
} JsEventLoopTickContext;
|
||||
|
||||
ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575
|
||||
ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575
|
||||
|
||||
@@ -51,7 +47,6 @@ struct JsEventLoop {
|
||||
FuriEventLoop* loop;
|
||||
SubscriptionArray_t subscriptions;
|
||||
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
|
||||
JsEventLoopTickContext* tick_context;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -60,7 +55,7 @@ struct JsEventLoop {
|
||||
static void js_event_loop_callback_generic(void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
mjs_val_t result;
|
||||
mjs_apply(
|
||||
mjs_err_t error = mjs_apply(
|
||||
context->mjs,
|
||||
&result,
|
||||
context->callback,
|
||||
@@ -68,6 +63,12 @@ static void js_event_loop_callback_generic(void* param) {
|
||||
context->arity,
|
||||
context->arguments);
|
||||
|
||||
bool is_error = strcmp(mjs_strerror(context->mjs, error), "NO_ERROR") != 0;
|
||||
bool asked_to_stop = js_flags_wait(context->mjs, ThreadEventStop, 0) & ThreadEventStop;
|
||||
if(is_error || asked_to_stop) {
|
||||
furi_event_loop_stop(context->event_loop);
|
||||
}
|
||||
|
||||
// save returned args for next call
|
||||
if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return;
|
||||
for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) {
|
||||
@@ -111,11 +112,14 @@ static void js_event_loop_subscription_cancel(struct mjs* mjs) {
|
||||
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
|
||||
// timer operations are deferred, which creates lifetime issues
|
||||
// just stop the timer and let the cleanup routine free everything when the script is done
|
||||
furi_event_loop_timer_stop(subscription->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
|
||||
return;
|
||||
}
|
||||
|
||||
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
|
||||
|
||||
free(subscription->context->arguments);
|
||||
free(subscription->context);
|
||||
|
||||
@@ -158,6 +162,7 @@ static void js_event_loop_subscribe(struct mjs* mjs) {
|
||||
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
|
||||
|
||||
// create callback context
|
||||
context->event_loop = module->loop;
|
||||
context->object_type = contract->object_type;
|
||||
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
|
||||
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
|
||||
@@ -333,37 +338,22 @@ static void js_event_loop_queue(struct mjs* mjs) {
|
||||
mjs_return(mjs, queue);
|
||||
}
|
||||
|
||||
static void js_event_loop_tick(void* param) {
|
||||
JsEventLoopTickContext* context = param;
|
||||
uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0);
|
||||
if(flags & FuriFlagError) {
|
||||
return;
|
||||
}
|
||||
if(flags & ThreadEventStop) {
|
||||
furi_event_loop_stop(context->loop);
|
||||
mjs_exit(context->mjs);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t event_loop_obj = mjs_mk_object(mjs);
|
||||
JsEventLoop* module = malloc(sizeof(JsEventLoop));
|
||||
JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext));
|
||||
module->loop = furi_event_loop_alloc();
|
||||
tick_ctx->loop = module->loop;
|
||||
tick_ctx->mjs = mjs;
|
||||
module->tick_context = tick_ctx;
|
||||
furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx);
|
||||
SubscriptionArray_init(module->subscriptions);
|
||||
ContractArray_init(module->owned_contracts);
|
||||
|
||||
mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||
mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe));
|
||||
mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run));
|
||||
mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop));
|
||||
mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer));
|
||||
mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue));
|
||||
JS_ASSIGN_MULTI(mjs, event_loop_obj) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
|
||||
JS_FIELD("subscribe", MJS_MK_FN(js_event_loop_subscribe));
|
||||
JS_FIELD("run", MJS_MK_FN(js_event_loop_run));
|
||||
JS_FIELD("stop", MJS_MK_FN(js_event_loop_stop));
|
||||
JS_FIELD("timer", MJS_MK_FN(js_event_loop_timer));
|
||||
JS_FIELD("queue", MJS_MK_FN(js_event_loop_queue));
|
||||
}
|
||||
|
||||
*object = event_loop_obj;
|
||||
return module;
|
||||
@@ -418,7 +408,6 @@ static void js_event_loop_destroy(void* inst) {
|
||||
ContractArray_clear(module->owned_contracts);
|
||||
|
||||
furi_event_loop_free(module->loop);
|
||||
free(module->tick_context);
|
||||
free(module);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_event_loop/js_event_loop.h"
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_pwm.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <expansion/expansion.h>
|
||||
#include <limits.h>
|
||||
@@ -17,6 +18,7 @@ typedef struct {
|
||||
FuriSemaphore* interrupt_semaphore;
|
||||
JsEventLoopContract* interrupt_contract;
|
||||
FuriHalAdcChannel adc_channel;
|
||||
FuriHalPwmOutputId pwm_output;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioPinInst;
|
||||
|
||||
@@ -231,6 +233,88 @@ static void js_gpio_read_analog(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines whether this pin supports PWM
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* assert_eq(true, gpio.get("pa4").isPwmSupported());
|
||||
* assert_eq(false, gpio.get("pa5").isPwmSupported());
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_is_pwm_supported(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, manager_data->pwm_output != FuriHalPwmOutputIdNone));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets PWM parameters and starts the PWM
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let pa4 = gpio.get("pa4");
|
||||
* pa4.pwmWrite(10000, 50);
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_pwm_write(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
int32_t frequency, duty;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&frequency), JS_ARG_INT32(&duty));
|
||||
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
|
||||
}
|
||||
|
||||
if(furi_hal_pwm_is_running(manager_data->pwm_output)) {
|
||||
furi_hal_pwm_set_params(manager_data->pwm_output, frequency, duty);
|
||||
} else {
|
||||
furi_hal_pwm_start(manager_data->pwm_output, frequency, duty);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines whether PWM is running
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* assert_eq(false, gpio.get("pa4").isPwmRunning());
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_is_pwm_running(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
if(manager_data->pwm_output == FuriHalPwmOutputIdNone) {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
|
||||
}
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, furi_hal_pwm_is_running(manager_data->pwm_output)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stops PWM
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let pa4 = gpio.get("pa4");
|
||||
* pa4.pwmWrite(10000, 50);
|
||||
* pa4.pwmStop();
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_pwm_stop(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
if(manager_data->pwm_output != FuriHalPwmOutputIdNone) {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "PWM is not supported on this pin");
|
||||
}
|
||||
|
||||
furi_hal_pwm_stop(manager_data->pwm_output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns an object that manages a specified pin.
|
||||
*
|
||||
@@ -269,12 +353,19 @@ static void js_gpio_get(struct mjs* mjs) {
|
||||
manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0);
|
||||
manager_data->adc_handle = module->adc_handle;
|
||||
manager_data->adc_channel = pin_record->channel;
|
||||
mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data));
|
||||
mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init));
|
||||
mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write));
|
||||
mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read));
|
||||
mjs_set(mjs, manager, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
|
||||
manager_data->pwm_output = pin_record->pwm_output;
|
||||
JS_ASSIGN_MULTI(mjs, manager) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, manager_data));
|
||||
JS_FIELD("init", MJS_MK_FN(js_gpio_init));
|
||||
JS_FIELD("write", MJS_MK_FN(js_gpio_write));
|
||||
JS_FIELD("read", MJS_MK_FN(js_gpio_read));
|
||||
JS_FIELD("readAnalog", MJS_MK_FN(js_gpio_read_analog));
|
||||
JS_FIELD("interrupt", MJS_MK_FN(js_gpio_interrupt));
|
||||
JS_FIELD("isPwmSupported", MJS_MK_FN(js_gpio_is_pwm_supported));
|
||||
JS_FIELD("pwmWrite", MJS_MK_FN(js_gpio_pwm_write));
|
||||
JS_FIELD("isPwmRunning", MJS_MK_FN(js_gpio_is_pwm_running));
|
||||
JS_FIELD("pwmStop", MJS_MK_FN(js_gpio_pwm_stop));
|
||||
}
|
||||
mjs_return(mjs, manager);
|
||||
|
||||
// remember pin
|
||||
|
||||
61
applications/system/js_app/modules/js_gui/icon.c
Normal file
61
applications/system/js_app/modules/js_gui/icon.c
Normal file
@@ -0,0 +1,61 @@
|
||||
#include "../../js_modules.h"
|
||||
#include <assets_icons.h>
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
const Icon* data;
|
||||
} IconDefinition;
|
||||
|
||||
#define ICON_DEF(icon) \
|
||||
(IconDefinition) { \
|
||||
.name = #icon, .data = &I_##icon \
|
||||
}
|
||||
|
||||
static const IconDefinition builtin_icons[] = {
|
||||
ICON_DEF(DolphinWait_59x54),
|
||||
ICON_DEF(js_script_10px),
|
||||
};
|
||||
|
||||
static void js_gui_icon_get_builtin(struct mjs* mjs) {
|
||||
const char* icon_name;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name));
|
||||
|
||||
for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) {
|
||||
if(strcmp(icon_name, builtin_icons[i].name) == 0) {
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, (void*)builtin_icons[i].data));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "no such built-in icon");
|
||||
}
|
||||
|
||||
static void* js_gui_icon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
*object = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, *object) {
|
||||
JS_FIELD("getBuiltin", MJS_MK_FN(js_gui_icon_get_builtin));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void js_gui_icon_destroy(void* inst) {
|
||||
UNUSED(inst);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_gui_icon_desc = {
|
||||
"gui__icon",
|
||||
js_gui_icon_create,
|
||||
js_gui_icon_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gui_icon_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gui_icon_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -247,6 +247,22 @@ static bool
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the list of children. Not available from JS.
|
||||
*/
|
||||
static bool
|
||||
js_gui_view_internal_set_children(struct mjs* mjs, mjs_val_t children, JsGuiViewData* data) {
|
||||
data->descriptor->reset_children(data->specific_view, data->custom_data);
|
||||
|
||||
for(size_t i = 0; i < mjs_array_length(mjs, children); i++) {
|
||||
mjs_val_t child = mjs_array_get(mjs, children, i);
|
||||
if(!data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.set`
|
||||
*/
|
||||
@@ -260,6 +276,46 @@ static void js_gui_view_set(struct mjs* mjs) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.addChild`
|
||||
*/
|
||||
static void js_gui_view_add_child(struct mjs* mjs) {
|
||||
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
|
||||
if(!data->descriptor->add_child || !data->descriptor->reset_children)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
|
||||
|
||||
mjs_val_t child;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&child));
|
||||
bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child);
|
||||
UNUSED(success);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.resetChildren`
|
||||
*/
|
||||
static void js_gui_view_reset_children(struct mjs* mjs) {
|
||||
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
|
||||
if(!data->descriptor->add_child || !data->descriptor->reset_children)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
|
||||
|
||||
data->descriptor->reset_children(data->specific_view, data->custom_data);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.setChildren`
|
||||
*/
|
||||
static void js_gui_view_set_children(struct mjs* mjs) {
|
||||
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
|
||||
if(!data->descriptor->add_child || !data->descriptor->reset_children)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
|
||||
|
||||
mjs_val_t children;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&children));
|
||||
js_gui_view_internal_set_children(mjs, children, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View` destructor
|
||||
*/
|
||||
@@ -283,7 +339,12 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr
|
||||
|
||||
// generic view API
|
||||
mjs_val_t view_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set));
|
||||
JS_ASSIGN_MULTI(mjs, view_obj) {
|
||||
JS_FIELD("set", MJS_MK_FN(js_gui_view_set));
|
||||
JS_FIELD("addChild", MJS_MK_FN(js_gui_view_add_child));
|
||||
JS_FIELD("resetChildren", MJS_MK_FN(js_gui_view_reset_children));
|
||||
JS_FIELD("setChildren", MJS_MK_FN(js_gui_view_set_children));
|
||||
}
|
||||
|
||||
// object data
|
||||
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
|
||||
@@ -314,7 +375,7 @@ static void js_gui_vf_make(struct mjs* mjs) {
|
||||
*/
|
||||
static void js_gui_vf_make_with(struct mjs* mjs) {
|
||||
mjs_val_t props;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props));
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_OBJ(&props));
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make the object like normal
|
||||
@@ -334,6 +395,18 @@ static void js_gui_vf_make_with(struct mjs* mjs) {
|
||||
}
|
||||
}
|
||||
|
||||
// assign children
|
||||
if(mjs_nargs(mjs) >= 2) {
|
||||
if(!data->descriptor->add_child || !data->descriptor->reset_children)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
|
||||
|
||||
mjs_val_t children = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_array(children))
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 1: expected array");
|
||||
|
||||
if(!js_gui_view_internal_set_children(mjs, children, data)) return;
|
||||
}
|
||||
|
||||
mjs_return(mjs, view_obj);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ typedef void (*JsViewFree)(void* specific_view);
|
||||
typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj);
|
||||
/** @brief Context destruction for glue code */
|
||||
typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop);
|
||||
/** @brief `addChild` callback for glue code */
|
||||
typedef bool (
|
||||
*JsViewAddChild)(struct mjs* mjs, void* specific_view, void* custom_state, mjs_val_t child_obj);
|
||||
/** @brief `resetChildren` callback for glue code */
|
||||
typedef void (*JsViewResetChildren)(void* specific_view, void* custom_state);
|
||||
|
||||
/**
|
||||
* @brief Descriptor for a JS view
|
||||
@@ -66,15 +71,22 @@ typedef struct {
|
||||
JsViewAlloc alloc;
|
||||
JsViewGetView get_view;
|
||||
JsViewFree free;
|
||||
|
||||
JsViewCustomMake custom_make; // <! May be NULL
|
||||
JsViewCustomDestroy custom_destroy; // <! May be NULL
|
||||
|
||||
JsViewAddChild add_child; // <! May be NULL
|
||||
JsViewResetChildren reset_children; // <! May be NULL
|
||||
|
||||
size_t prop_cnt; //<! Number of properties visible from JS
|
||||
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
|
||||
} JsViewDescriptor;
|
||||
|
||||
// Callback ordering:
|
||||
// alloc -> get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free
|
||||
// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/
|
||||
// +-> add_child -+
|
||||
// +-> reset_children -+
|
||||
// alloc -> get_view -> custom_make -+-> props[i].assign -+> custom_destroy -> free
|
||||
// \__________ creation __________/ \____ use ____/ \___ destruction ____/
|
||||
|
||||
/**
|
||||
* @brief Creates a JS `ViewFactory` object
|
||||
|
||||
281
applications/system/js_app/modules/js_gui/widget.c
Normal file
281
applications/system/js_app/modules/js_gui/widget.c
Normal file
@@ -0,0 +1,281 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/widget.h>
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsWidgetCtx;
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
/**
|
||||
* @brief Parses position (X and Y) from an element declaration object
|
||||
*/
|
||||
static bool element_get_position(struct mjs* mjs, mjs_val_t element, int32_t* x, int32_t* y) {
|
||||
mjs_val_t x_in = mjs_get(mjs, element, "x", ~0);
|
||||
mjs_val_t y_in = mjs_get(mjs, element, "y", ~0);
|
||||
if(!mjs_is_number(x_in) || !mjs_is_number(y_in)) return false;
|
||||
*x = mjs_get_int32(mjs, x_in);
|
||||
*y = mjs_get_int32(mjs, y_in);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parses size (W and h) from an element declaration object
|
||||
*/
|
||||
static bool element_get_size(struct mjs* mjs, mjs_val_t element, int32_t* w, int32_t* h) {
|
||||
mjs_val_t w_in = mjs_get(mjs, element, "w", ~0);
|
||||
mjs_val_t h_in = mjs_get(mjs, element, "h", ~0);
|
||||
if(!mjs_is_number(w_in) || !mjs_is_number(h_in)) return false;
|
||||
*w = mjs_get_int32(mjs, w_in);
|
||||
*h = mjs_get_int32(mjs, h_in);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parses alignment (V and H) from an element declaration object
|
||||
*/
|
||||
static bool
|
||||
element_get_alignment(struct mjs* mjs, mjs_val_t element, Align* align_v, Align* align_h) {
|
||||
mjs_val_t align_in = mjs_get(mjs, element, "align", ~0);
|
||||
const char* align = mjs_get_string(mjs, &align_in, NULL);
|
||||
if(!align) return false;
|
||||
if(strlen(align) != 2) return false;
|
||||
|
||||
if(align[0] == 't') {
|
||||
*align_v = AlignTop;
|
||||
} else if(align[0] == 'c') {
|
||||
*align_v = AlignCenter;
|
||||
} else if(align[0] == 'b') {
|
||||
*align_v = AlignBottom;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(align[1] == 'l') {
|
||||
*align_h = AlignLeft;
|
||||
} else if(align[1] == 'm') { // m = middle
|
||||
*align_h = AlignCenter;
|
||||
} else if(align[1] == 'r') {
|
||||
*align_h = AlignRight;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parses font from an element declaration object
|
||||
*/
|
||||
static bool element_get_font(struct mjs* mjs, mjs_val_t element, Font* font) {
|
||||
mjs_val_t font_in = mjs_get(mjs, element, "font", ~0);
|
||||
const char* font_str = mjs_get_string(mjs, &font_in, NULL);
|
||||
if(!font_str) return false;
|
||||
|
||||
if(strcmp(font_str, "primary") == 0) {
|
||||
*font = FontPrimary;
|
||||
} else if(strcmp(font_str, "secondary") == 0) {
|
||||
*font = FontSecondary;
|
||||
} else if(strcmp(font_str, "keyboard") == 0) {
|
||||
*font = FontKeyboard;
|
||||
} else if(strcmp(font_str, "big_numbers") == 0) {
|
||||
*font = FontBigNumbers;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parses text from an element declaration object
|
||||
*/
|
||||
static bool element_get_text(struct mjs* mjs, mjs_val_t element, mjs_val_t* text) {
|
||||
*text = mjs_get(mjs, element, "text", ~0);
|
||||
return mjs_is_string(*text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Widget button element callback
|
||||
*/
|
||||
static void js_widget_button_callback(GuiButtonType result, InputType type, JsWidgetCtx* context) {
|
||||
UNUSED(type);
|
||||
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
#define DESTRUCTURE_OR_RETURN(mjs, child_obj, part, ...) \
|
||||
if(!element_get_##part(mjs, child_obj, __VA_ARGS__)) \
|
||||
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element " #part);
|
||||
|
||||
static bool js_widget_add_child(
|
||||
struct mjs* mjs,
|
||||
Widget* widget,
|
||||
JsWidgetCtx* context,
|
||||
mjs_val_t child_obj) {
|
||||
UNUSED(context);
|
||||
if(!mjs_is_object(child_obj))
|
||||
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "child must be an object");
|
||||
|
||||
mjs_val_t element_type_term = mjs_get(mjs, child_obj, "element", ~0);
|
||||
const char* element_type = mjs_get_string(mjs, &element_type_term, NULL);
|
||||
if(!element_type)
|
||||
JS_ERROR_AND_RETURN_VAL(
|
||||
mjs, MJS_BAD_ARGS_ERROR, false, "child object must have `element` property");
|
||||
|
||||
if((strcmp(element_type, "string") == 0) || (strcmp(element_type, "string_multiline") == 0)) {
|
||||
int32_t x, y;
|
||||
Align align_v, align_h;
|
||||
Font font;
|
||||
mjs_val_t text;
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
||||
if(strcmp(element_type, "string") == 0) {
|
||||
widget_add_string_element(
|
||||
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
|
||||
} else {
|
||||
widget_add_string_multiline_element(
|
||||
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
|
||||
}
|
||||
|
||||
} else if(strcmp(element_type, "text_box") == 0) {
|
||||
int32_t x, y, w, h;
|
||||
Align align_v, align_h;
|
||||
Font font;
|
||||
mjs_val_t text;
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
||||
mjs_val_t strip_to_dots_in = mjs_get(mjs, child_obj, "stripToDots", ~0);
|
||||
if(!mjs_is_boolean(strip_to_dots_in))
|
||||
JS_ERROR_AND_RETURN_VAL(
|
||||
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element stripToDots");
|
||||
bool strip_to_dots = mjs_get_bool(mjs, strip_to_dots_in);
|
||||
widget_add_text_box_element(
|
||||
widget, x, y, w, h, align_h, align_v, mjs_get_string(mjs, &text, NULL), strip_to_dots);
|
||||
|
||||
} else if(strcmp(element_type, "text_scroll") == 0) {
|
||||
int32_t x, y, w, h;
|
||||
mjs_val_t text;
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
||||
widget_add_text_scroll_element(widget, x, y, w, h, mjs_get_string(mjs, &text, NULL));
|
||||
|
||||
} else if(strcmp(element_type, "button") == 0) {
|
||||
mjs_val_t btn_in = mjs_get(mjs, child_obj, "button", ~0);
|
||||
const char* btn_name = mjs_get_string(mjs, &btn_in, NULL);
|
||||
if(!btn_name)
|
||||
JS_ERROR_AND_RETURN_VAL(
|
||||
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element button");
|
||||
GuiButtonType btn_type;
|
||||
if(strcmp(btn_name, "left") == 0) {
|
||||
btn_type = GuiButtonTypeLeft;
|
||||
} else if(strcmp(btn_name, "center") == 0) {
|
||||
btn_type = GuiButtonTypeCenter;
|
||||
} else if(strcmp(btn_name, "right") == 0) {
|
||||
btn_type = GuiButtonTypeRight;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "incorrect button type");
|
||||
}
|
||||
mjs_val_t text;
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
||||
widget_add_button_element(
|
||||
widget,
|
||||
btn_type,
|
||||
mjs_get_string(mjs, &text, NULL),
|
||||
(ButtonCallback)js_widget_button_callback,
|
||||
context);
|
||||
|
||||
} else if(strcmp(element_type, "icon") == 0) {
|
||||
int32_t x, y;
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
||||
mjs_val_t icon_data_in = mjs_get(mjs, child_obj, "iconData", ~0);
|
||||
if(!mjs_is_foreign(icon_data_in))
|
||||
JS_ERROR_AND_RETURN_VAL(
|
||||
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element iconData");
|
||||
const Icon* icon = mjs_get_ptr(mjs, icon_data_in);
|
||||
widget_add_icon_element(widget, x, y, icon);
|
||||
|
||||
} else if(strcmp(element_type, "frame") == 0) {
|
||||
int32_t x, y, w, h;
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
||||
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
|
||||
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
|
||||
if(!mjs_is_number(radius_in))
|
||||
JS_ERROR_AND_RETURN_VAL(
|
||||
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
|
||||
int32_t radius = mjs_get_int32(mjs, radius_in);
|
||||
widget_add_frame_element(widget, x, y, w, h, radius);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_widget_reset_children(Widget* widget, void* state) {
|
||||
UNUSED(state);
|
||||
widget_reset(widget);
|
||||
}
|
||||
|
||||
static mjs_val_t js_widget_button_event_transformer(
|
||||
struct mjs* mjs,
|
||||
FuriMessageQueue* queue,
|
||||
JsWidgetCtx* context) {
|
||||
UNUSED(context);
|
||||
GuiButtonType btn_type;
|
||||
furi_check(furi_message_queue_get(queue, &btn_type, 0) == FuriStatusOk);
|
||||
const char* btn_name;
|
||||
if(btn_type == GuiButtonTypeLeft) {
|
||||
btn_name = "left";
|
||||
} else if(btn_type == GuiButtonTypeCenter) {
|
||||
btn_name = "center";
|
||||
} else if(btn_type == GuiButtonTypeRight) {
|
||||
btn_name = "right";
|
||||
} else {
|
||||
furi_crash();
|
||||
}
|
||||
return mjs_mk_string(mjs, btn_name, ~0, false);
|
||||
}
|
||||
|
||||
static void* js_widget_custom_make(struct mjs* mjs, Widget* widget, mjs_val_t view_obj) {
|
||||
UNUSED(widget);
|
||||
JsWidgetCtx* context = malloc(sizeof(JsWidgetCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(GuiButtonType));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)js_widget_button_event_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "button", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void js_widget_custom_destroy(Widget* widget, JsWidgetCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(widget);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)widget_alloc,
|
||||
.free = (JsViewFree)widget_free,
|
||||
.get_view = (JsViewGetView)widget_get_view,
|
||||
.custom_make = (JsViewCustomMake)js_widget_custom_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)js_widget_custom_destroy,
|
||||
.add_child = (JsViewAddChild)js_widget_add_child,
|
||||
.reset_children = (JsViewResetChildren)js_widget_reset_children,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(widget, &view_descriptor);
|
||||
@@ -75,6 +75,34 @@ export interface Pin {
|
||||
* @version Added in JS SDK 0.1
|
||||
*/
|
||||
interrupt(): Contract;
|
||||
/**
|
||||
* Determines whether this pin supports PWM. If `false`, all other
|
||||
* PWM-related methods on this pin will throw an error when called.
|
||||
* @note On Flipper Zero only pins PA4 and PA7 support PWM
|
||||
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
|
||||
*/
|
||||
isPwmSupported(): boolean;
|
||||
/**
|
||||
* Sets PWM parameters and starts the PWM. Configures the pin with
|
||||
* `{ direction: "out", outMode: "push_pull" }`. Throws an error if PWM is
|
||||
* not supported on this pin.
|
||||
* @param freq Frequency in Hz
|
||||
* @param duty Duty cycle in %
|
||||
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
|
||||
*/
|
||||
pwmWrite(freq: number, duty: number): void;
|
||||
/**
|
||||
* Determines whether PWM is running. Throws an error if PWM is not
|
||||
* supported on this pin.
|
||||
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
|
||||
*/
|
||||
isPwmRunning(): boolean;
|
||||
/**
|
||||
* Stops PWM. Does not restore previous pin configuration. Throws an error
|
||||
* if PWM is not supported on this pin.
|
||||
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
|
||||
*/
|
||||
pwmStop(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,9 +33,10 @@ type Props = {
|
||||
length: number,
|
||||
defaultData: Uint8Array | ArrayBuffer,
|
||||
}
|
||||
declare class ByteInput extends View<Props> {
|
||||
type Child = never;
|
||||
declare class ByteInput extends View<Props, Child> {
|
||||
input: Contract<string>;
|
||||
}
|
||||
declare class ByteInputFactory extends ViewFactory<Props, ByteInput> { }
|
||||
declare class ByteInputFactory extends ViewFactory<Props, Child, ByteInput> { }
|
||||
declare const factory: ByteInputFactory;
|
||||
export = factory;
|
||||
|
||||
@@ -37,9 +37,10 @@ type Props = {
|
||||
center: string,
|
||||
right: string,
|
||||
}
|
||||
declare class Dialog extends View<Props> {
|
||||
type Child = never;
|
||||
declare class Dialog extends View<Props, Child> {
|
||||
input: Contract<"left" | "center" | "right">;
|
||||
}
|
||||
declare class DialogFactory extends ViewFactory<Props, Dialog> { }
|
||||
declare class DialogFactory extends ViewFactory<Props, Child, Dialog> { }
|
||||
declare const factory: DialogFactory;
|
||||
export = factory;
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
|
||||
type Props = {};
|
||||
declare class EmptyScreen extends View<Props> { }
|
||||
declare class EmptyScreenFactory extends ViewFactory<Props, EmptyScreen> { }
|
||||
type Child = never;
|
||||
declare class EmptyScreen extends View<Props, Child> { }
|
||||
declare class EmptyScreenFactory extends ViewFactory<Props, Child, EmptyScreen> { }
|
||||
declare const factory: EmptyScreenFactory;
|
||||
export = factory;
|
||||
|
||||
11
applications/system/js_app/packages/fz-sdk/gui/icon.d.ts
vendored
Normal file
11
applications/system/js_app/packages/fz-sdk/gui/icon.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px";
|
||||
|
||||
export type IconData = symbol & { "__tag__": "icon" };
|
||||
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.
|
||||
|
||||
/**
|
||||
* Gets a built-in firmware icon for use in GUI
|
||||
* @param icon Name of the icon
|
||||
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
|
||||
*/
|
||||
export declare function getBuiltin(icon: BuiltinIcon): IconData;
|
||||
@@ -27,13 +27,13 @@
|
||||
* types of views are available (not all of which are unfortunately currently
|
||||
* implemented in JS):
|
||||
* | View | Has JS adapter? |
|
||||
* |----------------------|------------------|
|
||||
* |----------------------|-----------------------|
|
||||
* | `button_menu` | ❌ |
|
||||
* | `button_panel` | ❌ |
|
||||
* | `byte_input` | ✅ |
|
||||
* | `dialog_ex` | ✅ (as `dialog`) |
|
||||
* | `empty_screen` | ✅ |
|
||||
* | `file_browser` | ❌ |
|
||||
* | `file_browser` | ✅ (as `file_picker`) |
|
||||
* | `loading` | ✅ |
|
||||
* | `menu` | ❌ |
|
||||
* | `number_input` | ❌ |
|
||||
@@ -42,7 +42,7 @@
|
||||
* | `text_box` | ✅ |
|
||||
* | `text_input` | ✅ |
|
||||
* | `variable_item_list` | ❌ |
|
||||
* | `widget` | ❌ |
|
||||
* | `widget` | ✅ |
|
||||
*
|
||||
* In JS, each view has its own set of properties (or just "props"). The
|
||||
* programmer can manipulate these properties in two ways:
|
||||
@@ -121,7 +121,7 @@ import type { Contract } from "../event_loop";
|
||||
|
||||
type Properties = { [K: string]: any };
|
||||
|
||||
export declare class View<Props extends Properties> {
|
||||
export declare class View<Props extends Properties, Child> {
|
||||
/**
|
||||
* Assign value to property by name
|
||||
* @param property Name of the property
|
||||
@@ -129,9 +129,26 @@ export declare class View<Props extends Properties> {
|
||||
* @version Added in JS SDK 0.1
|
||||
*/
|
||||
set<P extends keyof Props>(property: P, value: Props[P]): void;
|
||||
/**
|
||||
* Adds a child to the View
|
||||
* @param child Child to add
|
||||
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
|
||||
*/
|
||||
addChild<C extends Child>(child: C): void;
|
||||
/**
|
||||
* Removes all children from the View
|
||||
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
|
||||
*/
|
||||
resetChildren(): void;
|
||||
/**
|
||||
* Removes all previous children from the View and assigns new children
|
||||
* @param children The list of children to assign
|
||||
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
|
||||
*/
|
||||
setChildren(children: Child[]): void;
|
||||
}
|
||||
|
||||
export declare class ViewFactory<Props extends Properties, V extends View<Props>> {
|
||||
export declare class ViewFactory<Props extends Properties, Child, V extends View<Props, Child>> {
|
||||
/**
|
||||
* Create view instance with default values, can be changed later with set()
|
||||
* @version Added in JS SDK 0.1
|
||||
@@ -140,9 +157,10 @@ export declare class ViewFactory<Props extends Properties, V extends View<Props>
|
||||
/**
|
||||
* Create view instance with custom values, can be changed later with set()
|
||||
* @param initial Dictionary of property names to values
|
||||
* @version Added in JS SDK 0.1
|
||||
* @param children Optional list of children to add to the view
|
||||
* @version Added in JS SDK 0.1; amended in JS SDK 0.2, extra feature `"gui-widget"`
|
||||
*/
|
||||
makeWith(initial: Partial<Props>): V;
|
||||
makeWith(initial: Partial<Props>, children?: Child[]): V;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,7 +181,7 @@ declare class ViewDispatcher {
|
||||
* View object currently shown
|
||||
* @version Added in JS SDK 0.1
|
||||
*/
|
||||
currentView: View<any>;
|
||||
currentView: View<any, any>;
|
||||
/**
|
||||
* Sends a number to the custom event handler
|
||||
* @param event number to send
|
||||
@@ -175,7 +193,7 @@ declare class ViewDispatcher {
|
||||
* @param assoc View-ViewDispatcher association as returned by `add`
|
||||
* @version Added in JS SDK 0.1
|
||||
*/
|
||||
switchTo(assoc: View<any>): void;
|
||||
switchTo(assoc: View<any, any>): void;
|
||||
/**
|
||||
* Sends this ViewDispatcher to the front or back, above or below all other
|
||||
* GUI viewports
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
import type { View, ViewFactory } from ".";
|
||||
|
||||
type Props = {};
|
||||
declare class Loading extends View<Props> { }
|
||||
declare class LoadingFactory extends ViewFactory<Props, Loading> { }
|
||||
type Child = never;
|
||||
declare class Loading extends View<Props, Child> { }
|
||||
declare class LoadingFactory extends ViewFactory<Props, Child, Loading> { }
|
||||
declare const factory: LoadingFactory;
|
||||
export = factory;
|
||||
|
||||
@@ -31,9 +31,10 @@ type Props = {
|
||||
header: string,
|
||||
items: string[],
|
||||
};
|
||||
declare class Submenu extends View<Props> {
|
||||
type Child = never;
|
||||
declare class Submenu extends View<Props, Child> {
|
||||
chosen: Contract<number>;
|
||||
}
|
||||
declare class SubmenuFactory extends ViewFactory<Props, Submenu> { }
|
||||
declare class SubmenuFactory extends ViewFactory<Props, Child, Submenu> { }
|
||||
declare const factory: SubmenuFactory;
|
||||
export = factory;
|
||||
|
||||
@@ -33,9 +33,10 @@ type Props = {
|
||||
font: "text" | "hex",
|
||||
focus: "start" | "end",
|
||||
}
|
||||
declare class TextBox extends View<Props> {
|
||||
type Child = never;
|
||||
declare class TextBox extends View<Props, Child> {
|
||||
chosen: Contract<number>;
|
||||
}
|
||||
declare class TextBoxFactory extends ViewFactory<Props, TextBox> { }
|
||||
declare class TextBoxFactory extends ViewFactory<Props, Child, TextBox> { }
|
||||
declare const factory: TextBoxFactory;
|
||||
export = factory;
|
||||
|
||||
@@ -37,9 +37,10 @@ type Props = {
|
||||
defaultText: string,
|
||||
defaultTextClear: boolean,
|
||||
}
|
||||
declare class TextInput extends View<Props> {
|
||||
type Child = never;
|
||||
declare class TextInput extends View<Props, Child> {
|
||||
input: Contract<string>;
|
||||
}
|
||||
declare class TextInputFactory extends ViewFactory<Props, TextInput> { }
|
||||
declare class TextInputFactory extends ViewFactory<Props, Child, TextInput> { }
|
||||
declare const factory: TextInputFactory;
|
||||
export = factory;
|
||||
|
||||
66
applications/system/js_app/packages/fz-sdk/gui/widget.d.ts
vendored
Normal file
66
applications/system/js_app/packages/fz-sdk/gui/widget.d.ts
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Displays a combination of custom elements on one screen.
|
||||
*
|
||||
* <img src="../images/widget.png" width="200" alt="Sample screenshot of the view" />
|
||||
*
|
||||
* ```js
|
||||
* let eventLoop = require("event_loop");
|
||||
* let gui = require("gui");
|
||||
* let emptyView = require("gui/widget");
|
||||
* ```
|
||||
*
|
||||
* This module depends on the `gui` module, which in turn depends on the
|
||||
* `event_loop` module, so they _must_ be imported in this order. It is also
|
||||
* recommended to conceptualize these modules first before using this one.
|
||||
*
|
||||
* # Example
|
||||
* For an example refer to the GUI example.
|
||||
*
|
||||
* # View props
|
||||
* This view does not have any props.
|
||||
*
|
||||
* # Children
|
||||
* This view has the elements as its children.
|
||||
*
|
||||
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type { View, ViewFactory } from ".";
|
||||
import type { IconData } from "./icon";
|
||||
import type { Contract } from "../event_loop";
|
||||
|
||||
type Position = { x: number, y: number };
|
||||
type Size = { w: number, h: number };
|
||||
type Alignment = { align: `${"t" | "c" | "b"}${"l" | "m" | "r"}` };
|
||||
type Font = { font: "primary" | "secondary" | "keyboard" | "big_numbers" };
|
||||
type Text = { text: string };
|
||||
|
||||
type StringMultilineElement = { element: "string_multiline" } & Position & Alignment & Font & Text;
|
||||
type StringElement = { element: "string" } & Position & Alignment & Font & Text;
|
||||
type TextBoxElement = { element: "text_box", stripToDots: boolean } & Position & Size & Alignment & Text;
|
||||
type TextScrollElement = { element: "text_scroll" } & Position & Size & Text;
|
||||
type ButtonElement = { element: "button", button: "left" | "center" | "right" } & Text;
|
||||
type IconElement = { element: "icon", iconData: IconData } & Position;
|
||||
type FrameElement = { element: "frame", radius: number } & Position & Size;
|
||||
|
||||
type Element = StringMultilineElement
|
||||
| StringElement
|
||||
| TextBoxElement
|
||||
| TextScrollElement
|
||||
| ButtonElement
|
||||
| IconElement
|
||||
| FrameElement;
|
||||
|
||||
type Props = {};
|
||||
type Child = Element;
|
||||
declare class Widget extends View<Props, Child> {
|
||||
/**
|
||||
* Event source for buttons. Only gets fired if there's a corresponding
|
||||
* button element.
|
||||
*/
|
||||
button: Contract<"left" | "center" | "right">;
|
||||
}
|
||||
declare class WidgetFactory extends ViewFactory<Props, Child, Widget> { }
|
||||
declare const factory: WidgetFactory;
|
||||
export = factory;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@flipperdevices/fz-sdk",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"description": "Type declarations and documentation for native JS modules available on Flipper Zero",
|
||||
"keywords": [
|
||||
"flipper",
|
||||
|
||||
BIN
documentation/images/widget.png
Normal file
BIN
documentation/images/widget.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -28,13 +28,13 @@ In Flipper's terminology, a "View" is a fullscreen design element that assumes
|
||||
control over the entire viewport and all input events. Different types of views
|
||||
are available (not all of which are unfortunately currently implemented in JS):
|
||||
| View | Has JS adapter? |
|
||||
|----------------------|------------------|
|
||||
|----------------------|-----------------------|
|
||||
| `button_menu` | ❌ |
|
||||
| `button_panel` | ❌ |
|
||||
| `byte_input` | ❌ |
|
||||
| `byte_input` | ✅ |
|
||||
| `dialog_ex` | ✅ (as `dialog`) |
|
||||
| `empty_screen` | ✅ |
|
||||
| `file_browser` | ❌ |
|
||||
| `file_browser` | ✅ (as `file_picker`) |
|
||||
| `loading` | ✅ |
|
||||
| `menu` | ❌ |
|
||||
| `number_input` | ❌ |
|
||||
@@ -43,7 +43,7 @@ are available (not all of which are unfortunately currently implemented in JS):
|
||||
| `text_box` | ✅ |
|
||||
| `text_input` | ✅ |
|
||||
| `variable_item_list` | ❌ |
|
||||
| `widget` | ❌ |
|
||||
| `widget` | ✅ |
|
||||
|
||||
In JS, each view has its own set of properties (or just "props"). The programmer
|
||||
can manipulate these properties in two ways:
|
||||
|
||||
25
documentation/js/js_gui__widget.md
Normal file
25
documentation/js/js_gui__widget.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# js_gui__widget {#js_gui__widget}
|
||||
|
||||
# Widget GUI view
|
||||
Displays a combination of custom elements on one screen
|
||||
|
||||
<img src="widget.png" width="200" alt="Sample screenshot of the view" />
|
||||
|
||||
```js
|
||||
let eventLoop = require("event_loop");
|
||||
let gui = require("gui");
|
||||
let widgetView = require("gui/widget");
|
||||
```
|
||||
|
||||
This module depends on the `gui` module, which in turn depends on the
|
||||
`event_loop` module, so they _must_ be imported in this order. It is also
|
||||
recommended to conceptualize these modules first before using this one.
|
||||
|
||||
# Example
|
||||
For an example refer to the `gui.js` example script.
|
||||
|
||||
# View props
|
||||
This view does not have any props.
|
||||
|
||||
# Children
|
||||
This view has the elements as its children.
|
||||
@@ -584,7 +584,8 @@ static void mjs_apply_(struct mjs* mjs) {
|
||||
if(mjs_is_array(v)) {
|
||||
nargs = mjs_array_length(mjs, v);
|
||||
args = calloc(nargs, sizeof(args[0]));
|
||||
for(i = 0; i < nargs; i++) args[i] = mjs_array_get(mjs, v, i);
|
||||
for(i = 0; i < nargs; i++)
|
||||
args[i] = mjs_array_get(mjs, v, i);
|
||||
}
|
||||
mjs_apply(mjs, &res, func, mjs_arg(mjs, 0), nargs, args);
|
||||
free(args);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal_adc.h>
|
||||
#include <furi_hal_pwm.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -40,6 +41,7 @@ typedef struct {
|
||||
const GpioPin* pin;
|
||||
const char* name;
|
||||
const FuriHalAdcChannel channel;
|
||||
const FuriHalPwmOutputId pwm_output;
|
||||
const uint8_t number;
|
||||
const bool debug;
|
||||
} GpioPinRecord;
|
||||
|
||||
@@ -12,6 +12,7 @@ extern "C" {
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
FuriHalPwmOutputIdNone,
|
||||
FuriHalPwmOutputIdTim1PA7,
|
||||
FuriHalPwmOutputIdLptim2PA4,
|
||||
} FuriHalPwmOutputId;
|
||||
|
||||
@@ -73,6 +73,7 @@ const GpioPinRecord gpio_pins[] = {
|
||||
{.pin = &gpio_ext_pa7,
|
||||
.name = "PA7",
|
||||
.channel = FuriHalAdcChannel12,
|
||||
.pwm_output = FuriHalPwmOutputIdTim1PA7,
|
||||
.number = 2,
|
||||
.debug = false},
|
||||
{.pin = &gpio_ext_pa6,
|
||||
@@ -83,6 +84,7 @@ const GpioPinRecord gpio_pins[] = {
|
||||
{.pin = &gpio_ext_pa4,
|
||||
.name = "PA4",
|
||||
.channel = FuriHalAdcChannel9,
|
||||
.pwm_output = FuriHalPwmOutputIdLptim2PA4,
|
||||
.number = 4,
|
||||
.debug = false},
|
||||
{.pin = &gpio_ext_pb3,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal_adc.h>
|
||||
#include <furi_hal_pwm.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -40,6 +41,7 @@ typedef struct {
|
||||
const GpioPin* pin;
|
||||
const char* name;
|
||||
const FuriHalAdcChannel channel;
|
||||
const FuriHalPwmOutputId pwm_output;
|
||||
const uint8_t number;
|
||||
const bool debug;
|
||||
} GpioPinRecord;
|
||||
|
||||
Reference in New Issue
Block a user