mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-04-25 03:29:58 -07:00
[FL-3893] JS modules (#3841)
* feat: backport js_gpio from unleashed * feat: backport js_keyboard, TextInputModel::minimum_length from unleashed * fix: api version inconsistency * style: js_gpio * build: fix submodule ._ . * refactor: js_gpio * docs: type declarations for gpio * feat: gpio interrupts * fix: js_gpio freeing, resetting and minor stylistic changes * style: js_gpio * style: mlib array, fixme's * feat: js_gpio adc * feat: js_event_loop * docs: js_event_loop * feat: js_event_loop subscription cancellation * feat: js_event_loop + js_gpio integration * fix: js_event_loop memory leak * feat: stop event loop on back button * test: js: basic, math, event_loop * feat: js_event_loop queue * feat: js linkage to previously loaded plugins * build: fix ci errors * feat: js module ordered teardown * feat: js_gui_defer_free * feat: basic hourglass view * style: JS ASS (Argument Schema for Scripts) * fix: js_event_loop mem leaks and lifetime problems * fix: crashing test and pvs false positives * feat: mjs custom obj destructors, gui submenu view * refactor: yank js_gui_defer_free (yuck) * refactor: maybe_unsubscribe * empty_screen, docs, typing fix-ups * docs: navigation event & demo * feat: submenu setHeader * feat: text_input * feat: text_box * docs: text_box availability * ci: silence irrelevant pvs low priority warning * style: use furistring * style: _get_at -> _safe_get * fix: built-in module name assignment * feat: js_dialog; refactor, optimize: js_gui * docs: js_gui * ci: silence pvs warning: Memory allocation is infallible * style: fix storage spelling * feat: foreign pointer signature checks * feat: js_storage * docs: js_storage * fix: my unit test was breaking other tests ;_; * ci: fix ci? * Make doxygen happy * docs: flipper, math, notification, global * style: review suggestions * style: review fixups * fix: badusb demo script * docs: badusb * ci: add nofl * ci: make linter happy * Bump api version Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
@@ -72,8 +72,8 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf
|
||||
}
|
||||
mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0);
|
||||
mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0);
|
||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0);
|
||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0);
|
||||
mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0);
|
||||
mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0);
|
||||
|
||||
if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) {
|
||||
hid_cfg->vid = mjs_get_int32(mjs, vid_obj);
|
||||
@@ -378,7 +378,8 @@ static void js_badusb_println(struct mjs* mjs) {
|
||||
badusb_print(mjs, true);
|
||||
}
|
||||
|
||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst));
|
||||
mjs_val_t badusb_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb));
|
||||
@@ -409,6 +410,7 @@ static const JsModuleDescriptor js_badusb_desc = {
|
||||
"badusb",
|
||||
js_badusb_create,
|
||||
js_badusb_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#include <core/common_defines.h>
|
||||
#include "../js_modules.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
|
||||
static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != 2) {
|
||||
return false;
|
||||
}
|
||||
mjs_val_t header_obj = mjs_arg(mjs, 0);
|
||||
mjs_val_t msg_obj = mjs_arg(mjs, 1);
|
||||
if((!mjs_is_string(header_obj)) || (!mjs_is_string(msg_obj))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t arg_len = 0;
|
||||
*hdr = mjs_get_string(mjs, &header_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
*hdr = NULL;
|
||||
}
|
||||
|
||||
*msg = mjs_get_string(mjs, &msg_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
*msg = NULL;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_dialog_message(struct mjs* mjs) {
|
||||
const char* dialog_header = NULL;
|
||||
const char* dialog_msg = NULL;
|
||||
if(!js_dialog_msg_parse_params(mjs, &dialog_header, &dialog_msg)) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
dialog_message_set_buttons(message, NULL, "OK", NULL);
|
||||
if(dialog_header) {
|
||||
dialog_message_set_header(message, dialog_header, 64, 3, AlignCenter, AlignTop);
|
||||
}
|
||||
if(dialog_msg) {
|
||||
dialog_message_set_text(message, dialog_msg, 64, 26, AlignCenter, AlignTop);
|
||||
}
|
||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
||||
dialog_message_free(message);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, result == DialogMessageButtonCenter));
|
||||
}
|
||||
|
||||
static void js_dialog_custom(struct mjs* mjs) {
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
|
||||
bool params_correct = false;
|
||||
|
||||
do {
|
||||
if(mjs_nargs(mjs) != 1) {
|
||||
break;
|
||||
}
|
||||
mjs_val_t params_obj = mjs_arg(mjs, 0);
|
||||
if(!mjs_is_object(params_obj)) {
|
||||
break;
|
||||
}
|
||||
|
||||
mjs_val_t text_obj = mjs_get(mjs, params_obj, "header", ~0);
|
||||
size_t arg_len = 0;
|
||||
const char* text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
text_str = NULL;
|
||||
}
|
||||
if(text_str) {
|
||||
dialog_message_set_header(message, text_str, 64, 3, AlignCenter, AlignTop);
|
||||
}
|
||||
|
||||
text_obj = mjs_get(mjs, params_obj, "text", ~0);
|
||||
text_str = mjs_get_string(mjs, &text_obj, &arg_len);
|
||||
if(arg_len == 0) {
|
||||
text_str = NULL;
|
||||
}
|
||||
if(text_str) {
|
||||
dialog_message_set_text(message, text_str, 64, 26, AlignCenter, AlignTop);
|
||||
}
|
||||
|
||||
mjs_val_t btn_obj[3] = {
|
||||
mjs_get(mjs, params_obj, "button_left", ~0),
|
||||
mjs_get(mjs, params_obj, "button_center", ~0),
|
||||
mjs_get(mjs, params_obj, "button_right", ~0),
|
||||
};
|
||||
const char* btn_text[3] = {NULL, NULL, NULL};
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
if(!mjs_is_string(btn_obj[i])) {
|
||||
continue;
|
||||
}
|
||||
btn_text[i] = mjs_get_string(mjs, &btn_obj[i], &arg_len);
|
||||
if(arg_len == 0) {
|
||||
btn_text[i] = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
dialog_message_set_buttons(message, btn_text[0], btn_text[1], btn_text[2]);
|
||||
|
||||
DialogMessageButton result = dialog_message_show(dialogs, message);
|
||||
mjs_val_t return_obj = MJS_UNDEFINED;
|
||||
if(result == DialogMessageButtonLeft) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[0], ~0, true);
|
||||
} else if(result == DialogMessageButtonCenter) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[1], ~0, true);
|
||||
} else if(result == DialogMessageButtonRight) {
|
||||
return_obj = mjs_mk_string(mjs, btn_text[2], ~0, true);
|
||||
} else {
|
||||
return_obj = mjs_mk_string(mjs, "", ~0, true);
|
||||
}
|
||||
|
||||
mjs_return(mjs, return_obj);
|
||||
params_correct = true;
|
||||
} while(0);
|
||||
|
||||
dialog_message_free(message);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
|
||||
if(!params_correct) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
mjs_val_t dialog_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message));
|
||||
mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom));
|
||||
*object = dialog_obj;
|
||||
|
||||
return (void*)1;
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_dialog_desc = {
|
||||
"dialog",
|
||||
js_dialog_create,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_dialog_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_dialog_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
451
applications/system/js_app/modules/js_event_loop/js_event_loop.c
Normal file
@@ -0,0 +1,451 @@
|
||||
#include "js_event_loop.h"
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include <expansion/expansion.h>
|
||||
#include <mlib/m-array.h>
|
||||
|
||||
/**
|
||||
* @brief Number of arguments that callbacks receive from this module that they can't modify
|
||||
*/
|
||||
#define SYSTEM_ARGS 2
|
||||
|
||||
/**
|
||||
* @brief Context passed to the generic event callback
|
||||
*/
|
||||
typedef struct {
|
||||
JsEventLoopObjectType object_type;
|
||||
|
||||
struct mjs* mjs;
|
||||
mjs_val_t callback;
|
||||
// NOTE: not using an mlib array because resizing is not needed.
|
||||
mjs_val_t* arguments;
|
||||
size_t arity;
|
||||
|
||||
JsEventLoopTransformer transformer;
|
||||
void* transformer_context;
|
||||
} JsEventLoopCallbackContext;
|
||||
|
||||
/**
|
||||
* @brief Contains data needed to cancel a subscription
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
JsEventLoopObjectType object_type;
|
||||
FuriEventLoopObject* object;
|
||||
JsEventLoopCallbackContext* context;
|
||||
JsEventLoopContract* contract;
|
||||
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
|
||||
|
||||
/**
|
||||
* @brief Per-module instance control structure
|
||||
*/
|
||||
struct JsEventLoop {
|
||||
FuriEventLoop* loop;
|
||||
SubscriptionArray_t subscriptions;
|
||||
ContractArray_t owned_contracts; //<! Contracts that were produced by this module
|
||||
JsEventLoopTickContext* tick_context;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Generic event callback, handles all events by calling the JS callbacks
|
||||
*/
|
||||
static void js_event_loop_callback_generic(void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
mjs_val_t result;
|
||||
mjs_apply(
|
||||
context->mjs,
|
||||
&result,
|
||||
context->callback,
|
||||
MJS_UNDEFINED,
|
||||
context->arity,
|
||||
context->arguments);
|
||||
|
||||
// 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++) {
|
||||
mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||
context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i);
|
||||
mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles non-timer events
|
||||
*/
|
||||
static bool js_event_loop_callback(void* object, void* param) {
|
||||
JsEventLoopCallbackContext* context = param;
|
||||
|
||||
if(context->transformer) {
|
||||
mjs_disown(context->mjs, &context->arguments[1]);
|
||||
context->arguments[1] =
|
||||
context->transformer(context->mjs, object, context->transformer_context);
|
||||
mjs_own(context->mjs, &context->arguments[1]);
|
||||
} else {
|
||||
// default behavior: take semaphores and mutexes
|
||||
switch(context->object_type) {
|
||||
case JsEventLoopObjectTypeSemaphore: {
|
||||
FuriSemaphore* semaphore = object;
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
} break;
|
||||
default:
|
||||
// the corresponding check has been performed when we were given the contract
|
||||
furi_crash();
|
||||
}
|
||||
}
|
||||
|
||||
js_event_loop_callback_generic(param);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cancels an event subscription
|
||||
*/
|
||||
static void js_event_loop_subscription_cancel(struct mjs* mjs) {
|
||||
JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs);
|
||||
|
||||
if(subscription->object_type == JsEventLoopObjectTypeTimer) {
|
||||
furi_event_loop_timer_stop(subscription->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(subscription->loop, subscription->object);
|
||||
}
|
||||
|
||||
free(subscription->context->arguments);
|
||||
free(subscription->context);
|
||||
|
||||
// find and remove ourselves from the array
|
||||
SubscriptionArray_it_t iterator;
|
||||
for(SubscriptionArray_it(iterator, subscription->subscriptions);
|
||||
!SubscriptionArray_end_p(iterator);
|
||||
SubscriptionArray_next(iterator)) {
|
||||
JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator);
|
||||
if(item == subscription) break;
|
||||
}
|
||||
SubscriptionArray_remove(subscription->subscriptions, iterator);
|
||||
free(subscription);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Subscribes a JavaScript function to an event
|
||||
*/
|
||||
static void js_event_loop_subscribe(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// get arguments
|
||||
JsEventLoopContract* contract;
|
||||
mjs_val_t callback;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback));
|
||||
|
||||
// create subscription object
|
||||
JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription));
|
||||
JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext));
|
||||
subscription->loop = module->loop;
|
||||
subscription->object_type = contract->object_type;
|
||||
subscription->context = context;
|
||||
subscription->subscriptions = module->subscriptions;
|
||||
if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract;
|
||||
mjs_val_t subscription_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription));
|
||||
mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel));
|
||||
|
||||
// create callback context
|
||||
context->object_type = contract->object_type;
|
||||
context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2;
|
||||
context->arguments = calloc(context->arity, sizeof(mjs_val_t));
|
||||
context->arguments[0] = subscription_obj;
|
||||
context->arguments[1] = MJS_UNDEFINED;
|
||||
for(size_t i = SYSTEM_ARGS; i < context->arity; i++) {
|
||||
mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2);
|
||||
context->arguments[i] = arg;
|
||||
mjs_own(mjs, &context->arguments[i]);
|
||||
}
|
||||
context->mjs = mjs;
|
||||
context->callback = callback;
|
||||
mjs_own(mjs, &context->callback);
|
||||
mjs_own(mjs, &context->arguments[0]);
|
||||
mjs_own(mjs, &context->arguments[1]);
|
||||
|
||||
// queue and stream contracts must have a transform callback, others are allowed to delegate
|
||||
// the obvious default behavior to this module
|
||||
if(contract->object_type == JsEventLoopObjectTypeQueue ||
|
||||
contract->object_type == JsEventLoopObjectTypeStream) {
|
||||
furi_check(contract->non_timer.transformer);
|
||||
}
|
||||
context->transformer = contract->non_timer.transformer;
|
||||
context->transformer_context = contract->non_timer.transformer_context;
|
||||
|
||||
// subscribe
|
||||
switch(contract->object_type) {
|
||||
case JsEventLoopObjectTypeTimer: {
|
||||
FuriEventLoopTimer* timer = furi_event_loop_timer_alloc(
|
||||
module->loop, js_event_loop_callback_generic, contract->timer.type, context);
|
||||
furi_event_loop_timer_start(timer, contract->timer.interval_ticks);
|
||||
contract->object = timer;
|
||||
} break;
|
||||
case JsEventLoopObjectTypeSemaphore:
|
||||
furi_event_loop_subscribe_semaphore(
|
||||
module->loop,
|
||||
contract->object,
|
||||
contract->non_timer.event,
|
||||
js_event_loop_callback,
|
||||
context);
|
||||
break;
|
||||
case JsEventLoopObjectTypeQueue:
|
||||
furi_event_loop_subscribe_message_queue(
|
||||
module->loop,
|
||||
contract->object,
|
||||
contract->non_timer.event,
|
||||
js_event_loop_callback,
|
||||
context);
|
||||
break;
|
||||
default:
|
||||
furi_crash("unimplemented");
|
||||
}
|
||||
|
||||
subscription->object = contract->object;
|
||||
SubscriptionArray_push_back(module->subscriptions, subscription);
|
||||
mjs_return(mjs, subscription_obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Runs the event loop until it is stopped
|
||||
*/
|
||||
static void js_event_loop_run(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
furi_event_loop_run(module->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stops a running event loop
|
||||
*/
|
||||
static void js_event_loop_stop(struct mjs* mjs) {
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
furi_event_loop_stop(module->loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a timer event that can be subscribed to just like any other
|
||||
* event
|
||||
*/
|
||||
static void js_event_loop_timer(struct mjs* mjs) {
|
||||
// get arguments
|
||||
const char* mode_str;
|
||||
int32_t interval;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval));
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
FuriEventLoopTimerType mode;
|
||||
if(strcasecmp(mode_str, "periodic") == 0) {
|
||||
mode = FuriEventLoopTimerTypePeriodic;
|
||||
} else if(strcasecmp(mode_str, "oneshot") == 0) {
|
||||
mode = FuriEventLoopTimerTypeOnce;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode");
|
||||
}
|
||||
|
||||
// make timer contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeTimer,
|
||||
.object = NULL,
|
||||
.timer =
|
||||
{
|
||||
.interval_ticks = furi_ms_to_ticks((uint32_t)interval),
|
||||
.type = mode,
|
||||
},
|
||||
};
|
||||
ContractArray_push_back(module->owned_contracts, contract);
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and
|
||||
* returns their dereferenced value
|
||||
*/
|
||||
static mjs_val_t
|
||||
js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||
UNUSED(context);
|
||||
mjs_val_t* message_ptr;
|
||||
furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk);
|
||||
mjs_val_t message = *message_ptr;
|
||||
mjs_disown(mjs, message_ptr);
|
||||
free(message_ptr);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sends a message to a queue
|
||||
*/
|
||||
static void js_event_loop_queue_send(struct mjs* mjs) {
|
||||
// get arguments
|
||||
mjs_val_t message;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message));
|
||||
JsEventLoopContract* contract = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// send message
|
||||
mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t));
|
||||
*message_ptr = message;
|
||||
mjs_own(mjs, message_ptr);
|
||||
furi_message_queue_put(contract->object, &message_ptr, 0);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a queue
|
||||
*/
|
||||
static void js_event_loop_queue(struct mjs* mjs) {
|
||||
// get arguments
|
||||
int32_t length;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length));
|
||||
JsEventLoop* module = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make queue contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
// we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values
|
||||
.object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)),
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = js_event_loop_queue_transformer,
|
||||
},
|
||||
};
|
||||
ContractArray_push_back(module->owned_contracts, contract);
|
||||
|
||||
// return object with control methods
|
||||
mjs_val_t queue = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract));
|
||||
mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract));
|
||||
mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send));
|
||||
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));
|
||||
|
||||
*object = event_loop_obj;
|
||||
return module;
|
||||
}
|
||||
|
||||
static void js_event_loop_destroy(void* inst) {
|
||||
if(inst) {
|
||||
JsEventLoop* module = inst;
|
||||
furi_event_loop_stop(module->loop);
|
||||
|
||||
// free subscriptions
|
||||
SubscriptionArray_it_t sub_iterator;
|
||||
for(SubscriptionArray_it(sub_iterator, module->subscriptions);
|
||||
!SubscriptionArray_end_p(sub_iterator);
|
||||
SubscriptionArray_next(sub_iterator)) {
|
||||
JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator);
|
||||
free((*sub)->context->arguments);
|
||||
free((*sub)->context);
|
||||
free(*sub);
|
||||
}
|
||||
SubscriptionArray_clear(module->subscriptions);
|
||||
|
||||
// free owned contracts
|
||||
ContractArray_it_t iterator;
|
||||
for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator);
|
||||
ContractArray_next(iterator)) {
|
||||
// unsubscribe object
|
||||
JsEventLoopContract* contract = *ContractArray_cref(iterator);
|
||||
if(contract->object_type == JsEventLoopObjectTypeTimer) {
|
||||
furi_event_loop_timer_stop(contract->object);
|
||||
} else {
|
||||
furi_event_loop_unsubscribe(module->loop, contract->object);
|
||||
}
|
||||
|
||||
// free object
|
||||
switch(contract->object_type) {
|
||||
case JsEventLoopObjectTypeTimer:
|
||||
furi_event_loop_timer_free(contract->object);
|
||||
break;
|
||||
case JsEventLoopObjectTypeSemaphore:
|
||||
furi_semaphore_free(contract->object);
|
||||
break;
|
||||
case JsEventLoopObjectTypeQueue:
|
||||
furi_message_queue_free(contract->object);
|
||||
break;
|
||||
default:
|
||||
furi_crash("unimplemented");
|
||||
}
|
||||
|
||||
free(contract);
|
||||
}
|
||||
ContractArray_clear(module->owned_contracts);
|
||||
|
||||
furi_event_loop_free(module->loop);
|
||||
free(module->tick_context);
|
||||
free(module);
|
||||
}
|
||||
}
|
||||
|
||||
extern const ElfApiInterface js_event_loop_hashtable_api_interface;
|
||||
|
||||
static const JsModuleDescriptor js_event_loop_desc = {
|
||||
"event_loop",
|
||||
js_event_loop_create,
|
||||
js_event_loop_destroy,
|
||||
&js_event_loop_hashtable_api_interface,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_event_loop_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_event_loop_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
|
||||
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) {
|
||||
// porta: not the proudest function that i ever wrote
|
||||
furi_check(loop);
|
||||
return loop->loop;
|
||||
}
|
||||
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
104
applications/system/js_app/modules/js_event_loop/js_event_loop.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include <furi/core/event_loop.h>
|
||||
#include <furi/core/event_loop_timer.h>
|
||||
|
||||
/**
|
||||
* @file js_event_loop.h
|
||||
*
|
||||
* In JS interpreter code, `js_event_loop` always creates and maintains the
|
||||
* event loop. There are two ways in which other modules can integrate with this
|
||||
* loop:
|
||||
* - Via contracts: The user of your module would have to acquire an opaque
|
||||
* JS value from you and pass it to `js_event_loop`. This is useful for
|
||||
* events that they user may be interested in. For more info, look at
|
||||
* `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which
|
||||
* you will need to unsubscribe the event loop from your object.
|
||||
* - Directly: When your module is created, you can acquire an instance of
|
||||
* `JsEventLoop` which you can use to acquire an instance of
|
||||
* `FuriEventLoop` that you can manipulate directly, without the JS
|
||||
* programmer having to pass contracts around. This is useful for
|
||||
* "behind-the-scenes" events that the user does not need to know about. For
|
||||
* more info, look at `js_event_loop_get_loop`.
|
||||
*
|
||||
* In both cases, your module is responsible for both instantiating,
|
||||
* unsubscribing and freeing the object that the event loop subscribes to.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct JsEventLoop JsEventLoop;
|
||||
|
||||
typedef enum {
|
||||
JsEventLoopObjectTypeTimer,
|
||||
JsEventLoopObjectTypeQueue,
|
||||
JsEventLoopObjectTypeMutex,
|
||||
JsEventLoopObjectTypeSemaphore,
|
||||
JsEventLoopObjectTypeStream,
|
||||
} JsEventLoopObjectType;
|
||||
|
||||
typedef mjs_val_t (
|
||||
*JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context);
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoopEvent event;
|
||||
JsEventLoopTransformer transformer;
|
||||
void* transformer_context;
|
||||
} JsEventLoopNonTimerContract;
|
||||
|
||||
typedef struct {
|
||||
FuriEventLoopTimerType type;
|
||||
uint32_t interval_ticks;
|
||||
} JsEventLoopTimerContract;
|
||||
|
||||
/**
|
||||
* @brief Adapter for other JS modules that wish to integrate with the event
|
||||
* loop JS module
|
||||
*
|
||||
* If another module wishes to integrate with `js_event_loop`, it needs to
|
||||
* implement a function callable from JS that returns an mJS foreign pointer to
|
||||
* an instance of this structure. This value is then read by `event_loop`'s
|
||||
* `subscribe` function.
|
||||
*
|
||||
* There are two fundamental variants of this structure:
|
||||
* - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is
|
||||
* valid, and the `non_timer` field is invalid.
|
||||
* - `object_type` is something else: the `timer` field is invalid, and the
|
||||
* `non_timer` field is valid. `non_timer.event` will be passed to
|
||||
* `furi_event_loop_subscribe`. `non_timer.transformer` will be called to
|
||||
* transform an object into a JS value (called an item) that's passed to the
|
||||
* JS callback. This is useful for example to take an item out of a message
|
||||
* queue and pass it to JS code in a convenient format. If
|
||||
* `non_timer.transformer` is NULL, the event loop will take semaphores and
|
||||
* mutexes on its own.
|
||||
*
|
||||
* The producer of the contract is responsible for freeing both the contract and
|
||||
* the object that it points to when the interpreter is torn down.
|
||||
*/
|
||||
typedef struct {
|
||||
JsForeignMagic magic; // <! `JsForeignMagic_JsEventLoopContract`
|
||||
JsEventLoopObjectType object_type;
|
||||
FuriEventLoopObject* object;
|
||||
union {
|
||||
JsEventLoopNonTimerContract non_timer;
|
||||
JsEventLoopTimerContract timer;
|
||||
};
|
||||
} JsEventLoopContract;
|
||||
|
||||
static_assert(offsetof(JsEventLoopContract, magic) == 0);
|
||||
|
||||
/**
|
||||
* @brief Gets the FuriEventLoop owned by a JsEventLoop
|
||||
*
|
||||
* This function is useful in case your JS module wishes to integrate with
|
||||
* the event loop without passing contracts through JS code. Your module will be
|
||||
* dynamically linked to this one if you use this function, but only if JS code
|
||||
* imports `event_loop` _before_ your module. An instance of `JsEventLoop` may
|
||||
* be obtained via `js_module_get`.
|
||||
*/
|
||||
FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||
|
||||
#include "js_event_loop_api_table_i.h"
|
||||
|
||||
static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!");
|
||||
|
||||
extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{
|
||||
{
|
||||
.api_version_major = 0,
|
||||
.api_version_minor = 0,
|
||||
.resolver_callback = &elf_resolve_from_hashtable,
|
||||
},
|
||||
js_event_loop_api_table.cbegin(),
|
||||
js_event_loop_api_table.cend(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "js_event_loop.h"
|
||||
|
||||
static constexpr auto js_event_loop_api_table = sort(
|
||||
create_array_t<sym_entry>(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*))));
|
||||
@@ -25,7 +25,8 @@ static void js_flipper_get_battery(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, info.charge));
|
||||
}
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t flipper_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model));
|
||||
mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object);
|
||||
void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
|
||||
345
applications/system/js_app/modules/js_gpio.c
Normal file
345
applications/system/js_app/modules/js_gpio.c
Normal file
@@ -0,0 +1,345 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_event_loop/js_event_loop.h"
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <expansion/expansion.h>
|
||||
#include <limits.h>
|
||||
#include <mlib/m-array.h>
|
||||
|
||||
#define INTERRUPT_QUEUE_LEN 16
|
||||
|
||||
/**
|
||||
* Per-pin control structure
|
||||
*/
|
||||
typedef struct {
|
||||
const GpioPin* pin;
|
||||
bool had_interrupt;
|
||||
FuriSemaphore* interrupt_semaphore;
|
||||
JsEventLoopContract* interrupt_contract;
|
||||
FuriHalAdcChannel adc_channel;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioPinInst;
|
||||
|
||||
ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575
|
||||
|
||||
/**
|
||||
* Per-module instance control structure
|
||||
*/
|
||||
typedef struct {
|
||||
FuriEventLoop* loop;
|
||||
ManagedPinsArray_t managed_pins;
|
||||
FuriHalAdcHandle* adc_handle;
|
||||
} JsGpioInst;
|
||||
|
||||
/**
|
||||
* @brief Interrupt callback
|
||||
*/
|
||||
static void js_gpio_int_cb(void* arg) {
|
||||
furi_assert(arg);
|
||||
FuriSemaphore* semaphore = arg;
|
||||
furi_semaphore_release(semaphore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initializes a GPIO pin according to the provided mode object
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* led.init({ direction: "out", outMode: "push_pull" });
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_init(struct mjs* mjs) {
|
||||
// deconstruct mode object
|
||||
mjs_val_t mode_arg;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg));
|
||||
mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0);
|
||||
mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0);
|
||||
mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0);
|
||||
mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0);
|
||||
mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0);
|
||||
|
||||
// get strings
|
||||
const char* direction = mjs_get_string(mjs, &direction_arg, NULL);
|
||||
const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL);
|
||||
const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL);
|
||||
const char* edge = mjs_get_string(mjs, &edge_arg, NULL);
|
||||
const char* pull = mjs_get_string(mjs, &pull_arg, NULL);
|
||||
if(!direction)
|
||||
JS_ERROR_AND_RETURN(
|
||||
mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object");
|
||||
if(!out_mode) out_mode = "open_drain";
|
||||
if(!in_mode) in_mode = "plain_digital";
|
||||
if(!edge) edge = "rising";
|
||||
|
||||
// convert strings to mode
|
||||
GpioMode mode;
|
||||
if(strcmp(direction, "out") == 0) {
|
||||
if(strcmp(out_mode, "push_pull") == 0)
|
||||
mode = GpioModeOutputPushPull;
|
||||
else if(strcmp(out_mode, "open_drain") == 0)
|
||||
mode = GpioModeOutputOpenDrain;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode");
|
||||
} else if(strcmp(direction, "in") == 0) {
|
||||
if(strcmp(in_mode, "analog") == 0) {
|
||||
mode = GpioModeAnalog;
|
||||
} else if(strcmp(in_mode, "plain_digital") == 0) {
|
||||
mode = GpioModeInput;
|
||||
} else if(strcmp(in_mode, "interrupt") == 0) {
|
||||
if(strcmp(edge, "rising") == 0)
|
||||
mode = GpioModeInterruptRise;
|
||||
else if(strcmp(edge, "falling") == 0)
|
||||
mode = GpioModeInterruptFall;
|
||||
else if(strcmp(edge, "both") == 0)
|
||||
mode = GpioModeInterruptRiseFall;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||
} else if(strcmp(in_mode, "event") == 0) {
|
||||
if(strcmp(edge, "rising") == 0)
|
||||
mode = GpioModeEventRise;
|
||||
else if(strcmp(edge, "falling") == 0)
|
||||
mode = GpioModeEventFall;
|
||||
else if(strcmp(edge, "both") == 0)
|
||||
mode = GpioModeEventRiseFall;
|
||||
else
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge");
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode");
|
||||
}
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction");
|
||||
}
|
||||
|
||||
// convert pull
|
||||
GpioPull pull_mode;
|
||||
if(!pull) {
|
||||
pull_mode = GpioPullNo;
|
||||
} else if(strcmp(pull, "up") == 0) {
|
||||
pull_mode = GpioPullUp;
|
||||
} else if(strcmp(pull, "down") == 0) {
|
||||
pull_mode = GpioPullDown;
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull");
|
||||
}
|
||||
|
||||
// init GPIO
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Writes a logic value to a GPIO pin
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* led.init({ direction: "out", outMode: "push_pull" });
|
||||
* led.write(true);
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_write(struct mjs* mjs) {
|
||||
bool level;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level));
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
furi_hal_gpio_write(manager_data->pin, level);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a logic value from a GPIO pin
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let button = gpio.get("pc1");
|
||||
* button.init({ direction: "in" });
|
||||
* if(button.read())
|
||||
* print("hi button!!!!!");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_read(struct mjs* mjs) {
|
||||
// get level
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
bool value = furi_hal_gpio_read(manager_data->pin);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a event loop contract that can be used to listen to interrupts
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let button = gpio.get("pc1");
|
||||
* let event_loop = require("event_loop");
|
||||
* button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" });
|
||||
* event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); });
|
||||
* event_loop.run();
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_interrupt(struct mjs* mjs) {
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// interrupt handling
|
||||
if(!manager_data->had_interrupt) {
|
||||
furi_hal_gpio_add_int_callback(
|
||||
manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore);
|
||||
furi_hal_gpio_enable_int_callback(manager_data->pin);
|
||||
manager_data->had_interrupt = true;
|
||||
}
|
||||
|
||||
// make contract
|
||||
JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract));
|
||||
*contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = manager_data->interrupt_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
},
|
||||
};
|
||||
manager_data->interrupt_contract = contract;
|
||||
mjs_return(mjs, mjs_mk_foreign(mjs, contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reads a voltage from a GPIO pin in analog mode
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let pot = gpio.get("pc0");
|
||||
* pot.init({ direction: "in", inMode: "analog" });
|
||||
* print("voltage:" pot.read_analog(), "mV");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_read_analog(struct mjs* mjs) {
|
||||
// get mV (ADC is configured for 12 bits and 2048 mV max)
|
||||
JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs);
|
||||
uint16_t millivolts =
|
||||
furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2;
|
||||
mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns an object that manages a specified pin.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```js
|
||||
* let gpio = require("gpio");
|
||||
* let led = gpio.get("pc3");
|
||||
* ```
|
||||
*/
|
||||
static void js_gpio_get(struct mjs* mjs) {
|
||||
mjs_val_t name_arg;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg));
|
||||
const char* name_string = mjs_get_string(mjs, &name_arg, NULL);
|
||||
const GpioPinRecord* pin_record = NULL;
|
||||
|
||||
// parse input argument to a pin pointer
|
||||
if(name_string) {
|
||||
pin_record = furi_hal_resources_pin_by_name(name_string);
|
||||
} else if(mjs_is_number(name_arg)) {
|
||||
int name_int = mjs_get_int(mjs, name_arg);
|
||||
pin_record = furi_hal_resources_pin_by_number(name_int);
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number");
|
||||
}
|
||||
|
||||
if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device");
|
||||
if(pin_record->debug)
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging");
|
||||
|
||||
// return pin manager object
|
||||
JsGpioInst* module = JS_GET_CONTEXT(mjs);
|
||||
mjs_val_t manager = mjs_mk_object(mjs);
|
||||
JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst));
|
||||
manager_data->pin = pin_record->pin;
|
||||
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_own(mjs, &manager);
|
||||
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, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog));
|
||||
mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt));
|
||||
mjs_return(mjs, manager);
|
||||
|
||||
// remember pin
|
||||
ManagedPinsArray_push_back(module->managed_pins, manager_data);
|
||||
}
|
||||
|
||||
static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||
|
||||
JsGpioInst* module = malloc(sizeof(JsGpioInst));
|
||||
ManagedPinsArray_init(module->managed_pins);
|
||||
module->adc_handle = furi_hal_adc_acquire();
|
||||
module->loop = loop;
|
||||
furi_hal_adc_configure(module->adc_handle);
|
||||
|
||||
mjs_val_t gpio_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module));
|
||||
mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get));
|
||||
*object = gpio_obj;
|
||||
|
||||
return (void*)module;
|
||||
}
|
||||
|
||||
static void js_gpio_destroy(void* inst) {
|
||||
furi_assert(inst);
|
||||
JsGpioInst* module = (JsGpioInst*)inst;
|
||||
|
||||
// reset pins
|
||||
ManagedPinsArray_it_t iterator;
|
||||
for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator);
|
||||
ManagedPinsArray_next(iterator)) {
|
||||
JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator);
|
||||
if(manager_data->had_interrupt) {
|
||||
furi_hal_gpio_disable_int_callback(manager_data->pin);
|
||||
furi_hal_gpio_remove_int_callback(manager_data->pin);
|
||||
}
|
||||
furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore);
|
||||
furi_semaphore_free(manager_data->interrupt_semaphore);
|
||||
free(manager_data->interrupt_contract);
|
||||
free(manager_data);
|
||||
}
|
||||
|
||||
// free buffers
|
||||
furi_hal_adc_release(module->adc_handle);
|
||||
ManagedPinsArray_clear(module->managed_pins);
|
||||
free(module);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_gpio_desc = {
|
||||
"gpio",
|
||||
js_gpio_create,
|
||||
js_gpio_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gpio_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gpio_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/dialog_ex.h>
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsDialogCtx;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
|
||||
UNUSED(context);
|
||||
DialogExResult result;
|
||||
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
|
||||
const char* string;
|
||||
if(result == DialogExResultLeft) {
|
||||
string = "left";
|
||||
} else if(result == DialogExResultCenter) {
|
||||
string = "center";
|
||||
} else if(result == DialogExResultRight) {
|
||||
string = "right";
|
||||
} else {
|
||||
furi_crash();
|
||||
}
|
||||
return mjs_mk_string(mjs, string, ~0, false);
|
||||
}
|
||||
|
||||
static void input_callback(DialogExResult result, JsDialogCtx* context) {
|
||||
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_left_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
static bool
|
||||
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_center_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
static bool
|
||||
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
dialog_ex_set_right_button_text(dialog, value.string);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
|
||||
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
|
||||
dialog_ex_set_context(dialog, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)dialog_ex_alloc,
|
||||
.free = (JsViewFree)dialog_ex_free,
|
||||
.get_view = (JsViewGetView)dialog_ex_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 5,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "text",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "left",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)left_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "center",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)center_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "right",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)right_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(dialog, &view_descriptor);
|
||||
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/empty_screen.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)empty_screen_alloc,
|
||||
.free = (JsViewFree)empty_screen_free,
|
||||
.get_view = (JsViewGetView)empty_screen_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);
|
||||
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
@@ -0,0 +1,348 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "./js_gui.h"
|
||||
#include <furi.h>
|
||||
#include <mlib/m-array.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <m-array.h>
|
||||
|
||||
#define EVENT_QUEUE_SIZE 16
|
||||
|
||||
typedef struct {
|
||||
uint32_t next_view_id;
|
||||
FuriEventLoop* loop;
|
||||
Gui* gui;
|
||||
ViewDispatcher* dispatcher;
|
||||
// event stuff
|
||||
JsEventLoopContract custom_contract;
|
||||
FuriMessageQueue* custom;
|
||||
JsEventLoopContract navigation_contract;
|
||||
FuriSemaphore*
|
||||
navigation; // FIXME: (-nofl) convert into callback once FuriEventLoop starts supporting this
|
||||
} JsGui;
|
||||
|
||||
// Useful for factories
|
||||
static JsGui* js_gui;
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
const JsViewDescriptor* descriptor;
|
||||
void* specific_view;
|
||||
void* custom_data;
|
||||
} JsGuiViewData;
|
||||
|
||||
/**
|
||||
* @brief Transformer for custom events
|
||||
*/
|
||||
static mjs_val_t
|
||||
js_gui_vd_custom_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
|
||||
UNUSED(context);
|
||||
furi_check(object);
|
||||
FuriMessageQueue* queue = object;
|
||||
uint32_t event;
|
||||
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
|
||||
return mjs_mk_number(mjs, (double)event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ViewDispatcher custom event callback
|
||||
*/
|
||||
static bool js_gui_vd_custom_callback(void* context, uint32_t event) {
|
||||
furi_check(context);
|
||||
JsGui* module = context;
|
||||
furi_check(furi_message_queue_put(module->custom, &event, 0) == FuriStatusOk);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ViewDispatcher navigation event callback
|
||||
*/
|
||||
static bool js_gui_vd_nav_callback(void* context) {
|
||||
furi_check(context);
|
||||
JsGui* module = context;
|
||||
furi_semaphore_release(module->navigation);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.sendCustom`
|
||||
*/
|
||||
static void js_gui_vd_send_custom(struct mjs* mjs) {
|
||||
int32_t event;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event));
|
||||
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.sendTo`
|
||||
*/
|
||||
static void js_gui_vd_send_to(struct mjs* mjs) {
|
||||
enum {
|
||||
SendDirToFront,
|
||||
SendDirToBack,
|
||||
} send_direction;
|
||||
JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack});
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection"));
|
||||
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
if(send_direction == SendDirToBack) {
|
||||
view_dispatcher_send_to_back(module->dispatcher);
|
||||
} else {
|
||||
view_dispatcher_send_to_front(module->dispatcher);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `viewDispatcher.switchTo`
|
||||
*/
|
||||
static void js_gui_vd_switch_to(struct mjs* mjs) {
|
||||
mjs_val_t view;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view));
|
||||
JsGuiViewData* view_data = JS_GET_INST(mjs, view);
|
||||
JsGui* module = JS_GET_CONTEXT(mjs);
|
||||
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
|
||||
}
|
||||
|
||||
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
// get event loop
|
||||
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
|
||||
if(M_UNLIKELY(!js_loop)) return NULL;
|
||||
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
|
||||
|
||||
// create C object
|
||||
JsGui* module = malloc(sizeof(JsGui));
|
||||
module->loop = loop;
|
||||
module->gui = furi_record_open(RECORD_GUI);
|
||||
module->dispatcher = view_dispatcher_alloc_ex(loop);
|
||||
module->custom = furi_message_queue_alloc(EVENT_QUEUE_SIZE, sizeof(uint32_t));
|
||||
module->navigation = furi_semaphore_alloc(EVENT_QUEUE_SIZE, 0);
|
||||
view_dispatcher_attach_to_gui(module->dispatcher, module->gui, ViewDispatcherTypeFullscreen);
|
||||
view_dispatcher_send_to_front(module->dispatcher);
|
||||
|
||||
// subscribe to events and create contracts
|
||||
view_dispatcher_set_event_callback_context(module->dispatcher, module);
|
||||
view_dispatcher_set_custom_event_callback(module->dispatcher, js_gui_vd_custom_callback);
|
||||
view_dispatcher_set_navigation_event_callback(module->dispatcher, js_gui_vd_nav_callback);
|
||||
module->custom_contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object = module->custom,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = js_gui_vd_custom_transformer,
|
||||
},
|
||||
};
|
||||
module->navigation_contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object = module->navigation,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
},
|
||||
};
|
||||
|
||||
// create viewDispatcher object
|
||||
mjs_val_t view_dispatcher = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, view_dispatcher) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
|
||||
JS_FIELD("sendCustom", MJS_MK_FN(js_gui_vd_send_custom));
|
||||
JS_FIELD("sendTo", MJS_MK_FN(js_gui_vd_send_to));
|
||||
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
|
||||
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
|
||||
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
|
||||
}
|
||||
|
||||
// create API object
|
||||
mjs_val_t api = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, api, "viewDispatcher", ~0, view_dispatcher);
|
||||
|
||||
*object = api;
|
||||
js_gui = module;
|
||||
return module;
|
||||
}
|
||||
|
||||
static void js_gui_destroy(void* inst) {
|
||||
furi_assert(inst);
|
||||
JsGui* module = inst;
|
||||
|
||||
view_dispatcher_free(module->dispatcher);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, module->custom);
|
||||
furi_event_loop_maybe_unsubscribe(module->loop, module->navigation);
|
||||
furi_message_queue_free(module->custom);
|
||||
furi_semaphore_free(module->navigation);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
free(module);
|
||||
js_gui = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Assigns a `View` property. Not available from JS.
|
||||
*/
|
||||
static bool
|
||||
js_gui_view_assign(struct mjs* mjs, const char* name, mjs_val_t value, JsGuiViewData* data) {
|
||||
const JsViewDescriptor* descriptor = data->descriptor;
|
||||
for(size_t i = 0; i < descriptor->prop_cnt; i++) {
|
||||
JsViewPropDescriptor prop = descriptor->props[i];
|
||||
if(strcmp(prop.name, name) != 0) continue;
|
||||
|
||||
// convert JS value to C
|
||||
JsViewPropValue c_value;
|
||||
const char* expected_type = NULL;
|
||||
switch(prop.type) {
|
||||
case JsViewPropTypeNumber: {
|
||||
if(!mjs_is_number(value)) {
|
||||
expected_type = "number";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.number = mjs_get_int32(mjs, value)};
|
||||
} break;
|
||||
case JsViewPropTypeString: {
|
||||
if(!mjs_is_string(value)) {
|
||||
expected_type = "string";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.string = mjs_get_string(mjs, &value, NULL)};
|
||||
} break;
|
||||
case JsViewPropTypeArr: {
|
||||
if(!mjs_is_array(value)) {
|
||||
expected_type = "array";
|
||||
break;
|
||||
}
|
||||
c_value = (JsViewPropValue){.array = value};
|
||||
} break;
|
||||
}
|
||||
|
||||
if(expected_type) {
|
||||
mjs_prepend_errorf(
|
||||
mjs, MJS_BAD_ARGS_ERROR, "view prop \"%s\" requires %s value", name, expected_type);
|
||||
return false;
|
||||
} else {
|
||||
return prop.assign(mjs, data->specific_view, c_value, data->custom_data);
|
||||
}
|
||||
}
|
||||
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "view has no prop named \"%s\"", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View.set`
|
||||
*/
|
||||
static void js_gui_view_set(struct mjs* mjs) {
|
||||
const char* name;
|
||||
mjs_val_t value;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value));
|
||||
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
|
||||
bool success = js_gui_view_assign(mjs, name, value, data);
|
||||
UNUSED(success);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `View` destructor
|
||||
*/
|
||||
static void js_gui_view_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
JsGuiViewData* data = JS_GET_INST(mjs, obj);
|
||||
view_dispatcher_remove_view(js_gui->dispatcher, data->id);
|
||||
if(data->descriptor->custom_destroy)
|
||||
data->descriptor->custom_destroy(data->specific_view, data->custom_data, js_gui->loop);
|
||||
data->descriptor->free(data->specific_view);
|
||||
free(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a `View` object from a descriptor. Not available from JS.
|
||||
*/
|
||||
static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descriptor) {
|
||||
void* specific_view = descriptor->alloc();
|
||||
View* view = descriptor->get_view(specific_view);
|
||||
uint32_t view_id = js_gui->next_view_id++;
|
||||
view_dispatcher_add_view(js_gui->dispatcher, view_id, view);
|
||||
|
||||
// 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));
|
||||
|
||||
// object data
|
||||
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
|
||||
*data = (JsGuiViewData){
|
||||
.descriptor = descriptor,
|
||||
.id = view_id,
|
||||
.specific_view = specific_view,
|
||||
.custom_data =
|
||||
descriptor->custom_make ? descriptor->custom_make(mjs, specific_view, view_obj) : NULL,
|
||||
};
|
||||
mjs_set(mjs, view_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, data));
|
||||
mjs_set(mjs, view_obj, MJS_DESTRUCTOR_PROP_NAME, ~0, MJS_MK_FN(js_gui_view_destructor));
|
||||
|
||||
return view_obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `ViewFactory.make`
|
||||
*/
|
||||
static void js_gui_vf_make(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, js_gui_make_view(mjs, descriptor));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief `ViewFactory.makeWith`
|
||||
*/
|
||||
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));
|
||||
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
|
||||
|
||||
// make the object like normal
|
||||
mjs_val_t view_obj = js_gui_make_view(mjs, descriptor);
|
||||
JsGuiViewData* data = JS_GET_INST(mjs, view_obj);
|
||||
|
||||
// assign properties one by one
|
||||
mjs_val_t key, iter = MJS_UNDEFINED;
|
||||
while((key = mjs_next(mjs, props, &iter)) != MJS_UNDEFINED) {
|
||||
furi_check(mjs_is_string(key));
|
||||
const char* name = mjs_get_string(mjs, &key, NULL);
|
||||
mjs_val_t value = mjs_get(mjs, props, name, ~0);
|
||||
|
||||
if(!js_gui_view_assign(mjs, name, value, data)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mjs_return(mjs, view_obj);
|
||||
}
|
||||
|
||||
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor) {
|
||||
mjs_val_t factory = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, factory, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, (void*)view_descriptor));
|
||||
mjs_set(mjs, factory, "make", ~0, MJS_MK_FN(js_gui_vf_make));
|
||||
mjs_set(mjs, factory, "makeWith", ~0, MJS_MK_FN(js_gui_vf_make_with));
|
||||
return factory;
|
||||
}
|
||||
|
||||
extern const ElfApiInterface js_gui_hashtable_api_interface;
|
||||
|
||||
static const JsModuleDescriptor js_gui_desc = {
|
||||
"gui",
|
||||
js_gui_create,
|
||||
js_gui_destroy,
|
||||
&js_gui_hashtable_api_interface,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_gui_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_gui_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "../../js_modules.h"
|
||||
#include <gui/view.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
JsViewPropTypeString,
|
||||
JsViewPropTypeNumber,
|
||||
JsViewPropTypeArr,
|
||||
} JsViewPropType;
|
||||
|
||||
typedef union {
|
||||
const char* string;
|
||||
int32_t number;
|
||||
mjs_val_t array;
|
||||
} JsViewPropValue;
|
||||
|
||||
/**
|
||||
* @brief Assigns a value to a view property
|
||||
*
|
||||
* The name and the type are implicit and defined in the property descriptor
|
||||
*/
|
||||
typedef bool (
|
||||
*JsViewPropAssign)(struct mjs* mjs, void* specific_view, JsViewPropValue value, void* context);
|
||||
|
||||
/** @brief Property descriptor */
|
||||
typedef struct {
|
||||
const char* name; //<! Property name, as visible from JS
|
||||
JsViewPropType type; // <! Property type, ensured by the GUI module
|
||||
JsViewPropAssign assign; // <! Property assignment callback
|
||||
} JsViewPropDescriptor;
|
||||
|
||||
// View method signatures
|
||||
|
||||
/** @brief View's `_alloc` method */
|
||||
typedef void* (*JsViewAlloc)(void);
|
||||
/** @brief View's `_get_view` method */
|
||||
typedef View* (*JsViewGetView)(void* specific_view);
|
||||
/** @brief View's `_free` method */
|
||||
typedef void (*JsViewFree)(void* specific_view);
|
||||
|
||||
// Glue code method signatures
|
||||
|
||||
/** @brief Context instantiation for glue code */
|
||||
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 Descriptor for a JS view
|
||||
*
|
||||
* Contains:
|
||||
* - Pointers to generic view methods (`alloc`, `get_view` and `free`)
|
||||
* - Pointers to glue code context ctor/dtor methods (`custom_make`,
|
||||
* `custom_destroy`)
|
||||
* - Descriptors of properties visible from JS (`prop_cnt`, `props`)
|
||||
*
|
||||
* `js_gui` uses this descriptor to produce view factories and views.
|
||||
*/
|
||||
typedef struct {
|
||||
JsViewAlloc alloc;
|
||||
JsViewGetView get_view;
|
||||
JsViewFree free;
|
||||
JsViewCustomMake custom_make; // <! May be NULL
|
||||
JsViewCustomDestroy custom_destroy; // <! 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 _________/
|
||||
|
||||
/**
|
||||
* @brief Creates a JS `ViewFactory` object
|
||||
*
|
||||
* This function is intended to be used by individual view adapter modules that
|
||||
* wish to create a unified JS API interface in a declarative way. Usually this
|
||||
* is done via the `JS_GUI_VIEW_DEF` macro which hides all the boilerplate.
|
||||
*
|
||||
* The `ViewFactory` object exposes two methods, `make` and `makeWith`, each
|
||||
* returning a `View` object. These objects fully comply with the expectations
|
||||
* of the `ViewDispatcher`, TS type definitions and the proposed Flipper JS
|
||||
* coding style.
|
||||
*/
|
||||
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor);
|
||||
|
||||
/**
|
||||
* @brief Defines a module implementing `View` glue code
|
||||
*/
|
||||
#define JS_GUI_VIEW_DEF(name, descriptor) \
|
||||
static void* view_mod_ctor(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { \
|
||||
UNUSED(modules); \
|
||||
*object = js_gui_make_view_factory(mjs, descriptor); \
|
||||
return NULL; \
|
||||
} \
|
||||
static const JsModuleDescriptor js_mod_desc = { \
|
||||
"gui__" #name, \
|
||||
view_mod_ctor, \
|
||||
NULL, \
|
||||
NULL, \
|
||||
}; \
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = { \
|
||||
.appid = PLUGIN_APP_ID, \
|
||||
.ep_api_version = PLUGIN_API_VERSION, \
|
||||
.entry_point = &js_mod_desc, \
|
||||
}; \
|
||||
const FlipperAppPluginDescriptor* js_view_##name##_ep(void) { \
|
||||
return &plugin_descriptor; \
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
#include <flipper_application/api_hashtable/api_hashtable.h>
|
||||
#include <flipper_application/api_hashtable/compilesort.hpp>
|
||||
|
||||
#include "js_gui_api_table_i.h"
|
||||
|
||||
static_assert(!has_hash_collisions(js_gui_api_table), "Detected API method hash collision!");
|
||||
|
||||
extern "C" constexpr HashtableApiInterface js_gui_hashtable_api_interface{
|
||||
{
|
||||
.api_version_major = 0,
|
||||
.api_version_minor = 0,
|
||||
.resolver_callback = &elf_resolve_from_hashtable,
|
||||
},
|
||||
js_gui_api_table.cbegin(),
|
||||
js_gui_api_table.cend(),
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "js_gui.h"
|
||||
|
||||
static constexpr auto js_gui_api_table = sort(create_array_t<sym_entry>(
|
||||
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*))));
|
||||
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/loading.h>
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)loading_alloc,
|
||||
.free = (JsViewFree)loading_free,
|
||||
.get_view = (JsViewGetView)loading_get_view,
|
||||
.prop_cnt = 0,
|
||||
.props = {},
|
||||
};
|
||||
JS_GUI_VIEW_DEF(loading, &view_descriptor);
|
||||
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/submenu.h>
|
||||
|
||||
#define QUEUE_LEN 2
|
||||
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue;
|
||||
JsEventLoopContract contract;
|
||||
} JsSubmenuCtx;
|
||||
|
||||
static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) {
|
||||
UNUSED(context);
|
||||
uint32_t index;
|
||||
furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk);
|
||||
return mjs_mk_number(mjs, (double)index);
|
||||
}
|
||||
|
||||
void choose_callback(void* context, uint32_t index) {
|
||||
JsSubmenuCtx* ctx = context;
|
||||
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
submenu_set_header(submenu, value.string);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
|
||||
UNUSED(mjs);
|
||||
submenu_reset(submenu);
|
||||
size_t len = mjs_array_length(mjs, value.array);
|
||||
for(size_t i = 0; i < len; i++) {
|
||||
mjs_val_t item = mjs_array_get(mjs, value.array, i);
|
||||
if(!mjs_is_string(item)) return false;
|
||||
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
|
||||
UNUSED(input);
|
||||
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
|
||||
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(uint32_t));
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeQueue,
|
||||
.object = context->queue,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)choose_transformer,
|
||||
},
|
||||
};
|
||||
mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(Submenu* input, JsSubmenuCtx* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
||||
furi_message_queue_free(context->queue);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)submenu_alloc,
|
||||
.free = (JsViewFree)submenu_free,
|
||||
.get_view = (JsViewGetView)submenu_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 2,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "items",
|
||||
.type = JsViewPropTypeArr,
|
||||
.assign = (JsViewPropAssign)items_assign},
|
||||
}};
|
||||
JS_GUI_VIEW_DEF(submenu, &view_descriptor);
|
||||
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include <gui/modules/text_box.h>
|
||||
|
||||
static bool
|
||||
text_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, FuriString* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context, value.string);
|
||||
text_box_set_text(text_box, furi_string_get_cstr(context));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool font_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
|
||||
UNUSED(context);
|
||||
TextBoxFont font;
|
||||
if(strcasecmp(value.string, "hex") == 0) {
|
||||
font = TextBoxFontHex;
|
||||
} else if(strcasecmp(value.string, "text") == 0) {
|
||||
font = TextBoxFontText;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"text\", \"hex\"");
|
||||
return false;
|
||||
}
|
||||
text_box_set_font(text_box, font);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
focus_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
|
||||
UNUSED(context);
|
||||
TextBoxFocus focus;
|
||||
if(strcasecmp(value.string, "start") == 0) {
|
||||
focus = TextBoxFocusStart;
|
||||
} else if(strcasecmp(value.string, "end") == 0) {
|
||||
focus = TextBoxFocusEnd;
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"start\", \"end\"");
|
||||
return false;
|
||||
}
|
||||
text_box_set_focus(text_box, focus);
|
||||
return true;
|
||||
}
|
||||
|
||||
FuriString* ctx_make(struct mjs* mjs, TextBox* specific_view, mjs_val_t view_obj) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(specific_view);
|
||||
UNUSED(view_obj);
|
||||
return furi_string_alloc();
|
||||
}
|
||||
|
||||
void ctx_destroy(TextBox* specific_view, FuriString* context, FuriEventLoop* loop) {
|
||||
UNUSED(specific_view);
|
||||
UNUSED(loop);
|
||||
furi_string_free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)text_box_alloc,
|
||||
.free = (JsViewFree)text_box_free,
|
||||
.get_view = (JsViewGetView)text_box_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 3,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "text",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)text_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "font",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)font_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "focus",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)focus_assign},
|
||||
}};
|
||||
JS_GUI_VIEW_DEF(text_box, &view_descriptor);
|
||||
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "../../js_modules.h" // IWYU pragma: keep
|
||||
#include "js_gui.h"
|
||||
#include "../js_event_loop/js_event_loop.h"
|
||||
#include <gui/modules/text_input.h>
|
||||
|
||||
#define DEFAULT_BUF_SZ 33
|
||||
|
||||
typedef struct {
|
||||
char* buffer;
|
||||
size_t buffer_size;
|
||||
FuriString* header;
|
||||
FuriSemaphore* input_semaphore;
|
||||
JsEventLoopContract contract;
|
||||
} JsKbdContext;
|
||||
|
||||
static mjs_val_t
|
||||
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsKbdContext* context) {
|
||||
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
|
||||
return mjs_mk_string(mjs, context->buffer, ~0, true);
|
||||
}
|
||||
|
||||
static void input_callback(JsKbdContext* context) {
|
||||
furi_semaphore_release(context->input_semaphore);
|
||||
}
|
||||
|
||||
static bool
|
||||
header_assign(struct mjs* mjs, TextInput* input, JsViewPropValue value, JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
furi_string_set(context->header, value.string);
|
||||
text_input_set_header_text(input, furi_string_get_cstr(context->header));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool min_len_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
UNUSED(context);
|
||||
text_input_set_minimum_length(input, (size_t)value.number);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool max_len_assign(
|
||||
struct mjs* mjs,
|
||||
TextInput* input,
|
||||
JsViewPropValue value,
|
||||
JsKbdContext* context) {
|
||||
UNUSED(mjs);
|
||||
context->buffer_size = (size_t)(value.number + 1);
|
||||
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
|
||||
text_input_set_result_callback(
|
||||
input,
|
||||
(TextInputCallback)input_callback,
|
||||
context,
|
||||
context->buffer,
|
||||
context->buffer_size,
|
||||
true);
|
||||
return true;
|
||||
}
|
||||
|
||||
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
|
||||
UNUSED(input);
|
||||
JsKbdContext* context = malloc(sizeof(JsKbdContext));
|
||||
*context = (JsKbdContext){
|
||||
.buffer_size = DEFAULT_BUF_SZ,
|
||||
.buffer = malloc(DEFAULT_BUF_SZ),
|
||||
.header = furi_string_alloc(),
|
||||
.input_semaphore = furi_semaphore_alloc(1, 0),
|
||||
};
|
||||
context->contract = (JsEventLoopContract){
|
||||
.magic = JsForeignMagic_JsEventLoopContract,
|
||||
.object_type = JsEventLoopObjectTypeSemaphore,
|
||||
.object = context->input_semaphore,
|
||||
.non_timer =
|
||||
{
|
||||
.event = FuriEventLoopEventIn,
|
||||
.transformer = (JsEventLoopTransformer)input_transformer,
|
||||
.transformer_context = context,
|
||||
},
|
||||
};
|
||||
UNUSED(mjs);
|
||||
UNUSED(view_obj);
|
||||
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
|
||||
return context;
|
||||
}
|
||||
|
||||
static void ctx_destroy(TextInput* input, JsKbdContext* context, FuriEventLoop* loop) {
|
||||
UNUSED(input);
|
||||
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
|
||||
furi_semaphore_free(context->input_semaphore);
|
||||
furi_string_free(context->header);
|
||||
free(context->buffer);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static const JsViewDescriptor view_descriptor = {
|
||||
.alloc = (JsViewAlloc)text_input_alloc,
|
||||
.free = (JsViewFree)text_input_free,
|
||||
.get_view = (JsViewGetView)text_input_get_view,
|
||||
.custom_make = (JsViewCustomMake)ctx_make,
|
||||
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
|
||||
.prop_cnt = 3,
|
||||
.props = {
|
||||
(JsViewPropDescriptor){
|
||||
.name = "header",
|
||||
.type = JsViewPropTypeString,
|
||||
.assign = (JsViewPropAssign)header_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "minLength",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)min_len_assign},
|
||||
(JsViewPropDescriptor){
|
||||
.name = "maxLength",
|
||||
.type = JsViewPropTypeNumber,
|
||||
.assign = (JsViewPropAssign)max_len_assign},
|
||||
}};
|
||||
|
||||
JS_GUI_VIEW_DEF(text_input, &view_descriptor);
|
||||
@@ -305,7 +305,8 @@ void js_math_trunc(struct mjs* mjs) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x)));
|
||||
}
|
||||
|
||||
static void* js_math_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t math_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal));
|
||||
mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs));
|
||||
@@ -342,6 +343,7 @@ static const JsModuleDescriptor js_math_desc = {
|
||||
"math",
|
||||
js_math_create,
|
||||
NULL,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -75,7 +75,8 @@ static void js_notify_blink(struct mjs* mjs) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_notification_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
mjs_val_t notify_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, notify_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, notification));
|
||||
@@ -96,6 +97,7 @@ static const JsModuleDescriptor js_notification_desc = {
|
||||
"notification",
|
||||
js_notification_create,
|
||||
js_notification_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
@@ -573,7 +573,8 @@ static void js_serial_expect(struct mjs* mjs) {
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
JsSerialInst* js_serial = malloc(sizeof(JsSerialInst));
|
||||
js_serial->mjs = mjs;
|
||||
mjs_val_t serial_obj = mjs_mk_object(mjs);
|
||||
@@ -606,6 +607,7 @@ static const JsModuleDescriptor js_serial_desc = {
|
||||
"serial",
|
||||
js_serial_create,
|
||||
js_serial_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
|
||||
383
applications/system/js_app/modules/js_storage.c
Normal file
383
applications/system/js_app/modules/js_storage.c
Normal file
@@ -0,0 +1,383 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include <path.h>
|
||||
|
||||
// ---=== file ops ===---
|
||||
|
||||
static void js_storage_file_close(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_is_open(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_read(struct mjs* mjs) {
|
||||
enum {
|
||||
ReadModeAscii,
|
||||
ReadModeBinary,
|
||||
} read_mode;
|
||||
JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary});
|
||||
int32_t length;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
char buffer[length];
|
||||
size_t actually_read = storage_file_read(file, buffer, length);
|
||||
if(read_mode == ReadModeAscii) {
|
||||
mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true));
|
||||
} else if(read_mode == ReadModeBinary) {
|
||||
mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read));
|
||||
}
|
||||
}
|
||||
|
||||
static void js_storage_file_write(struct mjs* mjs) {
|
||||
mjs_val_t data;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data));
|
||||
const void* buf;
|
||||
size_t len;
|
||||
if(mjs_is_string(data)) {
|
||||
buf = mjs_get_string(mjs, &data, &len);
|
||||
} else if(mjs_is_array_buf(data)) {
|
||||
buf = mjs_array_buf_get_ptr(mjs, data, &len);
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: expected string or ArrayBuffer");
|
||||
}
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len)));
|
||||
}
|
||||
|
||||
static void js_storage_file_seek_relative(struct mjs* mjs) {
|
||||
int32_t offset;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false)));
|
||||
}
|
||||
|
||||
static void js_storage_file_seek_absolute(struct mjs* mjs) {
|
||||
int32_t offset;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset));
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true)));
|
||||
}
|
||||
|
||||
static void js_storage_file_tell(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_truncate(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_size(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_eof(struct mjs* mjs) {
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
|
||||
File* file = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file)));
|
||||
}
|
||||
|
||||
static void js_storage_file_copy_to(struct mjs* mjs) {
|
||||
File* source = JS_GET_CONTEXT(mjs);
|
||||
mjs_val_t dest_obj;
|
||||
int32_t bytes;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes));
|
||||
File* destination = JS_GET_INST(mjs, dest_obj);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes)));
|
||||
}
|
||||
|
||||
// ---=== top-level file ops ===---
|
||||
|
||||
// common destructor for file and dir objects
|
||||
static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) {
|
||||
File* file = JS_GET_INST(mjs, obj);
|
||||
storage_file_free(file);
|
||||
}
|
||||
|
||||
static void js_storage_open_file(struct mjs* mjs) {
|
||||
const char* path;
|
||||
FS_AccessMode access_mode;
|
||||
FS_OpenMode open_mode;
|
||||
JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE});
|
||||
JS_ENUM_MAP(
|
||||
open_mode,
|
||||
{"open_existing", FSOM_OPEN_EXISTING},
|
||||
{"open_always", FSOM_OPEN_ALWAYS},
|
||||
{"open_append", FSOM_OPEN_APPEND},
|
||||
{"create_new", FSOM_CREATE_NEW},
|
||||
{"create_always", FSOM_CREATE_ALWAYS});
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs,
|
||||
JS_EXACTLY,
|
||||
JS_ARG_STR(&path),
|
||||
JS_ARG_ENUM(access_mode, "AccessMode"),
|
||||
JS_ARG_ENUM(open_mode, "OpenMode"));
|
||||
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(!storage_file_open(file, path, access_mode, open_mode)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t file_obj = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, file_obj) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, file));
|
||||
JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_file_destructor));
|
||||
JS_FIELD("close", MJS_MK_FN(js_storage_file_close));
|
||||
JS_FIELD("isOpen", MJS_MK_FN(js_storage_file_is_open));
|
||||
JS_FIELD("read", MJS_MK_FN(js_storage_file_read));
|
||||
JS_FIELD("write", MJS_MK_FN(js_storage_file_write));
|
||||
JS_FIELD("seekRelative", MJS_MK_FN(js_storage_file_seek_relative));
|
||||
JS_FIELD("seekAbsolute", MJS_MK_FN(js_storage_file_seek_absolute));
|
||||
JS_FIELD("tell", MJS_MK_FN(js_storage_file_tell));
|
||||
JS_FIELD("truncate", MJS_MK_FN(js_storage_file_truncate));
|
||||
JS_FIELD("size", MJS_MK_FN(js_storage_file_size));
|
||||
JS_FIELD("eof", MJS_MK_FN(js_storage_file_eof));
|
||||
JS_FIELD("copyTo", MJS_MK_FN(js_storage_file_copy_to));
|
||||
}
|
||||
mjs_return(mjs, file_obj);
|
||||
}
|
||||
|
||||
static void js_storage_file_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path)));
|
||||
}
|
||||
|
||||
// ---=== dir ops ===---
|
||||
|
||||
static void js_storage_read_directory(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
File* dir = storage_file_alloc(storage);
|
||||
if(!storage_dir_open(dir, path)) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
FileInfo file_info;
|
||||
char name[128];
|
||||
FuriString* file_path = furi_string_alloc_set_str(path);
|
||||
size_t path_size = furi_string_size(file_path);
|
||||
uint32_t timestamp;
|
||||
|
||||
mjs_val_t ret = mjs_mk_array(mjs);
|
||||
while(storage_dir_read(dir, &file_info, name, sizeof(name))) {
|
||||
furi_string_left(file_path, path_size);
|
||||
path_append(file_path, name);
|
||||
furi_check(
|
||||
storage_common_timestamp(storage, furi_string_get_cstr(file_path), ×tamp) ==
|
||||
FSE_OK);
|
||||
mjs_val_t obj = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, obj) {
|
||||
JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true));
|
||||
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
|
||||
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
|
||||
JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp));
|
||||
}
|
||||
mjs_array_push(mjs, ret, obj);
|
||||
}
|
||||
|
||||
storage_file_free(dir);
|
||||
furi_string_free(file_path);
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_directory_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_make_directory(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path)));
|
||||
}
|
||||
|
||||
// ---=== common ops ===---
|
||||
|
||||
static void js_storage_file_or_dir_exists(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_stat(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FileInfo file_info;
|
||||
uint32_t timestamp;
|
||||
if((storage_common_stat(storage, path, &file_info) |
|
||||
storage_common_timestamp(storage, path, ×tamp)) != FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
mjs_val_t ret = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, ret) {
|
||||
JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1));
|
||||
JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info)));
|
||||
JS_FIELD("size", mjs_mk_number(mjs, file_info.size));
|
||||
JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp));
|
||||
}
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_remove(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_rmrf(struct mjs* mjs) {
|
||||
const char* path;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path)));
|
||||
}
|
||||
|
||||
static void js_storage_rename(struct mjs* mjs) {
|
||||
const char *old, *new;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FS_Error status = storage_common_rename(storage, old, new);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK));
|
||||
}
|
||||
|
||||
static void js_storage_copy(struct mjs* mjs) {
|
||||
const char *source, *dest;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FS_Error status = storage_common_copy(storage, source, dest);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST));
|
||||
}
|
||||
|
||||
static void js_storage_fs_info(struct mjs* mjs) {
|
||||
const char* fs;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
uint64_t total_space, free_space;
|
||||
if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
mjs_val_t ret = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, ret) {
|
||||
JS_FIELD("totalSpace", mjs_mk_number(mjs, total_space));
|
||||
JS_FIELD("freeSpace", mjs_mk_number(mjs, free_space));
|
||||
}
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
static void js_storage_next_available_filename(struct mjs* mjs) {
|
||||
const char *dir_path, *file_name, *file_ext;
|
||||
int32_t max_len;
|
||||
JS_FETCH_ARGS_OR_RETURN(
|
||||
mjs,
|
||||
JS_EXACTLY,
|
||||
JS_ARG_STR(&dir_path),
|
||||
JS_ARG_STR(&file_name),
|
||||
JS_ARG_STR(&file_ext),
|
||||
JS_ARG_INT32(&max_len));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
FuriString* next_name = furi_string_alloc();
|
||||
storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len);
|
||||
mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(next_name), ~0, true));
|
||||
furi_string_free(next_name);
|
||||
}
|
||||
|
||||
// ---=== path ops ===---
|
||||
|
||||
static void js_storage_are_paths_equal(struct mjs* mjs) {
|
||||
const char *path1, *path2;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2)));
|
||||
}
|
||||
|
||||
static void js_storage_is_subpath_of(struct mjs* mjs) {
|
||||
const char *parent, *child;
|
||||
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&child));
|
||||
Storage* storage = JS_GET_CONTEXT(mjs);
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child)));
|
||||
}
|
||||
|
||||
// ---=== module ctor & dtor ===---
|
||||
|
||||
static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
UNUSED(storage);
|
||||
*object = mjs_mk_object(mjs);
|
||||
JS_ASSIGN_MULTI(mjs, *object) {
|
||||
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, storage));
|
||||
|
||||
// top-level file ops
|
||||
JS_FIELD("openFile", MJS_MK_FN(js_storage_open_file));
|
||||
JS_FIELD("fileExists", MJS_MK_FN(js_storage_file_exists));
|
||||
|
||||
// dir ops
|
||||
JS_FIELD("readDirectory", MJS_MK_FN(js_storage_read_directory));
|
||||
JS_FIELD("directoryExists", MJS_MK_FN(js_storage_directory_exists));
|
||||
JS_FIELD("makeDirectory", MJS_MK_FN(js_storage_make_directory));
|
||||
|
||||
// common ops
|
||||
JS_FIELD("fileOrDirExists", MJS_MK_FN(js_storage_file_or_dir_exists));
|
||||
JS_FIELD("stat", MJS_MK_FN(js_storage_stat));
|
||||
JS_FIELD("remove", MJS_MK_FN(js_storage_remove));
|
||||
JS_FIELD("rmrf", MJS_MK_FN(js_storage_rmrf));
|
||||
JS_FIELD("rename", MJS_MK_FN(js_storage_rename));
|
||||
JS_FIELD("copy", MJS_MK_FN(js_storage_copy));
|
||||
JS_FIELD("fsInfo", MJS_MK_FN(js_storage_fs_info));
|
||||
JS_FIELD("nextAvailableFilename", MJS_MK_FN(js_storage_next_available_filename));
|
||||
|
||||
// path ops
|
||||
JS_FIELD("arePathsEqual", MJS_MK_FN(js_storage_are_paths_equal));
|
||||
JS_FIELD("isSubpathOf", MJS_MK_FN(js_storage_is_subpath_of));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void js_storage_destroy(void* data) {
|
||||
UNUSED(data);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
// ---=== boilerplate ===---
|
||||
|
||||
static const JsModuleDescriptor js_storage_desc = {
|
||||
"storage",
|
||||
js_storage_create,
|
||||
js_storage_destroy,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_storage_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_storage_ep(void) {
|
||||
return &plugin_descriptor;
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include <gui/view.h>
|
||||
#include <toolbox/api_lock.h>
|
||||
#include "../js_modules.h"
|
||||
|
||||
typedef struct {
|
||||
Submenu* submenu;
|
||||
ViewHolder* view_holder;
|
||||
FuriApiLock lock;
|
||||
uint32_t result;
|
||||
bool accepted;
|
||||
} JsSubmenuInst;
|
||||
|
||||
static JsSubmenuInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsSubmenuInst* submenu = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(submenu);
|
||||
return submenu;
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void submenu_callback(void* context, uint32_t id) {
|
||||
JsSubmenuInst* submenu = context;
|
||||
submenu->result = id;
|
||||
submenu->accepted = true;
|
||||
api_lock_unlock(submenu->lock);
|
||||
}
|
||||
|
||||
static void submenu_exit(void* context) {
|
||||
JsSubmenuInst* submenu = context;
|
||||
submenu->result = 0;
|
||||
submenu->accepted = false;
|
||||
api_lock_unlock(submenu->lock);
|
||||
}
|
||||
|
||||
static void js_submenu_add_item(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
mjs_val_t label_arg = mjs_arg(mjs, 0);
|
||||
const char* label = mjs_get_string(mjs, &label_arg, NULL);
|
||||
if(!label) {
|
||||
ret_bad_args(mjs, "Label must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
mjs_val_t id_arg = mjs_arg(mjs, 1);
|
||||
if(!mjs_is_number(id_arg)) {
|
||||
ret_bad_args(mjs, "Id must be a number");
|
||||
return;
|
||||
}
|
||||
int32_t id = mjs_get_int32(mjs, id_arg);
|
||||
|
||||
submenu_add_item(submenu->submenu, label, id, submenu_callback, submenu);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_submenu_set_header(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
mjs_val_t header_arg = mjs_arg(mjs, 0);
|
||||
const char* header = mjs_get_string(mjs, &header_arg, NULL);
|
||||
if(!header) {
|
||||
ret_bad_args(mjs, "Header must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
submenu_set_header(submenu->submenu, header);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_submenu_show(struct mjs* mjs) {
|
||||
JsSubmenuInst* submenu = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
submenu->lock = api_lock_alloc_locked();
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
submenu->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(submenu->view_holder, gui);
|
||||
view_holder_set_back_callback(submenu->view_holder, submenu_exit, submenu);
|
||||
|
||||
view_holder_set_view(submenu->view_holder, submenu_get_view(submenu->submenu));
|
||||
api_lock_wait_unlock(submenu->lock);
|
||||
|
||||
view_holder_set_view(submenu->view_holder, NULL);
|
||||
view_holder_free(submenu->view_holder);
|
||||
furi_record_close(RECORD_GUI);
|
||||
api_lock_free(submenu->lock);
|
||||
|
||||
submenu_reset(submenu->submenu);
|
||||
if(submenu->accepted) {
|
||||
mjs_return(mjs, mjs_mk_number(mjs, submenu->result));
|
||||
} else {
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
}
|
||||
|
||||
static void* js_submenu_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsSubmenuInst* submenu = malloc(sizeof(JsSubmenuInst));
|
||||
mjs_val_t submenu_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, submenu_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, submenu));
|
||||
mjs_set(mjs, submenu_obj, "addItem", ~0, MJS_MK_FN(js_submenu_add_item));
|
||||
mjs_set(mjs, submenu_obj, "setHeader", ~0, MJS_MK_FN(js_submenu_set_header));
|
||||
mjs_set(mjs, submenu_obj, "show", ~0, MJS_MK_FN(js_submenu_show));
|
||||
submenu->submenu = submenu_alloc();
|
||||
*object = submenu_obj;
|
||||
return submenu;
|
||||
}
|
||||
|
||||
static void js_submenu_destroy(void* inst) {
|
||||
JsSubmenuInst* submenu = inst;
|
||||
submenu_free(submenu->submenu);
|
||||
free(submenu);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_submenu_desc = {
|
||||
"submenu",
|
||||
js_submenu_create,
|
||||
js_submenu_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor submenu_plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_submenu_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_submenu_ep(void) {
|
||||
return &submenu_plugin_descriptor;
|
||||
}
|
||||
104
applications/system/js_app/modules/js_tests.c
Normal file
104
applications/system/js_app/modules/js_tests.c
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "../js_modules.h" // IWYU pragma: keep
|
||||
#include <core/common_defines.h>
|
||||
#include <furi_hal_version.h>
|
||||
#include <power/power_service/power.h>
|
||||
|
||||
#define TAG "JsTests"
|
||||
|
||||
static void js_tests_fail(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 1);
|
||||
mjs_val_t message_arg = mjs_arg(mjs, 0);
|
||||
const char* message = mjs_get_string(mjs, &message_arg, NULL);
|
||||
furi_check(message);
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", message);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_tests_assert_eq(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 2);
|
||||
|
||||
mjs_val_t expected_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t result_arg = mjs_arg(mjs, 1);
|
||||
|
||||
if(mjs_is_number(expected_arg) && mjs_is_number(result_arg)) {
|
||||
int32_t expected = mjs_get_int32(mjs, expected_arg);
|
||||
int32_t result = mjs_get_int32(mjs, result_arg);
|
||||
if(expected == result) {
|
||||
FURI_LOG_T(TAG, "eq passed (exp=%ld res=%ld)", expected, result);
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "expected %d, found %d", expected, result);
|
||||
}
|
||||
} else if(mjs_is_string(expected_arg) && mjs_is_string(result_arg)) {
|
||||
const char* expected = mjs_get_string(mjs, &expected_arg, NULL);
|
||||
const char* result = mjs_get_string(mjs, &result_arg, NULL);
|
||||
if(strcmp(expected, result) == 0) {
|
||||
FURI_LOG_T(TAG, "eq passed (exp=\"%s\" res=\"%s\")", expected, result);
|
||||
} else {
|
||||
mjs_prepend_errorf(
|
||||
mjs, MJS_INTERNAL_ERROR, "expected \"%s\", found \"%s\"", expected, result);
|
||||
}
|
||||
} else if(mjs_is_boolean(expected_arg) && mjs_is_boolean(result_arg)) {
|
||||
bool expected = mjs_get_bool(mjs, expected_arg);
|
||||
bool result = mjs_get_bool(mjs, result_arg);
|
||||
if(expected == result) {
|
||||
FURI_LOG_T(
|
||||
TAG,
|
||||
"eq passed (exp=%s res=%s)",
|
||||
expected ? "true" : "false",
|
||||
result ? "true" : "false");
|
||||
} else {
|
||||
mjs_prepend_errorf(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"expected %s, found %s",
|
||||
expected ? "true" : "false",
|
||||
result ? "true" : "false");
|
||||
}
|
||||
} else {
|
||||
JS_ERROR_AND_RETURN(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"type mismatch (expected %s, result %s)",
|
||||
mjs_typeof(expected_arg),
|
||||
mjs_typeof(result_arg));
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_tests_assert_float_close(struct mjs* mjs) {
|
||||
furi_check(mjs_nargs(mjs) == 3);
|
||||
|
||||
mjs_val_t expected_arg = mjs_arg(mjs, 0);
|
||||
mjs_val_t result_arg = mjs_arg(mjs, 1);
|
||||
mjs_val_t epsilon_arg = mjs_arg(mjs, 2);
|
||||
furi_check(mjs_is_number(expected_arg));
|
||||
furi_check(mjs_is_number(result_arg));
|
||||
furi_check(mjs_is_number(epsilon_arg));
|
||||
double expected = mjs_get_double(mjs, expected_arg);
|
||||
double result = mjs_get_double(mjs, result_arg);
|
||||
double epsilon = mjs_get_double(mjs, epsilon_arg);
|
||||
|
||||
if(ABS(expected - result) > epsilon) {
|
||||
mjs_prepend_errorf(
|
||||
mjs,
|
||||
MJS_INTERNAL_ERROR,
|
||||
"expected %f found %f (tolerance=%f)",
|
||||
expected,
|
||||
result,
|
||||
epsilon);
|
||||
}
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
|
||||
UNUSED(modules);
|
||||
mjs_val_t tests_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, tests_obj, "fail", ~0, MJS_MK_FN(js_tests_fail));
|
||||
mjs_set(mjs, tests_obj, "assert_eq", ~0, MJS_MK_FN(js_tests_assert_eq));
|
||||
mjs_set(mjs, tests_obj, "assert_float_close", ~0, MJS_MK_FN(js_tests_assert_float_close));
|
||||
*object = tests_obj;
|
||||
|
||||
return (void*)1;
|
||||
}
|
||||
5
applications/system/js_app/modules/js_tests.h
Normal file
5
applications/system/js_app/modules/js_tests.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include "../js_thread_i.h"
|
||||
#include "../js_modules.h"
|
||||
|
||||
void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules);
|
||||
@@ -1,219 +0,0 @@
|
||||
#include <gui/modules/text_box.h>
|
||||
#include <gui/view_holder.h>
|
||||
#include "../js_modules.h"
|
||||
|
||||
typedef struct {
|
||||
TextBox* text_box;
|
||||
ViewHolder* view_holder;
|
||||
FuriString* text;
|
||||
bool is_shown;
|
||||
} JsTextboxInst;
|
||||
|
||||
static JsTextboxInst* get_this_ctx(struct mjs* mjs) {
|
||||
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
|
||||
JsTextboxInst* textbox = mjs_get_ptr(mjs, obj_inst);
|
||||
furi_assert(textbox);
|
||||
return textbox;
|
||||
}
|
||||
|
||||
static void ret_bad_args(struct mjs* mjs, const char* error) {
|
||||
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error);
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static bool check_arg_count(struct mjs* mjs, size_t count) {
|
||||
size_t num_args = mjs_nargs(mjs);
|
||||
if(num_args != count) {
|
||||
ret_bad_args(mjs, "Wrong argument count");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void js_textbox_set_config(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 2)) return;
|
||||
|
||||
TextBoxFocus set_focus = TextBoxFocusStart;
|
||||
mjs_val_t focus_arg = mjs_arg(mjs, 0);
|
||||
const char* focus = mjs_get_string(mjs, &focus_arg, NULL);
|
||||
if(!focus) {
|
||||
ret_bad_args(mjs, "Focus must be a string");
|
||||
return;
|
||||
} else {
|
||||
if(!strncmp(focus, "start", strlen("start"))) {
|
||||
set_focus = TextBoxFocusStart;
|
||||
} else if(!strncmp(focus, "end", strlen("end"))) {
|
||||
set_focus = TextBoxFocusEnd;
|
||||
} else {
|
||||
ret_bad_args(mjs, "Bad focus value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TextBoxFont set_font = TextBoxFontText;
|
||||
mjs_val_t font_arg = mjs_arg(mjs, 1);
|
||||
const char* font = mjs_get_string(mjs, &font_arg, NULL);
|
||||
if(!font) {
|
||||
ret_bad_args(mjs, "Font must be a string");
|
||||
return;
|
||||
} else {
|
||||
if(!strncmp(font, "text", strlen("text"))) {
|
||||
set_font = TextBoxFontText;
|
||||
} else if(!strncmp(font, "hex", strlen("hex"))) {
|
||||
set_font = TextBoxFontHex;
|
||||
} else {
|
||||
ret_bad_args(mjs, "Bad font value");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
text_box_set_focus(textbox->text_box, set_focus);
|
||||
text_box_set_font(textbox->text_box, set_font);
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_add_text(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 1)) return;
|
||||
|
||||
mjs_val_t text_arg = mjs_arg(mjs, 0);
|
||||
size_t text_len = 0;
|
||||
const char* text = mjs_get_string(mjs, &text_arg, &text_len);
|
||||
if(!text) {
|
||||
ret_bad_args(mjs, "Text must be a string");
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid condition race between GUI and JS thread
|
||||
text_box_set_text(textbox->text_box, "");
|
||||
|
||||
size_t new_len = furi_string_size(textbox->text) + text_len;
|
||||
if(new_len >= 4096) {
|
||||
furi_string_right(textbox->text, new_len / 2);
|
||||
}
|
||||
|
||||
furi_string_cat(textbox->text, text);
|
||||
|
||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_clear_text(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
// Avoid condition race between GUI and JS thread
|
||||
text_box_set_text(textbox->text_box, "");
|
||||
|
||||
furi_string_reset(textbox->text);
|
||||
|
||||
text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text));
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_is_open(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
mjs_return(mjs, mjs_mk_boolean(mjs, textbox->is_shown));
|
||||
}
|
||||
|
||||
static void textbox_callback(void* context, uint32_t arg) {
|
||||
UNUSED(arg);
|
||||
JsTextboxInst* textbox = context;
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
textbox->is_shown = false;
|
||||
}
|
||||
|
||||
static void textbox_exit(void* context) {
|
||||
JsTextboxInst* textbox = context;
|
||||
// Using timer to schedule view_holder stop, will not work under high CPU load
|
||||
furi_timer_pending_callback(textbox_callback, textbox, 0);
|
||||
}
|
||||
|
||||
static void js_textbox_show(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
if(textbox->is_shown) {
|
||||
mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Textbox is already shown");
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
return;
|
||||
}
|
||||
|
||||
view_holder_set_view(textbox->view_holder, text_box_get_view(textbox->text_box));
|
||||
textbox->is_shown = true;
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void js_textbox_close(struct mjs* mjs) {
|
||||
JsTextboxInst* textbox = get_this_ctx(mjs);
|
||||
if(!check_arg_count(mjs, 0)) return;
|
||||
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
textbox->is_shown = false;
|
||||
|
||||
mjs_return(mjs, MJS_UNDEFINED);
|
||||
}
|
||||
|
||||
static void* js_textbox_create(struct mjs* mjs, mjs_val_t* object) {
|
||||
JsTextboxInst* textbox = malloc(sizeof(JsTextboxInst));
|
||||
|
||||
mjs_val_t textbox_obj = mjs_mk_object(mjs);
|
||||
mjs_set(mjs, textbox_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, textbox));
|
||||
mjs_set(mjs, textbox_obj, "setConfig", ~0, MJS_MK_FN(js_textbox_set_config));
|
||||
mjs_set(mjs, textbox_obj, "addText", ~0, MJS_MK_FN(js_textbox_add_text));
|
||||
mjs_set(mjs, textbox_obj, "clearText", ~0, MJS_MK_FN(js_textbox_clear_text));
|
||||
mjs_set(mjs, textbox_obj, "isOpen", ~0, MJS_MK_FN(js_textbox_is_open));
|
||||
mjs_set(mjs, textbox_obj, "show", ~0, MJS_MK_FN(js_textbox_show));
|
||||
mjs_set(mjs, textbox_obj, "close", ~0, MJS_MK_FN(js_textbox_close));
|
||||
|
||||
textbox->text = furi_string_alloc();
|
||||
textbox->text_box = text_box_alloc();
|
||||
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
textbox->view_holder = view_holder_alloc();
|
||||
view_holder_attach_to_gui(textbox->view_holder, gui);
|
||||
view_holder_set_back_callback(textbox->view_holder, textbox_exit, textbox);
|
||||
|
||||
*object = textbox_obj;
|
||||
return textbox;
|
||||
}
|
||||
|
||||
static void js_textbox_destroy(void* inst) {
|
||||
JsTextboxInst* textbox = inst;
|
||||
|
||||
view_holder_set_view(textbox->view_holder, NULL);
|
||||
view_holder_free(textbox->view_holder);
|
||||
textbox->view_holder = NULL;
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
text_box_reset(textbox->text_box);
|
||||
furi_string_reset(textbox->text);
|
||||
|
||||
text_box_free(textbox->text_box);
|
||||
furi_string_free(textbox->text);
|
||||
free(textbox);
|
||||
}
|
||||
|
||||
static const JsModuleDescriptor js_textbox_desc = {
|
||||
"textbox",
|
||||
js_textbox_create,
|
||||
js_textbox_destroy,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor textbox_plugin_descriptor = {
|
||||
.appid = PLUGIN_APP_ID,
|
||||
.ep_api_version = PLUGIN_API_VERSION,
|
||||
.entry_point = &js_textbox_desc,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* js_textbox_ep(void) {
|
||||
return &textbox_plugin_descriptor;
|
||||
}
|
||||
Reference in New Issue
Block a user