diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 24571aed2..36fd7b16c 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -16,11 +16,70 @@ App( ) App( - appid="js_dialog", + appid="js_event_loop", apptype=FlipperAppType.PLUGIN, - entry_point="js_dialog_ep", + entry_point="js_event_loop_ep", requires=["js_app"], - sources=["modules/js_dialog.c"], + sources=[ + "modules/js_event_loop/js_event_loop.c", + "modules/js_event_loop/js_event_loop_api_table.cpp", + ], +) + +App( + appid="js_gui", + apptype=FlipperAppType.PLUGIN, + entry_point="js_gui_ep", + requires=["js_app", "js_event_loop"], + sources=["modules/js_gui/js_gui.c", "modules/js_gui/js_gui_api_table.cpp"], +) + +App( + appid="js_gui__loading", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_loading_ep", + requires=["js_app", "js_gui", "js_event_loop"], + sources=["modules/js_gui/loading.c"], +) + +App( + appid="js_gui__empty_screen", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_empty_screen_ep", + requires=["js_app", "js_gui", "js_event_loop"], + sources=["modules/js_gui/empty_screen.c"], +) + +App( + appid="js_gui__submenu", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_submenu_ep", + requires=["js_app", "js_gui"], + sources=["modules/js_gui/submenu.c"], +) + +App( + appid="js_gui__text_input", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_text_input_ep", + requires=["js_app", "js_gui", "js_event_loop"], + sources=["modules/js_gui/text_input.c"], +) + +App( + appid="js_gui__text_box", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_text_box_ep", + requires=["js_app"], + sources=["modules/js_gui/text_box.c"], +) + +App( + appid="js_gui__dialog", + apptype=FlipperAppType.PLUGIN, + entry_point="js_view_dialog_ep", + requires=["js_app"], + sources=["modules/js_gui/dialog.c"], ) App( @@ -48,35 +107,11 @@ App( ) App( - appid="js_storage", + appid="js_gpio", apptype=FlipperAppType.PLUGIN, - entry_point="js_storage_ep", - requires=["js_app"], - sources=["modules/js_storage.c"], -) - -App( - appid="js_usbdisk", - apptype=FlipperAppType.PLUGIN, - entry_point="js_usbdisk_ep", - requires=["js_app"], - sources=["modules/js_usbdisk/*.c"], -) - -App( - appid="js_submenu", - apptype=FlipperAppType.PLUGIN, - entry_point="js_submenu_ep", - requires=["js_app"], - sources=["modules/js_submenu.c"], -) - -App( - appid="js_blebeacon", - apptype=FlipperAppType.PLUGIN, - entry_point="js_blebeacon_ep", - requires=["js_app"], - sources=["modules/js_blebeacon.c"], + entry_point="js_gpio_ep", + requires=["js_app", "js_event_loop"], + sources=["modules/js_gpio.c"], ) App( @@ -88,48 +123,9 @@ App( ) App( - appid="js_keyboard", + appid="js_storage", apptype=FlipperAppType.PLUGIN, - entry_point="js_keyboard_ep", + entry_point="js_storage_ep", requires=["js_app"], - sources=["modules/js_keyboard.c"], -) -App( - appid="js_subghz", - apptype=FlipperAppType.PLUGIN, - entry_point="js_subghz_ep", - requires=["js_app"], - sources=["modules/js_subghz/*.c"], -) - -App( - appid="js_gpio", - apptype=FlipperAppType.PLUGIN, - entry_point="js_gpio_ep", - requires=["js_app"], - sources=["modules/js_gpio.c"], -) - -App( - appid="js_textbox", - apptype=FlipperAppType.PLUGIN, - entry_point="js_textbox_ep", - requires=["js_app"], - sources=["modules/js_textbox.c"], -) - -App( - appid="js_widget", - apptype=FlipperAppType.PLUGIN, - entry_point="js_widget_ep", - requires=["js_app"], - sources=["modules/js_widget.c"], -) - -App( - appid="js_vgm", - apptype=FlipperAppType.PLUGIN, - entry_point="js_vgm_ep", - requires=["js_app"], - sources=["modules/js_vgm/*.c", "modules/js_vgm/ICM42688P/*.c"], + sources=["modules/js_storage.c"], ) diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index 20c4ce733..c321150df 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -114,7 +114,7 @@ int32_t js_app(void* arg) { FuriString* start_text = furi_string_alloc_printf("Running %s", furi_string_get_cstr(name)); console_view_print(app->console_view, furi_string_get_cstr(start_text)); - console_view_print(app->console_view, "------------"); + console_view_print(app->console_view, "-------------"); furi_string_free(name); furi_string_free(start_text); diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 9ab6cb140..38ff46f75 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -1,7 +1,11 @@ #include #include "js_modules.h" -#include +#include + #include "modules/js_flipper.h" +#ifdef FW_CFG_unit_tests +#include "modules/js_tests.h" +#endif #define TAG "JS modules" @@ -9,54 +13,72 @@ #define MODULES_PATH "/ext/apps_data/js_app/plugins" typedef struct { - JsModeConstructor create; - JsModeDestructor destroy; + FuriString* name; + const JsModuleConstructor create; + const JsModuleDestructor destroy; void* context; } JsModuleData; -DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST); +// not using: +// - a dict because ordering is required +// - a bptree because it forces a sorted ordering +// - an rbtree because i deemed it more tedious to implement, and with the +// amount of modules in use (under 10 in the overwhelming majority of cases) +// i bet it's going to be slower than a plain array +ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST); +#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray) static const JsModuleDescriptor modules_builtin[] = { - {"flipper", js_flipper_create, NULL}, + {"flipper", js_flipper_create, NULL, NULL}, +#ifdef FW_CFG_unit_tests + {"tests", js_tests_create, NULL, NULL}, +#endif }; struct JsModules { struct mjs* mjs; - JsModuleDict_t module_dict; + JsModuleArray_t modules; PluginManager* plugin_manager; + CompositeApiResolver* resolver; }; JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) { JsModules* modules = malloc(sizeof(JsModules)); modules->mjs = mjs; - JsModuleDict_init(modules->module_dict); + JsModuleArray_init(modules->modules); modules->plugin_manager = plugin_manager_alloc( PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver)); + modules->resolver = resolver; + return modules; } -void js_modules_destroy(JsModules* modules) { - JsModuleDict_it_t it; - for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it); - JsModuleDict_next(it)) { - const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it); - if(module_itref->value.destroy) { - module_itref->value.destroy(module_itref->value.context); +void js_modules_destroy(JsModules* instance) { + for + M_EACH(module, instance->modules, JsModuleArray_t) { + FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name)); + if(module->destroy) module->destroy(module->context); + furi_string_free(module->name); } - } - plugin_manager_free(modules->plugin_manager); - JsModuleDict_clear(modules->module_dict); - free(modules); + plugin_manager_free(instance->plugin_manager); + JsModuleArray_clear(instance->modules); + free(instance); +} + +JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) { + for + M_EACH(module, instance->modules, JsModuleArray_t) { + if(furi_string_cmp_str(module->name, name) == 0) return module; + } + return NULL; } mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) { - FuriString* module_name = furi_string_alloc_set_str(name); // Check if module is already installed - JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name); + JsModuleData* module_inst = js_find_loaded_module(modules, name); if(module_inst) { //-V547 - furi_string_free(module_name); mjs_prepend_errorf( modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name); return MJS_UNDEFINED; @@ -73,8 +95,11 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) { JsModuleData module = { - .create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy}; - JsModuleDict_set_at(modules->module_dict, module_name, module); + .create = modules_builtin[i].create, + .destroy = modules_builtin[i].destroy, + .name = furi_string_alloc_set_str(name), + }; + JsModuleArray_push_at(modules->modules, 0, module); module_found = true; FURI_LOG_I(TAG, "Using built-in module %s", name); break; @@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le // External module load if(!module_found) { + FuriString* deslashed_name = furi_string_alloc_set_str(name); + furi_string_replace_all_str(deslashed_name, "/", "__"); FuriString* module_path = furi_string_alloc(); - furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name); - FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path)); + furi_string_printf( + module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name)); + FURI_LOG_I( + TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path)); do { uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager); PluginManagerError load_error = plugin_manager_load_single( modules->plugin_manager, furi_string_get_cstr(module_path)); if(load_error != PluginManagerErrorNone) { + FURI_LOG_E( + TAG, + "Module %s load error. It may depend on other modules that are not yet loaded.", + name); break; } const JsModuleDescriptor* plugin = plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last); furi_assert(plugin); - if(strncmp(name, plugin->name, name_len) != 0) { - FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name); + if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) { + FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name); break; } - JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy}; - JsModuleDict_set_at(modules->module_dict, module_name, module); + JsModuleData module = { + .create = plugin->create, + .destroy = plugin->destroy, + .name = furi_string_alloc_set_str(name), + }; + JsModuleArray_push_at(modules->modules, 0, module); + + if(plugin->api_interface) { + FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name); + composite_api_resolver_add(modules->resolver, plugin->api_interface); + } module_found = true; } while(0); furi_string_free(module_path); + furi_string_free(deslashed_name); } // Run module constructor mjs_val_t module_object = MJS_UNDEFINED; if(module_found) { - module_inst = JsModuleDict_get(modules->module_dict, module_name); + module_inst = js_find_loaded_module(modules, name); furi_assert(module_inst); if(module_inst->create) { //-V779 - module_inst->context = module_inst->create(modules->mjs, &module_object); + module_inst->context = module_inst->create(modules->mjs, &module_object, modules); } } @@ -123,7 +166,12 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name); } - furi_string_free(module_name); - return module_object; } + +void* js_module_get(JsModules* modules, const char* name) { + FuriString* module_name = furi_string_alloc_set_str(name); + JsModuleData* module_inst = js_find_loaded_module(modules, name); + furi_string_free(module_name); + return module_inst ? module_inst->context : NULL; +} diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 77e50786f..788715872 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -1,4 +1,6 @@ #pragma once + +#include #include "js_thread_i.h" #include #include @@ -7,19 +9,269 @@ #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 -typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object); -typedef void (*JsModeDestructor)(void* inst); +/** + * @brief Returns the foreign pointer in `obj["_"]` + */ +#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0)) +/** + * @brief Returns the foreign pointer in `this["_"]` + */ +#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs)) + +/** + * @brief Syntax sugar for constructing an object + * + * @example + * ```c + * mjs_val_t my_obj = mjs_mk_object(mjs); + * JS_ASSIGN_MULTI(mjs, my_obj) { + * JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open)); + * JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open)); + * } + * ``` + */ +#define JS_ASSIGN_MULTI(mjs, object) \ + for(struct { \ + struct mjs* mjs; \ + mjs_val_t val; \ + int i; \ + } _ass_multi = {mjs, object, 0}; \ + _ass_multi.i == 0; \ + _ass_multi.i++) +#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value) + +/** + * @brief The first word of structures that foreign pointer JS values point to + * + * This is used to detect situations where JS code mistakenly passes an opaque + * foreign pointer of one type as an argument to a native function which expects + * a struct of another type. + * + * It is recommended to use this functionality in conjunction with the following + * convenience verification macros: + * - `JS_ARG_STRUCT()` + * - `JS_ARG_OBJ_WITH_STRUCT()` + * + * @warning In order for the mechanism to work properly, your struct must store + * the magic value in the first word. + */ +typedef enum { + JsForeignMagicStart = 0x15BAD000, + JsForeignMagic_JsEventLoopContract, +} JsForeignMagic; + +// Are you tired of your silly little JS+C glue code functions being 75% +// argument validation code and 25% actual logic? Introducing: ASS (Argument +// Schema for Scripts)! ASS is a set of macros that reduce the typical +// boilerplate code of "check argument count, get arguments, validate arguments, +// extract C values from arguments" down to just one line! + +/** + * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies + * that the function requires exactly as many arguments as were specified. + */ +#define JS_EXACTLY == +/** + * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies + * that the function requires at least as many arguments as were specified. + */ +#define JS_AT_LEAST >= + +#define JS_ENUM_MAP(var_name, ...) \ + static const JsEnumMapping var_name##_mapping[] = { \ + {NULL, sizeof(var_name)}, \ + __VA_ARGS__, \ + {NULL, 0}, \ + }; + +typedef struct { + const char* name; + size_t value; +} JsEnumMapping; + +typedef struct { + void* out; + int (*validator)(mjs_val_t); + void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra); + const char* expected_type; + bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra); + const void* extra_data; +} _js_arg_decl; + +static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(int32_t*)out = mjs_get_int32(mjs, *in); +} +#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL}) + +static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(void**)out = mjs_get_ptr(mjs, *in); +} +#define JS_ARG_PTR(out) \ + ((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL}) + +static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(const char**)out = mjs_get_string(mjs, in, NULL); +} +#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL}) + +static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(bool*)out = !!mjs_get_bool(mjs, *in); +} +#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL}) + +static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + UNUSED(mjs); + *(mjs_val_t*)out = *in; +} +#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL}) +#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL}) +#define JS_ARG_FN(out) \ + ((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL}) +#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL}) + +static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { + JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; + JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val); + return struct_magic == expected_magic; +} +#define JS_ARG_STRUCT(type, out) \ + ((_js_arg_decl){ \ + out, \ + mjs_is_foreign, \ + _js_to_ptr, \ + #type, \ + _js_validate_struct, \ + (void*)JsForeignMagic##_##type}) + +static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { + JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; + JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val); + return struct_magic == expected_magic; +} +#define JS_ARG_OBJ_WITH_STRUCT(type, out) \ + ((_js_arg_decl){ \ + out, \ + mjs_is_object, \ + _js_passthrough, \ + #type, \ + _js_validate_obj_w_struct, \ + (void*)JsForeignMagic##_##type}) + +static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) { + for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++) + if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true; + return false; +} +static inline void + _js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { + const JsEnumMapping* mapping = (JsEnumMapping*)extra; + size_t size = mapping->value; // get enum size from first entry + for(mapping++; mapping->name; mapping++) { + if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) { + if(size == 1) + *(uint8_t*)out = mapping->value; + else if(size == 2) + *(uint16_t*)out = mapping->value; + else if(size == 4) + *(uint32_t*)out = mapping->value; + else if(size == 8) + *(uint64_t*)out = mapping->value; + return; + } + } + // unreachable, thanks to _js_validate_enum +} +#define JS_ARG_ENUM(var_name, name) \ + ((_js_arg_decl){ \ + &var_name, \ + mjs_is_string, \ + _js_convert_enum, \ + name " enum", \ + _js_validate_enum, \ + var_name##_mapping}) + +//-V:JS_FETCH_ARGS_OR_RETURN:1008 +/** + * @brief Fetches and validates the arguments passed to a JS function + * + * Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));` + * + * @warning This macro executes `return;` by design in case of an argument count + * mismatch or a validation failure + */ +#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \ + _js_arg_decl _js_args[] = {__VA_ARGS__}; \ + int _js_arg_cnt = COUNT_OF(_js_args); \ + mjs_val_t _js_arg_vals[_js_arg_cnt]; \ + if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "expected %s%d arguments, got %d", \ + #arg_operator, \ + _js_arg_cnt, \ + mjs_nargs(mjs)); \ + for(int _i = 0; _i < _js_arg_cnt; _i++) { \ + _js_arg_vals[_i] = mjs_arg(mjs, _i); \ + if(_js_args[_i].validator) \ + if(!_js_args[_i].validator(_js_arg_vals[_i])) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "argument %d: expected %s", \ + _i, \ + _js_args[_i].expected_type); \ + if(_js_args[_i].extended_validator) \ + if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "argument %d: expected %s", \ + _i, \ + _js_args[_i].expected_type); \ + _js_args[_i].converter( \ + mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \ + } + +/** + * @brief Prepends an error, sets the JS return value to `undefined` and returns + * from the C function + * @warning This macro executes `return;` by design + */ +#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \ + do { \ + mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \ + mjs_return(mjs, MJS_UNDEFINED); \ + return; \ + } while(0) + +typedef struct JsModules JsModules; + +typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules); +typedef void (*JsModuleDestructor)(void* inst); typedef struct { char* name; - JsModeConstructor create; - JsModeDestructor destroy; + JsModuleConstructor create; + JsModuleDestructor destroy; + const ElfApiInterface* api_interface; } JsModuleDescriptor; -typedef struct JsModules JsModules; - JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver); void js_modules_destroy(JsModules* modules); mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len); + +/** + * @brief Gets a module instance by its name + * This is useful when a module wants to access a stateful API of another + * module. + * @returns Pointer to module context, NULL if the module is not instantiated + */ +void* js_module_get(JsModules* modules, const char* name); diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index e1c21e9fb..95cad84f6 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -196,17 +196,11 @@ static void js_require(struct mjs* mjs) { } static void js_global_to_string(struct mjs* mjs) { - double num = mjs_get_double(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-2147483648"; - itoa(num, tmp_str, 10); - mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); - mjs_return(mjs, ret); -} - -static void js_global_to_hex_string(struct mjs* mjs) { + int base = 10; + if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); double num = mjs_get_int(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-FFFFFFFF"; - itoa(num, tmp_str, 16); + char tmp_str[] = "-2147483648"; + itoa(num, tmp_str, base); mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); mjs_return(mjs, ret); } @@ -340,8 +334,7 @@ static int32_t js_thread(void* arg) { } mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); - mjs_set(mjs, global, "to_string", ~0, MJS_MK_FN(js_global_to_string)); - mjs_set(mjs, global, "to_hex_string", ~0, MJS_MK_FN(js_global_to_hex_string)); + mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string)); mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); mjs_set(mjs, global, "parse_int", ~0, MJS_MK_FN(js_parse_int)); @@ -400,8 +393,8 @@ static int32_t js_thread(void* arg) { } } - js_modules_destroy(worker->modules); mjs_destroy(mjs); + js_modules_destroy(worker->modules); composite_api_resolver_free(worker->resolver); diff --git a/applications/system/js_app/js_thread.h b/applications/system/js_app/js_thread.h index 969715ec1..581a44919 100644 --- a/applications/system/js_app/js_thread.h +++ b/applications/system/js_app/js_thread.h @@ -1,5 +1,9 @@ #pragma once +#ifdef __cplusplus +extern "C" { +#endif + typedef struct JsThread JsThread; typedef enum { @@ -14,3 +18,7 @@ typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* con JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context); void js_thread_stop(JsThread* worker); + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/modules/js_badusb.c b/applications/system/js_app/modules/js_badusb.c index d2e30b334..85a70b493 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -102,8 +102,8 @@ static bool setup_parse_params( } 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); mjs_val_t layout_obj = mjs_get(mjs, arg, "layout_path", ~0); if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) { @@ -486,7 +486,8 @@ static void js_badusb_alt_println(struct mjs* mjs) { badusb_print(mjs, true, 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)); @@ -514,6 +515,7 @@ static const JsModuleDescriptor js_badusb_desc = { "badusb", js_badusb_create, js_badusb_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_dialog.c b/applications/system/js_app/modules/js_dialog.c deleted file mode 100644 index be2b5a95a..000000000 --- a/applications/system/js_app/modules/js_dialog.c +++ /dev/null @@ -1,218 +0,0 @@ -#include -#include "../js_modules.h" -#include - -// File icon -#include -static const uint8_t _I_file_10px_0[] = { - 0x00, 0x7f, 0x00, 0xa1, 0x00, 0x2d, 0x01, 0xe1, 0x01, 0x0d, 0x01, - 0x01, 0x01, 0x7d, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0x01, -}; -static const uint8_t* const _I_file_10px[] = {_I_file_10px_0}; - -static const Icon I_file_10px = - {.width = 10, .height = 10, .frame_count = 1, .frame_rate = 0, .frames = _I_file_10px}; -// File icon end - -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_pick_file(struct mjs* mjs) { - if(mjs_nargs(mjs) != 2) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Wrong arguments"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - mjs_val_t base_path_obj = mjs_arg(mjs, 0); - if(!mjs_is_string(base_path_obj)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base path must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - size_t base_path_len = 0; - const char* base_path = mjs_get_string(mjs, &base_path_obj, &base_path_len); - if((base_path_len == 0) || (base_path == NULL)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad base path argument"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - mjs_val_t extension_obj = mjs_arg(mjs, 1); - if(!mjs_is_string(extension_obj)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Extension must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - size_t extension_len = 0; - const char* extension = mjs_get_string(mjs, &extension_obj, &extension_len); - if((extension_len == 0) || (extension == NULL)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad extension argument"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); - const DialogsFileBrowserOptions browser_options = { - .extension = extension, - .icon = &I_file_10px, - .base_path = base_path, - }; - FuriString* path = furi_string_alloc_set(base_path); - if(dialog_file_browser_show(dialogs, path, path, &browser_options)) { - mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true)); - } else { - mjs_return(mjs, MJS_UNDEFINED); - } - furi_string_free(path); - furi_record_close(RECORD_DIALOGS); -} - -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)); - mjs_set(mjs, dialog_obj, "pickFile", ~0, MJS_MK_FN(js_dialog_pick_file)); - *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; -} diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.c b/applications/system/js_app/modules/js_event_loop/js_event_loop.c new file mode 100644 index 000000000..c4f0d1bee --- /dev/null +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.c @@ -0,0 +1,451 @@ +#include "js_event_loop.h" +#include "../../js_modules.h" // IWYU pragma: keep +#include +#include + +/** + * @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; //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; +} diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.h b/applications/system/js_app/modules/js_event_loop/js_event_loop.h new file mode 100644 index 000000000..7ae608e34 --- /dev/null +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.h @@ -0,0 +1,104 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include +#include + +/** + * @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; // +#include + +#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(), +}; diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop_api_table_i.h b/applications/system/js_app/modules/js_event_loop/js_event_loop_api_table_i.h new file mode 100644 index 000000000..49090caeb --- /dev/null +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop_api_table_i.h @@ -0,0 +1,4 @@ +#include "js_event_loop.h" + +static constexpr auto js_event_loop_api_table = sort( + create_array_t(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*)))); diff --git a/applications/system/js_app/modules/js_flipper.c b/applications/system/js_app/modules/js_flipper.c index 4619a1593..43c675e10 100644 --- a/applications/system/js_app/modules/js_flipper.c +++ b/applications/system/js_app/modules/js_flipper.c @@ -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)); diff --git a/applications/system/js_app/modules/js_flipper.h b/applications/system/js_app/modules/js_flipper.h index 3b05389cc..98979ce58 100644 --- a/applications/system/js_app/modules/js_flipper.h +++ b/applications/system/js_app/modules/js_flipper.h @@ -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); diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index fb42bea2b..70021968f 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -1,387 +1,337 @@ -#include "../js_modules.h" +#include "../js_modules.h" // IWYU pragma: keep +#include "./js_event_loop/js_event_loop.h" #include #include #include +#include +#include -typedef struct { - FuriHalAdcHandle* handle; -} JsGpioInst; +#define INTERRUPT_QUEUE_LEN 16 +/** + * Per-pin control structure + */ typedef struct { const GpioPin* pin; - const char* name; - const FuriHalAdcChannel channel; -} GpioPinCtx; + bool had_interrupt; + FuriSemaphore* interrupt_semaphore; + JsEventLoopContract* interrupt_contract; + FuriHalAdcChannel adc_channel; + FuriHalAdcHandle* adc_handle; +} JsGpioPinInst; -static const GpioPinCtx js_gpio_pins[] = { - {.pin = &gpio_ext_pa7, .name = "PA7", .channel = FuriHalAdcChannel12}, // 2 - {.pin = &gpio_ext_pa6, .name = "PA6", .channel = FuriHalAdcChannel11}, // 3 - {.pin = &gpio_ext_pa4, .name = "PA4", .channel = FuriHalAdcChannel9}, // 4 - {.pin = &gpio_ext_pb3, .name = "PB3", .channel = FuriHalAdcChannelNone}, // 5 - {.pin = &gpio_ext_pb2, .name = "PB2", .channel = FuriHalAdcChannelNone}, // 6 - {.pin = &gpio_ext_pc3, .name = "PC3", .channel = FuriHalAdcChannel4}, // 7 - {.pin = &gpio_swclk, .name = "PA14", .channel = FuriHalAdcChannelNone}, // 10 - {.pin = &gpio_swdio, .name = "PA13", .channel = FuriHalAdcChannelNone}, // 12 - {.pin = &gpio_usart_tx, .name = "PB6", .channel = FuriHalAdcChannelNone}, // 13 - {.pin = &gpio_usart_rx, .name = "PB7", .channel = FuriHalAdcChannelNone}, // 14 - {.pin = &gpio_ext_pc1, .name = "PC1", .channel = FuriHalAdcChannel2}, // 15 - {.pin = &gpio_ext_pc0, .name = "PC0", .channel = FuriHalAdcChannel1}, // 16 - {.pin = &gpio_ibutton, .name = "PB14", .channel = FuriHalAdcChannelNone}, // 17 -}; +ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575 -bool js_gpio_get_gpio_pull(const char* pull, GpioPull* value) { - if(strcmp(pull, "no") == 0) { - *value = GpioPullNo; - return true; - } else if(strcmp(pull, "up") == 0) { - *value = GpioPullUp; - return true; - } else if(strcmp(pull, "down") == 0) { - *value = GpioPullDown; - return true; - } else { - *value = GpioPullNo; - return true; - } - return false; -} - -bool js_gpio_get_gpio_mode(const char* mode, GpioMode* value) { - if(strcmp(mode, "input") == 0) { - *value = GpioModeInput; - return true; - } else if(strcmp(mode, "outputPushPull") == 0) { - *value = GpioModeOutputPushPull; - return true; - } else if(strcmp(mode, "outputOpenDrain") == 0) { - *value = GpioModeOutputOpenDrain; - return true; - } else if(strcmp(mode, "altFunctionPushPull") == 0) { - *value = GpioModeAltFunctionPushPull; - return true; - } else if(strcmp(mode, "altFunctionOpenDrain") == 0) { - *value = GpioModeAltFunctionOpenDrain; - return true; - } else if(strcmp(mode, "analog") == 0) { - *value = GpioModeAnalog; - return true; - } else if(strcmp(mode, "interruptRise") == 0) { - *value = GpioModeInterruptRise; - return true; - } else if(strcmp(mode, "interruptFall") == 0) { - *value = GpioModeInterruptFall; - return true; - } else if(strcmp(mode, "interruptRiseFall") == 0) { - *value = GpioModeInterruptRiseFall; - return true; - } else if(strcmp(mode, "eventRise") == 0) { - *value = GpioModeEventRise; - return true; - } else if(strcmp(mode, "eventFall") == 0) { - *value = GpioModeEventFall; - return true; - } else if(strcmp(mode, "eventRiseFall") == 0) { - *value = GpioModeEventRiseFall; - return true; - } else { - return false; - } -} - -const GpioPin* js_gpio_get_gpio_pin(const char* name) { - for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { - if(strcmp(js_gpio_pins[i].name, name) == 0) { - return js_gpio_pins[i].pin; - } - } - return NULL; -} - -FuriHalAdcChannel js_gpio_get_gpio_channel(const char* name) { - for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { - if(strcmp(js_gpio_pins[i].name, name) == 0) { - return js_gpio_pins[i].channel; - } - } - return FuriHalAdcChannelNone; +/** + * 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) { - mjs_val_t pin_arg = mjs_arg(mjs, 0); - mjs_val_t mode_arg = mjs_arg(mjs, 1); - mjs_val_t pull_arg = mjs_arg(mjs, 2); + // 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); - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; + // 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"); } - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; + // 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"); } - if(!mjs_is_string(mode_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* mode_name = mjs_get_string(mjs, &mode_arg, NULL); - if(!mode_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get mode name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - if(!mjs_is_string(pull_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pull_name = mjs_get_string(mjs, &pull_arg, NULL); - if(!pull_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pull name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); - if(gpio_pin == NULL) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - GpioMode gpio_mode; - if(!js_gpio_get_gpio_mode(mode_name, &gpio_mode)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid mode name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - GpioPull gpio_pull; - if(!js_gpio_get_gpio_pull(pull_name, &gpio_pull)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pull name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - expansion_disable(furi_record_open(RECORD_EXPANSION)); - furi_record_close(RECORD_EXPANSION); - - furi_hal_gpio_init(gpio_pin, gpio_mode, gpio_pull, GpioSpeedVeryHigh); - + // 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) { - mjs_val_t pin_arg = mjs_arg(mjs, 0); - mjs_val_t value_arg = mjs_arg(mjs, 1); - - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - if(!mjs_is_boolean(value_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a boolean"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - bool value = mjs_get_bool(mjs, value_arg); - - const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); - - if(gpio_pin == NULL) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - furi_hal_gpio_write(gpio_pin, value); - + 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) { - mjs_val_t pin_arg = mjs_arg(mjs, 0); - - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); - - if(gpio_pin == NULL) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - bool value = furi_hal_gpio_read(gpio_pin); - + // 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) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst); - furi_assert(gpio); - - if(gpio->handle == NULL) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - mjs_val_t pin_arg = mjs_arg(mjs, 0); - - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - FuriHalAdcChannel channel = js_gpio_get_gpio_channel(pin_name); - if(channel == FuriHalAdcChannelNone) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - uint16_t adc_value = furi_hal_adc_read(gpio->handle, channel); - float adc_mv = furi_hal_adc_convert_to_voltage(gpio->handle, adc_value); - - mjs_return(mjs, mjs_mk_number(mjs, adc_mv)); + // 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)); } -static void js_gpio_start_analog(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst); - furi_assert(gpio); +/** + * @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; - FuriHalAdcScale scale = FuriHalAdcScale2048; - if(mjs_nargs(mjs) > 0) { - mjs_val_t scale_arg = mjs_arg(mjs, 0); - - if(!mjs_is_number(scale_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a number"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - int32_t scale_num = mjs_get_int32(mjs, scale_arg); - if(scale_num == 2048 || scale_num == 2000) { // 2 volt reference - scale = FuriHalAdcScale2048; - } else if(scale_num == 2500) { // 2.5 volt reference - scale = FuriHalAdcScale2500; - } else { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid scale"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + // 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(gpio->handle != NULL) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode already started"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + 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"); - gpio->handle = furi_hal_adc_acquire(); - furi_hal_adc_configure_ex( - gpio->handle, - scale, - FuriHalAdcClockSync64, - FuriHalAdcOversample64, - FuriHalAdcSamplingtime247_5); + // 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_stop_analog(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst); - furi_assert(gpio); +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); - if(gpio->handle == NULL) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + 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); - furi_hal_adc_release(gpio->handle); - gpio->handle = NULL; -} - -static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object) { - JsGpioInst* gpio = malloc(sizeof(JsGpioInst)); - gpio->handle = NULL; mjs_val_t gpio_obj = mjs_mk_object(mjs); - mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, gpio)); - mjs_set(mjs, gpio_obj, "init", ~0, MJS_MK_FN(js_gpio_init)); - mjs_set(mjs, gpio_obj, "write", ~0, MJS_MK_FN(js_gpio_write)); - mjs_set(mjs, gpio_obj, "read", ~0, MJS_MK_FN(js_gpio_read)); - mjs_set(mjs, gpio_obj, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); - mjs_set(mjs, gpio_obj, "startAnalog", ~0, MJS_MK_FN(js_gpio_start_analog)); - mjs_set(mjs, gpio_obj, "stopAnalog", ~0, MJS_MK_FN(js_gpio_stop_analog)); + 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*)gpio; + return (void*)module; } static void js_gpio_destroy(void* inst) { - if(inst != NULL) { - JsGpioInst* gpio = (JsGpioInst*)inst; - if(gpio->handle != NULL) { - furi_hal_adc_release(gpio->handle); - gpio->handle = NULL; + 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); } - free(gpio); + 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); } - // loop through all pins and reset them to analog mode - for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { - furi_hal_gpio_write(js_gpio_pins[i].pin, false); - furi_hal_gpio_init(js_gpio_pins[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh); - } - - expansion_enable(furi_record_open(RECORD_EXPANSION)); - furi_record_close(RECORD_EXPANSION); + // 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 = { diff --git a/applications/system/js_app/modules/js_gui/dialog.c b/applications/system/js_app/modules/js_gui/dialog.c new file mode 100644 index 000000000..31eee237f --- /dev/null +++ b/applications/system/js_app/modules/js_gui/dialog.c @@ -0,0 +1,129 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#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); diff --git a/applications/system/js_app/modules/js_gui/empty_screen.c b/applications/system/js_app/modules/js_gui/empty_screen.c new file mode 100644 index 000000000..9684eabdc --- /dev/null +++ b/applications/system/js_app/modules/js_gui/empty_screen.c @@ -0,0 +1,12 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include + +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); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c new file mode 100644 index 000000000..8ac3055d5 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -0,0 +1,348 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "./js_gui.h" +#include +#include +#include +#include "../js_event_loop/js_event_loop.h" +#include + +#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; +} diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h new file mode 100644 index 000000000..02198ca4f --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -0,0 +1,116 @@ +#include "../../js_modules.h" +#include + +#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; // 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 diff --git a/applications/system/js_app/modules/js_gui/js_gui_api_table.cpp b/applications/system/js_app/modules/js_gui/js_gui_api_table.cpp new file mode 100644 index 000000000..2be9cb3b2 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui_api_table.cpp @@ -0,0 +1,16 @@ +#include +#include + +#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(), +}; diff --git a/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h b/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h new file mode 100644 index 000000000..852b3d107 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h @@ -0,0 +1,4 @@ +#include "js_gui.h" + +static constexpr auto js_gui_api_table = sort(create_array_t( + API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)))); diff --git a/applications/system/js_app/modules/js_gui/loading.c b/applications/system/js_app/modules/js_gui/loading.c new file mode 100644 index 000000000..e291824a0 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/loading.c @@ -0,0 +1,12 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include + +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); diff --git a/applications/system/js_app/modules/js_gui/submenu.c b/applications/system/js_app/modules/js_gui/submenu.c new file mode 100644 index 000000000..aecd413be --- /dev/null +++ b/applications/system/js_app/modules/js_gui/submenu.c @@ -0,0 +1,87 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#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); diff --git a/applications/system/js_app/modules/js_gui/text_box.c b/applications/system/js_app/modules/js_gui/text_box.c new file mode 100644 index 000000000..4e6c8247c --- /dev/null +++ b/applications/system/js_app/modules/js_gui/text_box.c @@ -0,0 +1,78 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include + +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); diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c new file mode 100644 index 000000000..575029f8e --- /dev/null +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -0,0 +1,120 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#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); diff --git a/applications/system/js_app/modules/js_keyboard.c b/applications/system/js_app/modules/js_keyboard.c deleted file mode 100644 index 53f48d01d..000000000 --- a/applications/system/js_app/modules/js_keyboard.c +++ /dev/null @@ -1,206 +0,0 @@ -#include "../js_modules.h" -#include -#include -#include -#include - -#define membersof(x) (sizeof(x) / sizeof(x[0])) - -typedef struct { - TextInput* text_input; - ByteInput* byte_input; - ViewHolder* view_holder; - FuriApiLock lock; - char* header; - bool accepted; -} JsKeyboardInst; - -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 JsKeyboardInst* get_this_ctx(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsKeyboardInst* keyboard = mjs_get_ptr(mjs, obj_inst); - furi_assert(keyboard); - return keyboard; -} - -static void keyboard_callback(void* context) { - JsKeyboardInst* keyboard = (JsKeyboardInst*)context; - keyboard->accepted = true; - api_lock_unlock(keyboard->lock); -} - -static void keyboard_exit(void* context) { - JsKeyboardInst* keyboard = (JsKeyboardInst*)context; - keyboard->accepted = false; - api_lock_unlock(keyboard->lock); -} - -static void js_keyboard_set_header(struct mjs* mjs) { - JsKeyboardInst* keyboard = get_this_ctx(mjs); - - 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; - } - - if(keyboard->header) { - free(keyboard->header); - } - keyboard->header = strdup(header); - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_keyboard_text(struct mjs* mjs) { - JsKeyboardInst* keyboard = get_this_ctx(mjs); - - mjs_val_t input_length_arg = mjs_arg(mjs, 0); - if(!mjs_is_number(input_length_arg)) { - ret_bad_args(mjs, "Input length must be a number"); - return; - } - int32_t input_length = mjs_get_int32(mjs, input_length_arg); - char* buffer = malloc(input_length); - - mjs_val_t default_text_arg = mjs_arg(mjs, 1); - const char* default_text = mjs_get_string(mjs, &default_text_arg, NULL); - bool clear_default = false; - if(default_text) { - strlcpy(buffer, default_text, input_length); - mjs_val_t bool_obj = mjs_arg(mjs, 2); - clear_default = mjs_get_bool(mjs, bool_obj); - } - - if(keyboard->header) { - text_input_set_header_text(keyboard->text_input, keyboard->header); - } - text_input_set_result_callback( - keyboard->text_input, keyboard_callback, keyboard, buffer, input_length, clear_default); - - text_input_set_minimum_length(keyboard->text_input, 0); - - keyboard->lock = api_lock_alloc_locked(); - Gui* gui = furi_record_open(RECORD_GUI); - keyboard->view_holder = view_holder_alloc(); - view_holder_attach_to_gui(keyboard->view_holder, gui); - view_holder_set_back_callback(keyboard->view_holder, keyboard_exit, keyboard); - - view_holder_set_view(keyboard->view_holder, text_input_get_view(keyboard->text_input)); - api_lock_wait_unlock(keyboard->lock); - - view_holder_set_view(keyboard->view_holder, NULL); - view_holder_free(keyboard->view_holder); - - furi_record_close(RECORD_GUI); - api_lock_free(keyboard->lock); - - text_input_reset(keyboard->text_input); - if(keyboard->header) { - free(keyboard->header); - keyboard->header = NULL; - } - if(keyboard->accepted) { - mjs_return(mjs, mjs_mk_string(mjs, buffer, ~0, true)); - } else { - mjs_return(mjs, MJS_UNDEFINED); - } - free(buffer); -} - -static void js_keyboard_byte(struct mjs* mjs) { - JsKeyboardInst* keyboard = get_this_ctx(mjs); - - mjs_val_t input_length_arg = mjs_arg(mjs, 0); - if(!mjs_is_number(input_length_arg)) { - ret_bad_args(mjs, "Input length must be a number"); - return; - } - int32_t input_length = mjs_get_int32(mjs, input_length_arg); - uint8_t* buffer = malloc(input_length); - - mjs_val_t default_data_arg = mjs_arg(mjs, 1); - if(mjs_is_typed_array(default_data_arg)) { - if(mjs_is_data_view(default_data_arg)) { - default_data_arg = mjs_dataview_get_buf(mjs, default_data_arg); - } - size_t default_data_len = 0; - char* default_data = mjs_array_buf_get_ptr(mjs, default_data_arg, &default_data_len); - memcpy(buffer, (uint8_t*)default_data, MIN((size_t)input_length, default_data_len)); - } - - if(keyboard->header) { - byte_input_set_header_text(keyboard->byte_input, keyboard->header); - } - byte_input_set_result_callback( - keyboard->byte_input, keyboard_callback, NULL, keyboard, buffer, input_length); - - keyboard->lock = api_lock_alloc_locked(); - Gui* gui = furi_record_open(RECORD_GUI); - keyboard->view_holder = view_holder_alloc(); - view_holder_attach_to_gui(keyboard->view_holder, gui); - view_holder_set_back_callback(keyboard->view_holder, keyboard_exit, keyboard); - - view_holder_set_view(keyboard->view_holder, byte_input_get_view(keyboard->byte_input)); - api_lock_wait_unlock(keyboard->lock); - - view_holder_set_view(keyboard->view_holder, NULL); - view_holder_free(keyboard->view_holder); - - furi_record_close(RECORD_GUI); - api_lock_free(keyboard->lock); - - if(keyboard->header) { - free(keyboard->header); - keyboard->header = NULL; - } - byte_input_set_result_callback(keyboard->byte_input, NULL, NULL, NULL, NULL, 0); - byte_input_set_header_text(keyboard->byte_input, ""); - if(keyboard->accepted) { - mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)buffer, input_length)); - } else { - mjs_return(mjs, MJS_UNDEFINED); - } - free(buffer); -} - -static void* js_keyboard_create(struct mjs* mjs, mjs_val_t* object) { - JsKeyboardInst* keyboard = malloc(sizeof(JsKeyboardInst)); - mjs_val_t keyboard_obj = mjs_mk_object(mjs); - mjs_set(mjs, keyboard_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, keyboard)); - mjs_set(mjs, keyboard_obj, "setHeader", ~0, MJS_MK_FN(js_keyboard_set_header)); - mjs_set(mjs, keyboard_obj, "text", ~0, MJS_MK_FN(js_keyboard_text)); - mjs_set(mjs, keyboard_obj, "byte", ~0, MJS_MK_FN(js_keyboard_byte)); - keyboard->byte_input = byte_input_alloc(); - keyboard->text_input = text_input_alloc(); - *object = keyboard_obj; - return keyboard; -} - -static void js_keyboard_destroy(void* inst) { - JsKeyboardInst* keyboard = inst; - byte_input_free(keyboard->byte_input); - text_input_free(keyboard->text_input); - free(keyboard); -} - -static const JsModuleDescriptor js_keyboard_desc = { - "keyboard", - js_keyboard_create, - js_keyboard_destroy, -}; - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = PLUGIN_APP_ID, - .ep_api_version = PLUGIN_API_VERSION, - .entry_point = &js_keyboard_desc, -}; - -const FlipperAppPluginDescriptor* js_keyboard_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index d8812e61b..7d54cf9b9 100644 --- a/applications/system/js_app/modules/js_math.c +++ b/applications/system/js_app/modules/js_math.c @@ -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 = { diff --git a/applications/system/js_app/modules/js_notification.c b/applications/system/js_app/modules/js_notification.c index 2f57c45d1..994283a09 100644 --- a/applications/system/js_app/modules/js_notification.c +++ b/applications/system/js_app/modules/js_notification.c @@ -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 = { diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index 293798a12..b1e578fbc 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -658,7 +658,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); @@ -686,6 +687,7 @@ static const JsModuleDescriptor js_serial_desc = { "serial", js_serial_create, js_serial_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_storage.c b/applications/system/js_app/modules/js_storage.c index d6ff9cf4e..1d4053a5f 100644 --- a/applications/system/js_app/modules/js_storage.c +++ b/applications/system/js_app/modules/js_storage.c @@ -1,262 +1,375 @@ -#include "../js_modules.h" -#include +#include "../js_modules.h" // IWYU pragma: keep +#include -typedef struct { - Storage* api; -} JsStorageInst; +// ---=== file ops ===--- -static JsStorageInst* get_this_ctx(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsStorageInst* storage = mjs_get_ptr(mjs, obj_inst); - furi_assert(storage); - return storage; +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 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 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 ret_int_err(struct mjs* mjs, const char* error) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_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; +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)); } - return true; } -static bool get_path_arg(struct mjs* mjs, const char** path, size_t index) { - mjs_val_t path_obj = mjs_arg(mjs, index); - if(!mjs_is_string(path_obj)) { - ret_bad_args(mjs, "Path must be a string"); - return false; +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"); } - size_t path_len = 0; - *path = mjs_get_string(mjs, &path_obj, &path_len); - if((path_len == 0) || (*path == NULL)) { - ret_bad_args(mjs, "Bad path argument"); - return false; - } - return true; + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len))); } -static void js_storage_read(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); +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))); +} - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; +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))); +} - File* file = storage_file_alloc(storage->api); - do { - if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { - ret_int_err(mjs, storage_file_get_error_desc(file)); - break; - } +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))); +} - uint64_t size = storage_file_size(file); - mjs_val_t size_arg = mjs_arg(mjs, 1); - if(mjs_is_number(size_arg)) { - size = mjs_get_int32(mjs, size_arg); - } +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))); +} - mjs_val_t seek_arg = mjs_arg(mjs, 2); - if(mjs_is_number(seek_arg)) { - storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true); - size = MIN(size, storage_file_size(file) - storage_file_tell(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))); +} - if(size > memmgr_heap_get_max_free_block()) { - ret_int_err(mjs, "Read size too large"); - break; - } +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))); +} - uint8_t* data = malloc(size); - size_t read = storage_file_read(file, data, size); - if(read == size) { - mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)data, size)); - } else { - ret_int_err(mjs, "File read failed"); - } - free(data); - } while(0); +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_write(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - +static void js_storage_open_file(struct mjs* mjs) { const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + 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")); - mjs_val_t data_arg = mjs_arg(mjs, 1); - if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) { - ret_bad_args(mjs, "Data must be string, arraybuf or dataview"); + 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; } - if(mjs_is_data_view(data_arg)) { - data_arg = mjs_dataview_get_buf(mjs, data_arg); + + 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)); } - size_t data_len = 0; - const char* data = NULL; - if(mjs_is_string(data_arg)) { - data = mjs_get_string(mjs, &data_arg, &data_len); - } else if(mjs_is_typed_array(data_arg)) { - data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); - } - - mjs_val_t seek_arg = mjs_arg(mjs, 2); - - File* file = storage_file_alloc(storage->api); - if(!storage_file_open( - file, - path, - FSAM_WRITE, - mjs_is_number(seek_arg) ? FSOM_OPEN_ALWAYS : FSOM_CREATE_ALWAYS)) { - ret_int_err(mjs, storage_file_get_error_desc(file)); - - } else { - if(mjs_is_number(seek_arg)) { - storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true); - } - - size_t write = storage_file_write(file, data, data_len); - mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len)); - } - storage_file_free(file); + mjs_return(mjs, file_obj); } -static void js_storage_append(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; - +static void js_storage_file_exists(struct mjs* mjs) { const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + 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))); +} - mjs_val_t data_arg = mjs_arg(mjs, 1); - if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) { - ret_bad_args(mjs, "Data must be string, arraybuf or dataview"); +// ---=== 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; } - if(mjs_is_data_view(data_arg)) { - data_arg = mjs_dataview_get_buf(mjs, data_arg); - } - size_t data_len = 0; - const char* data = NULL; - if(mjs_is_string(data_arg)) { - data = mjs_get_string(mjs, &data_arg, &data_len); - } else if(mjs_is_typed_array(data_arg)) { - data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); + + 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); } - File* file = storage_file_alloc(storage->api); - if(!storage_file_open(file, path, FSAM_WRITE, FSOM_OPEN_APPEND)) { - ret_int_err(mjs, storage_file_get_error_desc(file)); - } else { - size_t write = storage_file_write(file, data, data_len); - mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len)); - } - storage_file_free(file); + storage_file_free(dir); + furi_string_free(file_path); + mjs_return(mjs, ret); } -static void js_storage_exists(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - +static void js_storage_directory_exists(struct mjs* mjs) { const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + 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))); +} - mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage->api, 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) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + 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))); +} - mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage->api, 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) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; + 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)); +} - const char* old_path; - if(!get_path_arg(mjs, &old_path, 0)) return; - - const char* new_path; - if(!get_path_arg(mjs, &new_path, 1)) return; - - FS_Error error = storage_common_copy(storage->api, old_path, new_path); - if(error == FSE_OK) { +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); - } else { - ret_int_err(mjs, storage_error_get_desc(error)); + return; } -} - -static void js_storage_move(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; - - const char* old_path; - if(!get_path_arg(mjs, &old_path, 0)) return; - - const char* new_path; - if(!get_path_arg(mjs, &new_path, 1)) return; - - FS_Error error = storage_common_rename(storage->api, old_path, new_path); - if(error == FSE_OK) { - mjs_return(mjs, MJS_UNDEFINED); - } else { - ret_int_err(mjs, storage_error_get_desc(error)); + 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_mkdir(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; - - mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage->api, path))); +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); } -static void* js_storage_create(struct mjs* mjs, mjs_val_t* object) { - JsStorageInst* storage = malloc(sizeof(JsStorageInst)); - mjs_val_t storage_obj = mjs_mk_object(mjs); - mjs_set(mjs, storage_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, storage)); - mjs_set(mjs, storage_obj, "read", ~0, MJS_MK_FN(js_storage_read)); - mjs_set(mjs, storage_obj, "write", ~0, MJS_MK_FN(js_storage_write)); - mjs_set(mjs, storage_obj, "append", ~0, MJS_MK_FN(js_storage_append)); - mjs_set(mjs, storage_obj, "exists", ~0, MJS_MK_FN(js_storage_exists)); - mjs_set(mjs, storage_obj, "remove", ~0, MJS_MK_FN(js_storage_remove)); - mjs_set(mjs, storage_obj, "copy", ~0, MJS_MK_FN(js_storage_copy)); - mjs_set(mjs, storage_obj, "move", ~0, MJS_MK_FN(js_storage_move)); - mjs_set(mjs, storage_obj, "mkdir", ~0, MJS_MK_FN(js_storage_mkdir)); - storage->api = furi_record_open(RECORD_STORAGE); - *object = storage_obj; - return storage; +// ---=== 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_destroy(void* inst) { - JsStorageInst* storage = inst; +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); - free(storage); } +// ---=== boilerplate ===--- + static const JsModuleDescriptor js_storage_desc = { "storage", js_storage_create, js_storage_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_submenu.c b/applications/system/js_app/modules/js_submenu.c deleted file mode 100644 index 5ab9bef77..000000000 --- a/applications/system/js_app/modules/js_submenu.c +++ /dev/null @@ -1,147 +0,0 @@ -#include -#include -#include -#include -#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; -} diff --git a/applications/system/js_app/modules/js_tests.c b/applications/system/js_app/modules/js_tests.c new file mode 100644 index 000000000..f27564000 --- /dev/null +++ b/applications/system/js_app/modules/js_tests.c @@ -0,0 +1,104 @@ +#include "../js_modules.h" // IWYU pragma: keep +#include +#include +#include + +#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; +} diff --git a/applications/system/js_app/modules/js_tests.h b/applications/system/js_app/modules/js_tests.h new file mode 100644 index 000000000..49f752c2b --- /dev/null +++ b/applications/system/js_app/modules/js_tests.h @@ -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); diff --git a/applications/system/js_app/modules/js_textbox.c b/applications/system/js_app/modules/js_textbox.c deleted file mode 100644 index b90dbc153..000000000 --- a/applications/system/js_app/modules/js_textbox.c +++ /dev/null @@ -1,219 +0,0 @@ -#include -#include -#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; -} diff --git a/applications/system/js_app/plugin_api/app_api_table_i.h b/applications/system/js_app/plugin_api/app_api_table_i.h index b48221343..b2debbde8 100644 --- a/applications/system/js_app/plugin_api/app_api_table_i.h +++ b/applications/system/js_app/plugin_api/app_api_table_i.h @@ -7,4 +7,5 @@ static constexpr auto app_api_table = sort(create_array_t( API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)), API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)), - API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)))); + API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)), + API_METHOD(js_module_get, void*, (JsModules*, const char*)))); diff --git a/applications/system/js_app/plugin_api/js_plugin_api.h b/applications/system/js_app/plugin_api/js_plugin_api.h index a817d34a9..421b68576 100644 --- a/applications/system/js_app/plugin_api/js_plugin_api.h +++ b/applications/system/js_app/plugin_api/js_plugin_api.h @@ -7,12 +7,16 @@ extern "C" { #endif +typedef void JsModules; + bool js_delay_with_flags(struct mjs* mjs, uint32_t time); void js_flags_set(struct mjs* mjs, uint32_t flags); uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout); +void* js_module_get(JsModules* modules, const char* name); + #ifdef __cplusplus } #endif diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/types/badusb/index.d.ts new file mode 100644 index 000000000..210790967 --- /dev/null +++ b/applications/system/js_app/types/badusb/index.d.ts @@ -0,0 +1,81 @@ +/** + * @brief Special key codes that this module recognizes + */ +export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI"; + +export type MainKey = + "DOWN" | "LEFT" | "RIGHT" | "UP" | + + "ENTER" | "PAUSE" | "CAPSLOCK" | "DELETE" | "BACKSPACE" | "END" | "ESC" | + "HOME" | "INSERT" | "NUMLOCK" | "PAGEUP" | "PAGEDOWN" | "PRINTSCREEN" | + "SCROLLLOCK" | "SPACE" | "TAB" | "MENU" | + + "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | + "F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" | + "F20" | "F21" | "F22" | "F23" | "F24" | + + "\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | + "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" | + "]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" | + + "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | + + "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | + "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | + "Y" | "Z" | + + "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | + "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | + "y" | "z"; + +export type KeyCode = MainKey | ModifierKey | number; + +/** + * @brief Initializes the module + * @param settings USB device settings. Omit to select default parameters + */ +export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void; + +/** + * @brief Tells whether the virtual USB HID device has successfully connected + */ +export declare function isConnected(): boolean; + +/** + * @brief Presses one or multiple keys at once, then releases them + * @param keys The arguments represent a set of keys to. Out of that set, only + * one of the keys may represent a "main key" (see `MainKey`), with + * the rest being modifier keys (see `ModifierKey`). + */ +export declare function press(...keys: KeyCode[]): void; + +/** + * @brief Presses one or multiple keys at once without releasing them + * @param keys The arguments represent a set of keys to. Out of that set, only + * one of the keys may represent a "main key" (see `MainKey`), with + * the rest being modifier keys (see `ModifierKey`). + */ +export declare function hold(...keys: KeyCode[]): void; + +/** + * @brief Releases one or multiple keys at once + * @param keys The arguments represent a set of keys to. Out of that set, only + * one of the keys may represent a "main key" (see `MainKey`), with + * the rest being modifier keys (see `ModifierKey`). + */ +export declare function release(...keys: KeyCode[]): void; + +/** + * @brief Prints a string by repeatedly pressing and releasing keys + * @param string The string to print + * @param delay How many milliseconds to wait between key presses + */ +export declare function print(string: string, delay?: number): void; + +/** + * @brief Prints a string by repeatedly pressing and releasing keys. Presses + * "Enter" after printing the string + * @param string The string to print + * @param delay How many milliseconds to wait between key presses + */ +export declare function println(): void; diff --git a/applications/system/js_app/types/event_loop/index.d.ts b/applications/system/js_app/types/event_loop/index.d.ts new file mode 100644 index 000000000..49237782c --- /dev/null +++ b/applications/system/js_app/types/event_loop/index.d.ts @@ -0,0 +1,70 @@ +type Lit = undefined | null | {}; + +/** + * Subscription control interface + */ +export interface Subscription { + /** + * Cancels the subscription, preventing any future events managed by the + * subscription from firing + */ + cancel(): void; +} + +/** + * Opaque event source identifier + */ +export type Contract = symbol; + +/** + * A callback can be assigned to an event loop to listen to an event. It may + * return an array with values that will be passed to it as arguments the next + * time that it is called. The first argument is always the subscription + * manager, and the second argument is always the item that trigged the event. + * The type of the item is defined by the event source. + */ +export type Callback = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void; + +/** + * Subscribes a callback to an event + * @param contract Event identifier + * @param callback Function to call when the event is triggered + * @param args Initial arguments passed to the callback + */ +export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; +/** + * Runs the event loop until it is stopped (potentially never) + */ +export function run(): void | never; +/** + * Stops the event loop + */ +export function stop(): void; + +/** + * Creates a timer event that can be subscribed to just like any other event + * @param mode Either `"oneshot"` or `"periodic"` + * @param interval Timer interval in milliseconds + */ +export function timer(mode: "oneshot" | "periodic", interval: number): Contract; + +/** + * Message queue + */ +export interface Queue { + /** + * Message event + */ + input: Contract; + /** + * Sends a message to the queue + * @param message message to send + */ + send(message: T): void; +} + +/** + * Creates a message queue + * @param length maximum queue capacity + */ +export function queue(length: number): Queue; diff --git a/applications/system/js_app/types/flipper/index.d.ts b/applications/system/js_app/types/flipper/index.d.ts new file mode 100644 index 000000000..b1b1d474b --- /dev/null +++ b/applications/system/js_app/types/flipper/index.d.ts @@ -0,0 +1,14 @@ +/** + * @brief Returns the device model + */ +export declare function getModel(): string; + +/** + * @brief Returns the name of the virtual dolphin + */ +export declare function getName(): string; + +/** + * @brief Returns the battery charge percentage + */ +export declare function getBatteryCharge(): number; diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts new file mode 100644 index 000000000..ab1660cf6 --- /dev/null +++ b/applications/system/js_app/types/global.d.ts @@ -0,0 +1,178 @@ +/** + * @brief Pauses JavaScript execution for a while + * @param ms How many milliseconds to pause the execution for + */ +declare function delay(ms: number): void; + +/** + * @brief Prints to the GUI console view + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the console view + */ +declare function print(...args: any[]): void; + +/** + * @brief Converts a number to a string + * @param value The number to convert to a string + * @param base Integer base (`2`...`16`), default: 16 + */ +declare function toString(value: number, base?: number): string; + +/** + * @brief Reads a JS value from a file + * + * Reads a file at the specified path, interprets it as a JS value and returns + * said value. + * + * @param path The path to the file + */ +declare function load(path: string): any; + +/** + * @brief mJS Foreign Pointer type + * + * JavaScript code cannot do anything with values of `RawPointer` type except + * acquire them from native code and pass them right back to other parts of + * native code. These values cannot be turned into something meaningful, nor can + * be they modified. + */ +declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * @brief Holds raw bytes + */ +declare class ArrayBuffer { + /** + * @brief The pointer to the byte buffer + * @note Like other `RawPointer` values, this value is essentially useless + * to JS code. + */ + getPtr: RawPointer; + /** + * @brief The length of the buffer in bytes + */ + byteLength: number; + /** + * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer + * @param start The index of the byte in the source buffer to be used as the + * start for the new buffer + * @param end The index of the byte in the source buffer that follows the + * byte to be used as the last byte for the new buffer + */ + slice(start: number, end?: number): ArrayBuffer; +} + +declare function ArrayBuffer(): ArrayBuffer; + +declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; + +declare class TypedArray { + /** + * @brief The length of the buffer in bytes + */ + byteLength: number; + /** + * @brief The length of the buffer in typed elements + */ + length: number; + /** + * @brief The underlying `ArrayBuffer` + */ + buffer: ArrayBuffer; +} + +declare class Uint8Array extends TypedArray<"u8"> { } +declare class Int8Array extends TypedArray<"i8"> { } +declare class Uint16Array extends TypedArray<"u16"> { } +declare class Int16Array extends TypedArray<"i16"> { } +declare class Uint32Array extends TypedArray<"u32"> { } +declare class Int32Array extends TypedArray<"i32"> { } + +declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; +declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; +declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; +declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; +declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; +declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; + +declare const console: { + /** + * @brief Prints to the UART logs at the `[I]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + log(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[D]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + debug(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[W]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + warn(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[E]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + error(...args: any[]): void; +}; + +declare class Array { + /** + * @brief Takes items out of the array + * + * Removes elements from the array and returns them in a new array + * + * @param start The index to start taking elements from + * @param deleteCount How many elements to take + * @returns The elements that were taken out of the original array as a new + * array + */ + splice(start: number, deleteCount: number): T[]; + /** + * @brief Adds a value to the end of the array + * @param value The value to add + * @returns New length of the array + */ + push(value: T): number; + /** + * @brief How many elements there are in the array + */ + length: number; +} + +declare class String { + /** + * @brief How many characters there are in the string + */ + length: number; + /** + * @brief Returns the character code at an index in the string + * @param index The index to consult + */ + charCodeAt(index: number): number; + /** + * See `charCodeAt` + */ + at(index: number): number; +} + +declare class Boolean { } + +declare class Function { } + +declare class Number { } + +declare class Object { } + +declare class RegExp { } + +declare interface IArguments { } + +declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gpio/index.d.ts b/applications/system/js_app/types/gpio/index.d.ts new file mode 100644 index 000000000..18705f898 --- /dev/null +++ b/applications/system/js_app/types/gpio/index.d.ts @@ -0,0 +1,45 @@ +import type { Contract } from "../event_loop"; + +export interface Mode { + direction: "in" | "out"; + outMode?: "push_pull" | "open_drain"; + inMode?: "analog" | "plain_digital" | "interrupt" | "event"; + edge?: "rising" | "falling" | "both"; + pull?: "up" | "down"; +} + +export interface Pin { + /** + * Configures a pin. This may be done several times. + * @param mode Pin configuration object + */ + init(mode: Mode): void; + /** + * Sets the output value of a pin if it's been configured with + * `direction: "out"`. + * @param value Logic value to output + */ + write(value: boolean): void; + /** + * Gets the input value of a pin if it's been configured with + * `direction: "in"`, but not `inMode: "analog"`. + */ + read(): boolean; + /** + * Gets the input voltage of a pin in millivolts if it's been configured + * with `direction: "in"` and `inMode: "analog"` + */ + read_analog(): number; + /** + * Returns an `event_loop` event that can be used to listen to interrupts, + * as configured by `init` + */ + interrupt(): Contract; +} + +/** + * Returns an object that can be used to manage a GPIO pin. For the list of + * available pins, see https://docs.flipper.net/gpio-and-modules#miFsS + * @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`) + */ +export function get(pin: string | number): Pin; diff --git a/applications/system/js_app/types/gui/dialog.d.ts b/applications/system/js_app/types/gui/dialog.d.ts new file mode 100644 index 000000000..6d9c8d43b --- /dev/null +++ b/applications/system/js_app/types/gui/dialog.d.ts @@ -0,0 +1,16 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + text: string, + left: string, + center: string, + right: string, +} +declare class Dialog extends View { + input: Contract<"left" | "center" | "right">; +} +declare class DialogFactory extends ViewFactory { } +declare const factory: DialogFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/empty_screen.d.ts b/applications/system/js_app/types/gui/empty_screen.d.ts new file mode 100644 index 000000000..c71e93b32 --- /dev/null +++ b/applications/system/js_app/types/gui/empty_screen.d.ts @@ -0,0 +1,7 @@ +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class EmptyScreen extends View { } +declare class EmptyScreenFactory extends ViewFactory { } +declare const factory: EmptyScreenFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts new file mode 100644 index 000000000..3f95ab780 --- /dev/null +++ b/applications/system/js_app/types/gui/index.d.ts @@ -0,0 +1,41 @@ +import type { Contract } from "../event_loop"; + +type Properties = { [K: string]: any }; + +export declare class View { + set

(property: P, value: Props[P]): void; +} + +export declare class ViewFactory> { + make(): V; + makeWith(initial: Partial): V; +} + +declare class ViewDispatcher { + /** + * Event source for `sendCustom` events + */ + custom: Contract; + /** + * Event source for navigation events (back key presses) + */ + navigation: Contract; + /** + * Sends a number to the custom event handler + * @param event number to send + */ + sendCustom(event: number): void; + /** + * Switches to a view + * @param assoc View-ViewDispatcher association as returned by `add` + */ + switchTo(assoc: View): void; + /** + * Sends this ViewDispatcher to the front or back, above or below all other + * GUI viewports + * @param direction Either `"front"` or `"back"` + */ + sendTo(direction: "front" | "back"): void; +} + +export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/types/gui/loading.d.ts b/applications/system/js_app/types/gui/loading.d.ts new file mode 100644 index 000000000..73a963349 --- /dev/null +++ b/applications/system/js_app/types/gui/loading.d.ts @@ -0,0 +1,7 @@ +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class Loading extends View { } +declare class LoadingFactory extends ViewFactory { } +declare const factory: LoadingFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/submenu.d.ts b/applications/system/js_app/types/gui/submenu.d.ts new file mode 100644 index 000000000..59d535864 --- /dev/null +++ b/applications/system/js_app/types/gui/submenu.d.ts @@ -0,0 +1,13 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + items: string[], +}; +declare class Submenu extends View { + chosen: Contract; +} +declare class SubmenuFactory extends ViewFactory { } +declare const factory: SubmenuFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/text_box.d.ts b/applications/system/js_app/types/gui/text_box.d.ts new file mode 100644 index 000000000..3dbbac571 --- /dev/null +++ b/applications/system/js_app/types/gui/text_box.d.ts @@ -0,0 +1,14 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + text: string, + font: "text" | "hex", + focus: "start" | "end", +} +declare class TextBox extends View { + chosen: Contract; +} +declare class TextBoxFactory extends ViewFactory { } +declare const factory: TextBoxFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/text_input.d.ts b/applications/system/js_app/types/gui/text_input.d.ts new file mode 100644 index 000000000..96652b1d4 --- /dev/null +++ b/applications/system/js_app/types/gui/text_input.d.ts @@ -0,0 +1,14 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + minLength: number, + maxLength: number, +} +declare class TextInput extends View { + input: Contract; +} +declare class TextInputFactory extends ViewFactory { } +declare const factory: TextInputFactory; +export = factory; diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/types/math/index.d.ts new file mode 100644 index 000000000..25abca4af --- /dev/null +++ b/applications/system/js_app/types/math/index.d.ts @@ -0,0 +1,24 @@ +export function abs(n: number): number; +export function acos(n: number): number; +export function acosh(n: number): number; +export function asin(n: number): number; +export function asinh(n: number): number; +export function atan(n: number): number; +export function atan2(a: number, b: number): number; +export function atanh(n: number): number; +export function cbrt(n: number): number; +export function ceil(n: number): number; +export function clz32(n: number): number; +export function cos(n: number): number; +export function exp(n: number): number; +export function floor(n: number): number; +export function max(n: number, m: number): number; +export function min(n: number, m: number): number; +export function pow(n: number, m: number): number; +export function random(): number; +export function sign(n: number): number; +export function sin(n: number): number; +export function sqrt(n: number): number; +export function trunc(n: number): number; +declare const PI: number; +declare const EPSILON: number; diff --git a/applications/system/js_app/types/notification/index.d.ts b/applications/system/js_app/types/notification/index.d.ts new file mode 100644 index 000000000..947daba21 --- /dev/null +++ b/applications/system/js_app/types/notification/index.d.ts @@ -0,0 +1,20 @@ +/** + * @brief Signals success to the user via the color LED, speaker and vibration + * motor + */ +export declare function success(): void; + +/** + * @brief Signals failure to the user via the color LED, speaker and vibration + * motor + */ +export declare function error(): void; + +export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta"; + +/** + * @brief Displays a basic color on the color LED + * @param color The color to display, see `Color` + * @param duration The duration, either `"short"` (10ms) or `"long"` (100ms) + */ +export declare function blink(color: Color, duration: "short" | "long"): void; diff --git a/applications/system/js_app/types/serial/index.d.ts b/applications/system/js_app/types/serial/index.d.ts new file mode 100644 index 000000000..1a7ed6397 --- /dev/null +++ b/applications/system/js_app/types/serial/index.d.ts @@ -0,0 +1,77 @@ +/** + * @brief Initializes the serial port + * @param port The port to initialize (`"lpuart"` or `"start"`) + * @param baudRate + */ +export declare function setup(port: "lpuart" | "usart", baudRate: number): void; + +/** + * @brief Writes data to the serial port + * @param value The data to write: + * - Strings will get sent as ASCII. + * - Numbers will get sent as a single byte. + * - Arrays of numbers will get sent as a sequence of bytes. + * - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence + * of bytes. + */ +export declare function write(value: string | number | number[] | ArrayBuffer | TypedArray): void; + +/** + * @brief Reads data from the serial port + * @param length The number of bytes to read + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. + * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes + * were read. + */ +export declare function read(length: number, timeout?: number): string | undefined; + +/** + * @brief Reads data from the serial port + * + * Data is read one character after another until either a `\r` or `\n` + * character is received, neither of which is included in the result. + * + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. The timeout only + * applies to characters, not entire strings. + * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes + * were read. + */ +export declare function readln(timeout?: number): string; + +/** + * @brief Reads data from the serial port + * @param length The number of bytes to read + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. + * @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were + * read. + */ +export declare function readBytes(length: number, timeout?: number): ArrayBuffer; + +/** + * @brief Reads data from the serial port, trying to match it to a pattern + * @param patterns A single pattern or an array of patterns: + * - If the argument is a single `string`, this function will + * match against the given string. + * - If the argument is an array of `number`s, this function + * will match against the given sequence of bytes, + * - If the argument is an array of `string`s, this function + * will match against any string out of the ones that were + * provided. + * - If the argument is an array of arrays of `number`s, this + * function will match against any sequence of bytes out of + * the ones that were provided. + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. The timeout only + * applies to characters, not entire strings. + * @returns The index of the matched pattern if multiple were provided, or 0 if + * only one was provided and it matched, or `undefined` if none of the + * patterns matched. + */ +export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined; diff --git a/applications/system/js_app/types/storage/index.d.ts b/applications/system/js_app/types/storage/index.d.ts new file mode 100644 index 000000000..0dd29e121 --- /dev/null +++ b/applications/system/js_app/types/storage/index.d.ts @@ -0,0 +1,237 @@ +/** + * File readability mode: + * - `"r"`: read-only + * - `"w"`: write-only + * - `"rw"`: read-write + */ +export type AccessMode = "r" | "w" | "rw"; + +/** + * File creation mode: + * - `"open_existing"`: open file or fail if it doesn't exist + * - `"open_always"`: open file or create a new empty one if it doesn't exist + * - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist + * - `"create_new"`: create new file or fail if it exists + * - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist + */ +export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always"; + +/** Standard UNIX timestamp */ +export type Timestamp = number; + +/** File information structure */ +export declare class FileInfo { + /** + * Full path (e.g. "/ext/test", returned by `stat`) or file name + * (e.g. "test", returned by `readDirectory`) + */ + path: string; + /** + * Is the file a directory? + */ + isDirectory: boolean; + /** + * File size in bytes, or 0 in the case of directories + */ + size: number; + /** + * Time of last access as a UNIX timestamp + */ + accessTime: Timestamp; +} + +/** Filesystem information structure */ +export declare class FsInfo { + /** Total size of the filesystem, in bytes */ + totalSpace: number; + /** Free space in the filesystem, in bytes */ + freeSpace: number; +} + +// file operations + +/** File class */ +export declare class File { + /** + * Closes the file. After this method is called, all other operations + * related to this file become unavailable. + * @returns `true` on success, `false` on failure + */ + close(): boolean; + /** + * Is the file currently open? + */ + isOpen(): boolean; + /** + * Reads bytes from a file opened in read-only or read-write mode + * @param mode The data type to interpret the bytes as: a `string` decoded + * from ASCII data (`"ascii"`), or an `ArrayBuf` (`"binary"`) + * @param bytes How many bytes to read from the file + * @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode + * is `ascii`. The number of bytes that was actually read may be + * fewer than requested. + */ + read(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T; + /** + * Writes bytes to a file opened in write-only or read-write mode + * @param data The data to write: a string that will be ASCII-encoded, or an + * ArrayBuf + * @returns the amount of bytes that was actually written + */ + write(data: ArrayBuffer | string): number; + /** + * Moves the R/W pointer forward + * @param bytes How many bytes to move the pointer forward by + * @returns `true` on success, `false` on failure + */ + seekRelative(bytes: number): boolean; + /** + * Moves the R/W pointer to an absolute position inside the file + * @param bytes The position inside the file + * @returns `true` on success, `false` on failure + */ + seekAbsolute(bytes: number): boolean; + /** + * Gets the absolute position of the R/W pointer in bytes + */ + tell(): number; + /** + * Discards the data after the current position of the R/W pointer in a file + * opened in either write-only or read-write mode. + * @returns `true` on success, `false` on failure + */ + truncate(): boolean; + /** + * Reads the total size of the file in bytes + */ + size(): number; + /** + * Detects whether the R/W pointer has reached the end of the file + */ + eof(): boolean; + /** + * Copies bytes from the R/W pointer in the current file to the R/W pointer + * in another file + * @param dest The file to copy the bytes into + * @param bytes The number of bytes to copy + * @returns `true` on success, `false` on failure + */ + copyTo(dest: File, bytes: number): boolean; +} + +/** + * Opens a file + * @param path The path to the file + * @param accessMode `"r"`, `"w"` or `"rw"`; see `AccessMode` + * @param openMode `"open_existing"`, `"open_always"`, `"open_append"`, + * `"create_new"` or `"create_always"`; see `OpenMode` + * @returns a `File` on success, or `undefined` on failure + */ +export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined; +/** + * Detects whether a file exists + * @param path The path to the file + * @returns `true` on success, `false` on failure + */ +export declare function fileExists(path: string): boolean; + +// directory operations + +/** + * Reads the list of files in a directory + * @param path The path to the directory + * @returns Array of `FileInfo` structures with directory entries, + * or `undefined` on failure + */ +export declare function readDirectory(path: string): FileInfo[] | undefined; +/** + * Detects whether a directory exists + * @param path The path to the directory + */ +export declare function directoryExists(path: string): boolean; +/** + * Creates an empty directory + * @param path The path to the new directory + * @returns `true` on success, `false` on failure + */ +export declare function makeDirectory(path: string): boolean; + +// common (file/dir) operations + +/** + * Detects whether a file or a directory exists + * @param path The path to the file or directory + */ +export declare function fileOrDirExists(path: string): boolean; +/** + * Acquires metadata about a file or directory + * @param path The path to the file or directory + * @returns A `FileInfo` structure or `undefined` on failure + */ +export declare function stat(path: string): FileInfo | undefined; +/** + * Removes a file or an empty directory + * @param path The path to the file or directory + * @returns `true` on success, `false` on failure + */ +export declare function remove(path: string): boolean; +/** + * Removes a file or recursively removes a possibly non-empty directory + * @param path The path to the file or directory + * @returns `true` on success, `false` on failure + */ +export declare function rmrf(path: string): boolean; +/** + * Renames or moves a file or directory + * @param oldPath The old path to the file or directory + * @param newPath The new path that the file or directory will become accessible + * under + * @returns `true` on success, `false` on failure + */ +export declare function rename(oldPath: string, newPath: string): boolean; +/** + * Copies a file or recursively copies a possibly non-empty directory + * @param oldPath The original path to the file or directory + * @param newPath The new path that the copy of the file or directory will be + * accessible under + */ +export declare function copy(oldPath: string, newPath: string): boolean; +/** + * Fetches generic information about a filesystem + * @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`) + */ +export declare function fsInfo(filesystem: string): FsInfo | undefined; +/** + * Chooses the next available filename with a numeric suffix in a directory + * + * ``` + * "/ext/example_dir/example_file123.txt" + * \______________/ \__________/\_/\__/ + * dirPath fileName | | + * | +---- fileExt + * +------- selected by this function + * ``` + * + * @param dirPath The directory to look in + * @param fileName The base of the filename (the part before the numeric suffix) + * @param fileExt The extension of the filename (the part after the numeric suffix) + * @param maxLen The maximum length of the filename with the numeric suffix + * @returns The base of the filename with the next available numeric suffix, + * without the extension or the base directory. + */ +export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string; + +// path operations that do not access the filesystem + +/** + * Determines whether the two paths are equivalent. Respects filesystem-defined + * path equivalence rules. + */ +export declare function arePathsEqual(path1: string, path2: string): boolean; +/** + * Determines whether a path is a subpath of another path. Respects + * filesystem-defined path equivalence rules. + * @param parentPath The parent path + * @param childPath The child path + */ +export declare function isSubpathOf(parentPath: string, childPath: string): boolean; diff --git a/applications/system/js_app/types/tests/index.d.ts b/applications/system/js_app/types/tests/index.d.ts new file mode 100644 index 000000000..8aaeec5e5 --- /dev/null +++ b/applications/system/js_app/types/tests/index.d.ts @@ -0,0 +1,8 @@ +/** + * Unit test module. Only available if the firmware has been configured with + * `FIRMWARE_APP_SET=unit_tests`. + */ + +export function fail(message: string): never; +export function assert_eq(expected: T, result: T): void | never; +export function assert_float_close(expected: number, result: number, epsilon: number): void | never;