diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index c87305847..dec3283e4 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -221,6 +221,14 @@ App( requires=["unit_tests"], ) +App( + appid="test_js", + sources=["tests/common/*.c", "tests/js/*.c"], + apptype=FlipperAppType.PLUGIN, + entry_point="get_api", + requires=["unit_tests", "js_app"], +) + App( appid="test_strint", sources=["tests/common/*.c", "tests/strint/*.c"], diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js new file mode 100644 index 000000000..0927595a2 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -0,0 +1,4 @@ +let tests = require("tests"); + +tests.assert_eq(1337, 1337); +tests.assert_eq("hello", "hello"); diff --git a/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js b/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js new file mode 100644 index 000000000..0437b8293 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js @@ -0,0 +1,30 @@ +let tests = require("tests"); +let event_loop = require("event_loop"); + +let ext = { + i: 0, + received: false, +}; + +let queue = event_loop.queue(16); + +event_loop.subscribe(queue.input, function (_, item, tests, ext) { + tests.assert_eq(123, item); + ext.received = true; +}, tests, ext); + +event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) { + ext.i++; + queue.send(123); + if (counter === 10) + event_loop.stop(); + return [queue, counter + 1, ext]; +}, queue, 1, ext); + +event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) { + tests.fail("event loop was not stopped"); +}, tests); + +event_loop.run(); +tests.assert_eq(10, ext.i); +tests.assert_eq(true, ext.received); diff --git a/applications/debug/unit_tests/resources/unit_tests/js/math.js b/applications/debug/unit_tests/resources/unit_tests/js/math.js new file mode 100644 index 000000000..ea8d80f91 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/math.js @@ -0,0 +1,34 @@ +let tests = require("tests"); +let math = require("math"); + +// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16 + +// basics +tests.assert_float_close(5, math.abs(-5), math.EPSILON); +tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON); +tests.assert_float_close(5, math.abs(5), math.EPSILON); +tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON); +tests.assert_float_close(3, math.cbrt(27), math.EPSILON); +tests.assert_float_close(6, math.ceil(5.3), math.EPSILON); +tests.assert_float_close(31, math.clz32(1), math.EPSILON); +tests.assert_float_close(5, math.floor(5.7), math.EPSILON); +tests.assert_float_close(5, math.max(3, 5), math.EPSILON); +tests.assert_float_close(3, math.min(3, 5), math.EPSILON); +tests.assert_float_close(-1, math.sign(-5), math.EPSILON); +tests.assert_float_close(5, math.trunc(5.7), math.EPSILON); + +// trig +tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON); +tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON); +tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON); +tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON); +tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON); +tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON); +tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON); +tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15 +tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16 + +// powers +tests.assert_float_close(5, math.sqrt(25), math.EPSILON); +tests.assert_float_close(8, math.pow(2, 3), math.EPSILON); +tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16 diff --git a/applications/debug/unit_tests/resources/unit_tests/js/storage.js b/applications/debug/unit_tests/resources/unit_tests/js/storage.js new file mode 100644 index 000000000..872b29cfb --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/storage.js @@ -0,0 +1,136 @@ +let storage = require("storage"); +let tests = require("tests"); + +let baseDir = "/ext/.tmp/unit_tests"; + +tests.assert_eq(true, storage.rmrf(baseDir)); +tests.assert_eq(true, storage.makeDirectory(baseDir)); + +// write +let file = storage.openFile(baseDir + "/helloworld", "w", "create_always"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.write("Hello, World!")); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// read +file = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.size()); +tests.assert_eq("Hello, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// seek +file = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.size()); +tests.assert_eq("Hello, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.seekAbsolute(1)); +tests.assert_eq(true, file.seekRelative(2)); +tests.assert_eq(3, file.tell()); +tests.assert_eq(false, file.eof()); +tests.assert_eq("lo, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.eof()); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// byte-level copy +let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always"); +tests.assert_eq(true, !!src); +tests.assert_eq(true, src.isOpen()); +tests.assert_eq(true, !!dst); +tests.assert_eq(true, dst.isOpen()); +tests.assert_eq(true, src.copyTo(dst, 10)); +tests.assert_eq(true, dst.seekAbsolute(0)); +tests.assert_eq("Hello, Wor", dst.read("ascii", 128)); +tests.assert_eq(true, src.copyTo(dst, 3)); +tests.assert_eq(true, dst.seekAbsolute(0)); +tests.assert_eq("Hello, World!", dst.read("ascii", 128)); +tests.assert_eq(true, src.eof()); +tests.assert_eq(true, src.close()); +tests.assert_eq(false, src.isOpen()); +tests.assert_eq(true, dst.eof()); +tests.assert_eq(true, dst.close()); +tests.assert_eq(false, dst.isOpen()); + +// truncate +tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2")); +file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.seekAbsolute(5)); +tests.assert_eq(true, file.truncate()); +tests.assert_eq(true, file.close()); +file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq("Hello", file.read("ascii", 128)); +tests.assert_eq(true, file.close()); + +// existence +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2")); +tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123")); +tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir)); +tests.assert_eq(true, storage.directoryExists(baseDir)); +tests.assert_eq(true, storage.fileOrDirExists(baseDir)); +tests.assert_eq(true, storage.remove(baseDir + "/helloworld2")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2")); + +// stat +let stat = storage.stat(baseDir + "/helloworld"); +tests.assert_eq(true, !!stat); +tests.assert_eq(baseDir + "/helloworld", stat.path); +tests.assert_eq(false, stat.isDirectory); +tests.assert_eq(13, stat.size); + +// rename +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); + +// copy +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123")); + +// next avail +tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20)); + +// fs info +let fsInfo = storage.fsInfo("/ext"); +tests.assert_eq(true, !!fsInfo); +tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/ +fsInfo = storage.fsInfo("/int"); +tests.assert_eq(true, !!fsInfo); +tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); + +// path operations +tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test")); +tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt")); +tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub")); +tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test")); + +// dir +let entries = storage.readDirectory(baseDir); +tests.assert_eq(true, !!entries); +// FIXME: (-nofl) this test suite assumes that files are listed by +// `readDirectory` in the exact order that they were created, which is not +// something that is actually guaranteed. +// Possible solution: sort and compare the array. +tests.assert_eq("helloworld", entries[0].path); +tests.assert_eq("helloworld123", entries[1].path); + +tests.assert_eq(true, storage.rmrf(baseDir)); +tests.assert_eq(true, storage.makeDirectory(baseDir)); diff --git a/applications/debug/unit_tests/tests/js/js_test.c b/applications/debug/unit_tests/tests/js/js_test.c new file mode 100644 index 000000000..af590e899 --- /dev/null +++ b/applications/debug/unit_tests/tests/js/js_test.c @@ -0,0 +1,88 @@ +#include "../test.h" // IWYU pragma: keep + +#include +#include +#include + +#include +#include + +#include + +#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js") + +typedef enum { + JsTestsFinished = 1, + JsTestsError = 2, +} JsTestFlag; + +typedef struct { + FuriEventFlag* event_flags; + FuriString* error_string; +} JsTestCallbackContext; + +static void js_test_callback(JsThreadEvent event, const char* msg, void* param) { + JsTestCallbackContext* context = param; + if(event == JsThreadEventPrint) { + FURI_LOG_I("js_test", "%s", msg); + } else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) { + context->error_string = furi_string_alloc_set_str(msg); + furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError); + } else if(event == JsThreadEventDone) { + furi_event_flag_set(context->event_flags, JsTestsFinished); + } +} + +static void js_test_run(const char* script_path) { + JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext)); + context->event_flags = furi_event_flag_alloc(); + + JsThread* thread = js_thread_run(script_path, js_test_callback, context); + uint32_t flags = furi_event_flag_wait( + context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever); + if(flags & FuriFlagError) { + // getting the flags themselves should not fail + furi_crash(); + } + + FuriString* error_string = context->error_string; + + js_thread_stop(thread); + furi_event_flag_free(context->event_flags); + free(context); + + if(flags & JsTestsError) { + // memory leak: not freeing the FuriString if the tests fail, + // because mu_fail executes a return + // + // who cares tho? + mu_fail(furi_string_get_cstr(error_string)); + } +} + +MU_TEST(js_test_basic) { + js_test_run(JS_SCRIPT_PATH("basic")); +} +MU_TEST(js_test_math) { + js_test_run(JS_SCRIPT_PATH("math")); +} +MU_TEST(js_test_event_loop) { + js_test_run(JS_SCRIPT_PATH("event_loop")); +} +MU_TEST(js_test_storage) { + js_test_run(JS_SCRIPT_PATH("storage")); +} + +MU_TEST_SUITE(test_js) { + MU_RUN_TEST(js_test_basic); + MU_RUN_TEST(js_test_math); + MU_RUN_TEST(js_test_event_loop); + MU_RUN_TEST(js_test_storage); +} + +int run_minunit_test_js(void) { + MU_RUN_SUITE(test_js); + return MU_EXIT_CODE; +} + +TEST_API_DEFINE(run_minunit_test_js) diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index 9310cfc9c..9ca3bb403 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -31,7 +31,7 @@ extern "C" { #include #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf -#define __func__ __FUNCTION__ +#define __func__ __FUNCTION__ //-V1059 #endif #elif defined(__unix__) || defined(__unix) || defined(unix) || \ @@ -56,7 +56,7 @@ extern "C" { #endif #if __GNUC__ >= 5 && !defined(__STDC_VERSION__) -#define __func__ __extension__ __FUNCTION__ +#define __func__ __extension__ __FUNCTION__ //-V1059 #endif #else @@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...); MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;) /* Test runner */ +//-V:MU_RUN_TEST:550 #define MU_RUN_TEST(test) \ MU__SAFE_BLOCK( \ if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \ diff --git a/applications/debug/unit_tests/unit_test_api_table_i.h b/applications/debug/unit_tests/unit_test_api_table_i.h index 50524e5b7..10b089022 100644 --- a/applications/debug/unit_tests/unit_test_api_table_i.h +++ b/applications/debug/unit_tests/unit_test_api_table_i.h @@ -7,7 +7,7 @@ #include #include -#include +#include static constexpr auto unit_tests_api_table = sort(create_array_t( API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)), @@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t( xQueueGenericSend, BaseType_t, (QueueHandle_t, const void* const, TickType_t, const BaseType_t)), - API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)), - API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)), API_METHOD( - furi_event_loop_subscribe_message_queue, - void, - (FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)), - API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)), - API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)), - API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)), + js_thread_run, + JsThread*, + (const char* script_path, JsThreadCallback callback, void* context)), + API_METHOD(js_thread_stop, void, (JsThread * worker)), API_VARIABLE(PB_Main_msg, PB_Main_msg_t))); diff --git a/applications/services/gui/modules/text_input.c b/applications/services/gui/modules/text_input.c index dc1c9c8c1..753f0b3b8 100644 --- a/applications/services/gui/modules/text_input.c +++ b/applications/services/gui/modules/text_input.c @@ -18,6 +18,7 @@ typedef struct { const char* header; char* text_buffer; size_t text_buffer_size; + size_t minimum_length; bool clear_default_text; TextInputCallback callback; @@ -321,7 +322,7 @@ static void text_input_handle_ok(TextInput* text_input, TextInputModel* model, b model->text_buffer, model->validator_text, model->validator_callback_context))) { model->validator_message_visible = true; furi_timer_start(text_input->timer, furi_kernel_get_tick_frequency() * 4); - } else if(model->callback != 0 && text_length > 0) { + } else if(model->callback != 0 && text_length >= model->minimum_length) { model->callback(model->callback_context); } } else if(selected == BACKSPACE_KEY) { @@ -487,6 +488,7 @@ void text_input_reset(TextInput* text_input) { model->header = ""; model->selected_row = 0; model->selected_column = 0; + model->minimum_length = 1; model->clear_default_text = false; model->text_buffer = NULL; model->text_buffer_size = 0; @@ -531,6 +533,14 @@ void text_input_set_result_callback( true); } +void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length) { + with_view_model( + text_input->view, + TextInputModel * model, + { model->minimum_length = minimum_length; }, + true); +} + void text_input_set_validator( TextInput* text_input, TextInputValidatorCallback callback, diff --git a/applications/services/gui/modules/text_input.h b/applications/services/gui/modules/text_input.h index b6ca6b54f..b6198f969 100644 --- a/applications/services/gui/modules/text_input.h +++ b/applications/services/gui/modules/text_input.h @@ -65,6 +65,13 @@ void text_input_set_result_callback( size_t text_buffer_size, bool clear_default_text); +/** + * @brief Sets the minimum length of a TextInput + * @param [in] text_input TextInput + * @param [in] minimum_length Minimum input length + */ +void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length); + void text_input_set_validator( TextInput* text_input, TextInputValidatorCallback callback, diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 63878fc19..6db4d8241 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -5,6 +5,12 @@ #define VIEW_DISPATCHER_QUEUE_LEN (16U) ViewDispatcher* view_dispatcher_alloc(void) { + ViewDispatcher* dispatcher = view_dispatcher_alloc_ex(furi_event_loop_alloc()); + dispatcher->is_event_loop_owned = true; + return dispatcher; +} + +ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop) { ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher)); view_dispatcher->view_port = view_port_alloc(); @@ -16,7 +22,7 @@ ViewDispatcher* view_dispatcher_alloc(void) { ViewDict_init(view_dispatcher->views); - view_dispatcher->event_loop = furi_event_loop_alloc(); + view_dispatcher->event_loop = loop; view_dispatcher->input_queue = furi_message_queue_alloc(VIEW_DISPATCHER_QUEUE_LEN, sizeof(InputEvent)); @@ -57,7 +63,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) { furi_message_queue_free(view_dispatcher->input_queue); furi_message_queue_free(view_dispatcher->event_queue); - furi_event_loop_free(view_dispatcher->event_loop); + if(view_dispatcher->is_event_loop_owned) furi_event_loop_free(view_dispatcher->event_loop); // Free dispatcher free(view_dispatcher); } @@ -85,6 +91,7 @@ void view_dispatcher_set_tick_event_callback( ViewDispatcherTickEventCallback callback, uint32_t tick_period) { furi_check(view_dispatcher); + furi_check(view_dispatcher->is_event_loop_owned); view_dispatcher->tick_event_callback = callback; view_dispatcher->tick_period = tick_period; } @@ -106,11 +113,12 @@ void view_dispatcher_run(ViewDispatcher* view_dispatcher) { uint32_t tick_period = view_dispatcher->tick_period == 0 ? FuriWaitForever : view_dispatcher->tick_period; - furi_event_loop_tick_set( - view_dispatcher->event_loop, - tick_period, - view_dispatcher_handle_tick_event, - view_dispatcher); + if(view_dispatcher->is_event_loop_owned) + furi_event_loop_tick_set( + view_dispatcher->event_loop, + tick_period, + view_dispatcher_handle_tick_event, + view_dispatcher); furi_event_loop_run(view_dispatcher->event_loop); diff --git a/applications/services/gui/view_dispatcher.h b/applications/services/gui/view_dispatcher.h index 9fbf89791..5820bcad3 100644 --- a/applications/services/gui/view_dispatcher.h +++ b/applications/services/gui/view_dispatcher.h @@ -47,6 +47,15 @@ typedef void (*ViewDispatcherTickEventCallback)(void* context); */ ViewDispatcher* view_dispatcher_alloc(void); +/** Allocate ViewDispatcher instance with an externally owned event loop. If + * this constructor is used instead of `view_dispatcher_alloc`, the burden of + * freeing the event loop is placed on the caller. + * + * @param loop pointer to FuriEventLoop instance + * @return pointer to ViewDispatcher instance + */ +ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop); + /** Free ViewDispatcher instance * * @warning All added views MUST be removed using view_dispatcher_remove_view() @@ -97,6 +106,10 @@ void view_dispatcher_set_navigation_event_callback( /** Set tick event handler * + * @warning Requires the event loop to be owned by the view dispatcher, i.e. + * it should have been instantiated with `view_dispatcher_alloc`, not + * `view_dispatcher_alloc_ex`. + * * @param view_dispatcher ViewDispatcher instance * @param callback ViewDispatcherTickEventCallback * @param tick_period callback call period diff --git a/applications/services/gui/view_dispatcher_i.h b/applications/services/gui/view_dispatcher_i.h index c6c8dc665..3d84b5499 100644 --- a/applications/services/gui/view_dispatcher_i.h +++ b/applications/services/gui/view_dispatcher_i.h @@ -14,6 +14,7 @@ DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT struct ViewDispatcher { + bool is_event_loop_owned; FuriEventLoop* event_loop; FuriMessageQueue* input_queue; FuriMessageQueue* event_queue; diff --git a/applications/services/storage/storage.h b/applications/services/storage/storage.h index ea0ff24ad..c28a5e10f 100644 --- a/applications/services/storage/storage.h +++ b/applications/services/storage/storage.h @@ -377,7 +377,7 @@ void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, Furi * @param storage pointer to a storage API instance. * @param source pointer to a zero-terminated string containing the source path. * @param dest pointer to a zero-terminated string containing the destination path. - * @return FSE_OK if the migration was successfull completed, any other error code on failure. + * @return FSE_OK if the migration was successfully completed, any other error code on failure. */ FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest); @@ -425,7 +425,7 @@ bool storage_common_is_subdir(Storage* storage, const char* parent, const char* /******************* Error Functions *******************/ /** - * @brief Get the textual description of a numeric error identifer. + * @brief Get the textual description of a numeric error identifier. * * @param error_id numeric identifier of the error in question. * @return pointer to a statically allocated zero-terminated string containing the respective error text. diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index a7ae5c7c7..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,11 +107,11 @@ App( ) App( - appid="js_submenu", + appid="js_gpio", apptype=FlipperAppType.PLUGIN, - entry_point="js_submenu_ep", - requires=["js_app"], - sources=["modules/js_submenu.c"], + entry_point="js_gpio_ep", + requires=["js_app", "js_event_loop"], + sources=["modules/js_gpio.c"], ) App( @@ -64,9 +123,9 @@ App( ) App( - appid="js_textbox", + appid="js_storage", apptype=FlipperAppType.PLUGIN, - entry_point="js_textbox_ep", + entry_point="js_storage_ep", requires=["js_app"], - sources=["modules/js_textbox.c"], + sources=["modules/js_storage.c"], ) diff --git a/applications/system/js_app/examples/apps/Scripts/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/badusb_demo.js index 21090f603..7284d86b7 100644 --- a/applications/system/js_app/examples/apps/Scripts/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/badusb_demo.js @@ -1,33 +1,58 @@ let badusb = require("badusb"); let notify = require("notification"); let flipper = require("flipper"); -let dialog = require("dialog"); +let eventLoop = require("event_loop"); +let gui = require("gui"); +let dialog = require("gui/dialog"); -badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfr_name: "Flipper", prod_name: "Zero" }); -dialog.message("BadUSB demo", "Press OK to start"); +let views = { + dialog: dialog.makeWith({ + header: "BadUSB demo", + text: "Press OK to start", + center: "Start", + }), +}; -if (badusb.isConnected()) { - notify.blink("green", "short"); - print("USB is connected"); +badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, mfrName: "Flipper", prodName: "Zero" }); - badusb.println("Hello, world!"); +eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) { + if (button !== "center") + return; - badusb.press("CTRL", "a"); - badusb.press("CTRL", "c"); - badusb.press("DOWN"); - delay(1000); - badusb.press("CTRL", "v"); - delay(1000); - badusb.press("CTRL", "v"); + gui.viewDispatcher.sendTo("back"); - badusb.println("1234", 200); + if (badusb.isConnected()) { + notify.blink("green", "short"); + print("USB is connected"); - badusb.println("Flipper Model: " + flipper.getModel()); - badusb.println("Flipper Name: " + flipper.getName()); - badusb.println("Battery level: " + to_string(flipper.getBatteryCharge()) + "%"); + badusb.println("Hello, world!"); - notify.success(); -} else { - print("USB not connected"); - notify.error(); -} + badusb.press("CTRL", "a"); + badusb.press("CTRL", "c"); + badusb.press("DOWN"); + delay(1000); + badusb.press("CTRL", "v"); + delay(1000); + badusb.press("CTRL", "v"); + + badusb.println("1234", 200); + + badusb.println("Flipper Model: " + flipper.getModel()); + badusb.println("Flipper Name: " + flipper.getName()); + badusb.println("Battery level: " + toString(flipper.getBatteryCharge()) + "%"); + + notify.success(); + } else { + print("USB not connected"); + notify.error(); + } + + eventLoop.stop(); +}, eventLoop, gui); + +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _item, eventLoop) { + eventLoop.stop(); +}, eventLoop); + +gui.viewDispatcher.switchTo(views.dialog); +eventLoop.run(); diff --git a/applications/system/js_app/examples/apps/Scripts/delay.js b/applications/system/js_app/examples/apps/Scripts/delay.js index 9f64abee8..5d8fbe422 100644 --- a/applications/system/js_app/examples/apps/Scripts/delay.js +++ b/applications/system/js_app/examples/apps/Scripts/delay.js @@ -6,4 +6,4 @@ print("2"); delay(1000) print("3"); delay(1000) -print("end"); \ No newline at end of file +print("end"); diff --git a/applications/system/js_app/examples/apps/Scripts/dialog.js b/applications/system/js_app/examples/apps/Scripts/dialog.js deleted file mode 100644 index 9fc44f8b9..000000000 --- a/applications/system/js_app/examples/apps/Scripts/dialog.js +++ /dev/null @@ -1,19 +0,0 @@ -let dialog = require("dialog"); - -let result1 = dialog.message("Dialog demo", "Press OK to start"); -print(result1); - -let dialog_params = ({ - header: "Test_header", - text: "Test_text", - button_left: "Left", - button_right: "Right", - button_center: "OK" -}); - -let result2 = dialog.custom(dialog_params); -if (result2 === "") { - print("Back is pressed"); -} else { - print(result2, "is pressed"); -} diff --git a/applications/system/js_app/examples/apps/Scripts/event_loop.js b/applications/system/js_app/examples/apps/Scripts/event_loop.js new file mode 100644 index 000000000..ad2f8a7dc --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/event_loop.js @@ -0,0 +1,25 @@ +let eventLoop = require("event_loop"); + +// print a string after 1337 milliseconds +eventLoop.subscribe(eventLoop.timer("oneshot", 1337), function (_subscription, _item) { + print("Hi after 1337 ms"); +}); + +// count up to 5 with a delay of 100ms between increments +eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, counter) { + print("Counter two:", counter); + if (counter === 5) + subscription.cancel(); + return [counter + 1]; +}, 0); + +// count up to 15 with a delay of 100ms between increments +// and stop the program when the count reaches 15 +eventLoop.subscribe(eventLoop.timer("periodic", 100), function (subscription, _item, event_loop, counter) { + print("Counter one:", counter); + if (counter === 15) + event_loop.stop(); + return [event_loop, counter + 1]; +}, eventLoop, 0); + +eventLoop.run(); diff --git a/applications/system/js_app/examples/apps/Scripts/gpio.js b/applications/system/js_app/examples/apps/Scripts/gpio.js new file mode 100644 index 000000000..f3b4bc121 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/gpio.js @@ -0,0 +1,57 @@ +let eventLoop = require("event_loop"); +let gpio = require("gpio"); + +// initialize pins +let led = gpio.get("pc3"); // same as `gpio.get(7)` +let pot = gpio.get("pc0"); // same as `gpio.get(16)` +let button = gpio.get("pc1"); // same as `gpio.get(15)` +led.init({ direction: "out", outMode: "push_pull" }); +pot.init({ direction: "in", inMode: "analog" }); +button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" }); + +// blink led +print("Commencing blinking (PC3)"); +eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, state) { + led.write(state); + return [led, !state]; +}, led, true); + +// read potentiometer when button is pressed +print("Press the button (PC1)"); +eventLoop.subscribe(button.interrupt(), function (_, _item, pot) { + print("PC0 is at", pot.read_analog(), "mV"); +}, pot); + +// the program will just exit unless this is here +eventLoop.run(); + +// possible pins https://docs.flipper.net/gpio-and-modules#miFsS +// "PA7" aka 2 +// "PA6" aka 3 +// "PA4" aka 4 +// "PB3" aka 5 +// "PB2" aka 6 +// "PC3" aka 7 +// "PA14" aka 10 +// "PA13" aka 12 +// "PB6" aka 13 +// "PB7" aka 14 +// "PC1" aka 15 +// "PC0" aka 16 +// "PB14" aka 17 + +// possible modes +// { direction: "out", outMode: "push_pull" } +// { direction: "out", outMode: "open_drain" } +// { direction: "out", outMode: "push_pull", altFn: true } +// { direction: "out", outMode: "open_drain", altFn: true } +// { direction: "in", inMode: "analog" } +// { direction: "in", inMode: "plain_digital" } +// { direction: "in", inMode: "interrupt", edge: "rising" } +// { direction: "in", inMode: "interrupt", edge: "falling" } +// { direction: "in", inMode: "interrupt", edge: "both" } +// { direction: "in", inMode: "event", edge: "rising" } +// { direction: "in", inMode: "event", edge: "falling" } +// { direction: "in", inMode: "event", edge: "both" } +// all variants support an optional `pull` field which can either be undefined, +// "up" or "down" diff --git a/applications/system/js_app/examples/apps/Scripts/gui.js b/applications/system/js_app/examples/apps/Scripts/gui.js new file mode 100644 index 000000000..dd80b5bc4 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/gui.js @@ -0,0 +1,77 @@ +// import modules +let eventLoop = require("event_loop"); +let gui = require("gui"); +let loadingView = require("gui/loading"); +let submenuView = require("gui/submenu"); +let emptyView = require("gui/empty_screen"); +let textInputView = require("gui/text_input"); +let textBoxView = require("gui/text_box"); +let dialogView = require("gui/dialog"); + +// declare view instances +let views = { + loading: loadingView.make(), + empty: emptyView.make(), + keyboard: textInputView.makeWith({ + header: "Enter your name", + minLength: 0, + maxLength: 32, + }), + helloDialog: dialogView.makeWith({ + center: "Hi Flipper! :)", + }), + longText: textBoxView.makeWith({ + text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.", + }), + demos: submenuView.makeWith({ + header: "Choose a demo", + items: [ + "Hourglass screen", + "Empty screen", + "Text input & Dialog", + "Text box", + "Exit app", + ], + }), +}; + +// demo selector +eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) { + if (index === 0) { + gui.viewDispatcher.switchTo(views.loading); + // the loading view captures all back events, preventing our navigation callback from firing + // switch to the demo chooser after a second + eventLoop.subscribe(eventLoop.timer("oneshot", 1000), function (_sub, _, gui, views) { + gui.viewDispatcher.switchTo(views.demos); + }, gui, views); + } else if (index === 1) { + gui.viewDispatcher.switchTo(views.empty); + } else if (index === 2) { + gui.viewDispatcher.switchTo(views.keyboard); + } else if (index === 3) { + gui.viewDispatcher.switchTo(views.longText); + } else if (index === 4) { + eventLoop.stop(); + } +}, gui, eventLoop, views); + +// say hi after keyboard input +eventLoop.subscribe(views.keyboard.input, function (_sub, name, gui, views) { + views.helloDialog.set("text", "Hi " + name + "! :)"); + gui.viewDispatcher.switchTo(views.helloDialog); +}, gui, views); + +// go back after the greeting dialog +eventLoop.subscribe(views.helloDialog.input, function (_sub, button, gui, views) { + if (button === "center") + gui.viewDispatcher.switchTo(views.demos); +}, gui, views); + +// go to the demo chooser screen when the back key is pressed +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) { + gui.viewDispatcher.switchTo(views.demos); +}, gui, views); + +// run UI +gui.viewDispatcher.switchTo(views.demos); +eventLoop.run(); diff --git a/applications/system/js_app/examples/apps/Scripts/load.js b/applications/system/js_app/examples/apps/Scripts/load.js index dfb110ca5..813619741 100644 --- a/applications/system/js_app/examples/apps/Scripts/load.js +++ b/applications/system/js_app/examples/apps/Scripts/load.js @@ -1,3 +1,3 @@ let math = load("/ext/apps/Scripts/load_api.js"); let result = math.add(5, 10); -print(result); \ No newline at end of file +print(result); diff --git a/applications/system/js_app/examples/apps/Scripts/load_api.js b/applications/system/js_app/examples/apps/Scripts/load_api.js index ad3b26e15..80712c40b 100644 --- a/applications/system/js_app/examples/apps/Scripts/load_api.js +++ b/applications/system/js_app/examples/apps/Scripts/load_api.js @@ -1,3 +1,3 @@ ({ add: function (a, b) { return a + b; }, -}) \ No newline at end of file +}) diff --git a/applications/system/js_app/examples/apps/Scripts/math.js b/applications/system/js_app/examples/apps/Scripts/math.js index c5a0bf18d..63527ea67 100644 --- a/applications/system/js_app/examples/apps/Scripts/math.js +++ b/applications/system/js_app/examples/apps/Scripts/math.js @@ -22,48 +22,3 @@ print("math.sign(-5):", math.sign(-5)); print("math.sin(math.PI/2):", math.sin(math.PI / 2)); print("math.sqrt(25):", math.sqrt(25)); print("math.trunc(5.7):", math.trunc(5.7)); - -// Unit tests. Please add more if you have time and knowledge. -// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16 - -let succeeded = 0; -let failed = 0; - -function test(text, result, expected, epsilon) { - let is_equal = math.is_equal(result, expected, epsilon); - if (is_equal) { - succeeded += 1; - } else { - failed += 1; - print(text, "expected", expected, "got", result); - } -} - -test("math.abs(5)", math.abs(-5), 5, math.EPSILON); -test("math.abs(0.5)", math.abs(-0.5), 0.5, math.EPSILON); -test("math.abs(5)", math.abs(5), 5, math.EPSILON); -test("math.abs(-0.5)", math.abs(0.5), 0.5, math.EPSILON); -test("math.acos(0.5)", math.acos(0.5), 1.0471975511965976, math.EPSILON); -test("math.acosh(2)", math.acosh(2), 1.3169578969248166, math.EPSILON); -test("math.asin(0.5)", math.asin(0.5), 0.5235987755982988, math.EPSILON); -test("math.asinh(2)", math.asinh(2), 1.4436354751788103, math.EPSILON); -test("math.atan(1)", math.atan(1), 0.7853981633974483, math.EPSILON); -test("math.atan2(1, 1)", math.atan2(1, 1), 0.7853981633974483, math.EPSILON); -test("math.atanh(0.5)", math.atanh(0.5), 0.5493061443340549, math.EPSILON); -test("math.cbrt(27)", math.cbrt(27), 3, math.EPSILON); -test("math.ceil(5.3)", math.ceil(5.3), 6, math.EPSILON); -test("math.clz32(1)", math.clz32(1), 31, math.EPSILON); -test("math.floor(5.7)", math.floor(5.7), 5, math.EPSILON); -test("math.max(3, 5)", math.max(3, 5), 5, math.EPSILON); -test("math.min(3, 5)", math.min(3, 5), 3, math.EPSILON); -test("math.pow(2, 3)", math.pow(2, 3), 8, math.EPSILON); -test("math.sign(-5)", math.sign(-5), -1, math.EPSILON); -test("math.sqrt(25)", math.sqrt(25), 5, math.EPSILON); -test("math.trunc(5.7)", math.trunc(5.7), 5, math.EPSILON); -test("math.cos(math.PI)", math.cos(math.PI), -1, math.EPSILON * 18); // Error 3.77475828372553223744e-15 -test("math.exp(1)", math.exp(1), 2.718281828459045, math.EPSILON * 2); // Error 4.44089209850062616169e-16 -test("math.sin(math.PI / 2)", math.sin(math.PI / 2), 1, math.EPSILON * 4.5); // Error 9.99200722162640886381e-16 - -if (failed > 0) { - print("!!!", failed, "Unit tests failed !!!"); -} \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/notify.js b/applications/system/js_app/examples/apps/Scripts/notify.js index 20f60c732..dd471650c 100644 --- a/applications/system/js_app/examples/apps/Scripts/notify.js +++ b/applications/system/js_app/examples/apps/Scripts/notify.js @@ -6,4 +6,4 @@ delay(1000); for (let i = 0; i < 10; i++) { notify.blink("red", "short"); delay(500); -} \ No newline at end of file +} diff --git a/applications/system/js_app/examples/apps/Scripts/submenu.js b/applications/system/js_app/examples/apps/Scripts/submenu.js deleted file mode 100644 index 245551309..000000000 --- a/applications/system/js_app/examples/apps/Scripts/submenu.js +++ /dev/null @@ -1,11 +0,0 @@ -let submenu = require("submenu"); - -submenu.addItem("Item 1", 0); -submenu.addItem("Item 2", 1); -submenu.addItem("Item 3", 2); - -submenu.setHeader("Select an option:"); - -let result = submenu.show(); -// Returns undefined when pressing back -print("Result:", result); diff --git a/applications/system/js_app/examples/apps/Scripts/textbox.js b/applications/system/js_app/examples/apps/Scripts/textbox.js deleted file mode 100644 index 6caf37234..000000000 --- a/applications/system/js_app/examples/apps/Scripts/textbox.js +++ /dev/null @@ -1,30 +0,0 @@ -let textbox = require("textbox"); - -// You should set config before adding text -// Focus (start / end), Font (text / hex) -textbox.setConfig("end", "text"); - -// Can make sure it's cleared before showing, in case of reusing in same script -// (Closing textbox already clears the text, but maybe you added more in a loop for example) -textbox.clearText(); - -// Add default text -textbox.addText("Example dynamic updating textbox\n"); - -// Non-blocking, can keep updating text after, can close in JS or in GUI -textbox.show(); - -let i = 0; -while (textbox.isOpen() && i < 20) { - print("console", i++); - - // Add text to textbox buffer - textbox.addText("textbox " + to_string(i) + "\n"); - - delay(500); -} - -// If not closed by user (instead i < 20 is false above), close forcefully -if (textbox.isOpen()) { - textbox.close(); -} diff --git a/applications/system/js_app/examples/apps/Scripts/uart_echo.js b/applications/system/js_app/examples/apps/Scripts/uart_echo.js index 60d44d078..2a0159b46 100644 --- a/applications/system/js_app/examples/apps/Scripts/uart_echo.js +++ b/applications/system/js_app/examples/apps/Scripts/uart_echo.js @@ -6,6 +6,6 @@ while (1) { if (rx_data !== undefined) { serial.write(rx_data); let data_view = Uint8Array(rx_data); - print("0x" + to_hex_string(data_view[0])); + print("0x" + toString(data_view[0], 16)); } } \ No newline at end of file diff --git a/applications/system/js_app/js_app.c b/applications/system/js_app/js_app.c index d36f3c8db..5de720b43 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 78b6f6ff4..7e7280e9c 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -195,17 +195,11 @@ static void js_require(struct mjs* mjs) { } static void js_global_to_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[] = "-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) { - double num = mjs_get_int(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-FFFFFFFF"; - itoa(num, tmp_str, 16); + itoa(num, tmp_str, base); mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); mjs_return(mjs, ret); } @@ -239,8 +233,7 @@ static int32_t js_thread(void* arg) { mjs_val_t global = mjs_get_global(mjs); 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)); @@ -296,8 +289,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 99f8958f7..891bfa2cd 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -72,8 +72,8 @@ static bool setup_parse_params(struct mjs* mjs, mjs_val_t arg, FuriHalUsbHidConf } mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0); mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0); - mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0); - mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0); + mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0); + mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0); if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) { hid_cfg->vid = mjs_get_int32(mjs, vid_obj); @@ -378,7 +378,8 @@ static void js_badusb_println(struct mjs* mjs) { badusb_print(mjs, true); } -static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) { +static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst)); mjs_val_t badusb_obj = mjs_mk_object(mjs); mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb)); @@ -409,6 +410,7 @@ static const JsModuleDescriptor js_badusb_desc = { "badusb", js_badusb_create, js_badusb_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { 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 34de6d641..000000000 --- a/applications/system/js_app/modules/js_dialog.c +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include "../js_modules.h" -#include - -static bool js_dialog_msg_parse_params(struct mjs* mjs, const char** hdr, const char** msg) { - size_t num_args = mjs_nargs(mjs); - if(num_args != 2) { - return false; - } - mjs_val_t header_obj = mjs_arg(mjs, 0); - mjs_val_t msg_obj = mjs_arg(mjs, 1); - if((!mjs_is_string(header_obj)) || (!mjs_is_string(msg_obj))) { - return false; - } - - size_t arg_len = 0; - *hdr = mjs_get_string(mjs, &header_obj, &arg_len); - if(arg_len == 0) { - *hdr = NULL; - } - - *msg = mjs_get_string(mjs, &msg_obj, &arg_len); - if(arg_len == 0) { - *msg = NULL; - } - - return true; -} - -static void js_dialog_message(struct mjs* mjs) { - const char* dialog_header = NULL; - const char* dialog_msg = NULL; - if(!js_dialog_msg_parse_params(mjs, &dialog_header, &dialog_msg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); - DialogMessage* message = dialog_message_alloc(); - dialog_message_set_buttons(message, NULL, "OK", NULL); - if(dialog_header) { - dialog_message_set_header(message, dialog_header, 64, 3, AlignCenter, AlignTop); - } - if(dialog_msg) { - dialog_message_set_text(message, dialog_msg, 64, 26, AlignCenter, AlignTop); - } - DialogMessageButton result = dialog_message_show(dialogs, message); - dialog_message_free(message); - furi_record_close(RECORD_DIALOGS); - mjs_return(mjs, mjs_mk_boolean(mjs, result == DialogMessageButtonCenter)); -} - -static void js_dialog_custom(struct mjs* mjs) { - DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); - DialogMessage* message = dialog_message_alloc(); - - bool params_correct = false; - - do { - if(mjs_nargs(mjs) != 1) { - break; - } - mjs_val_t params_obj = mjs_arg(mjs, 0); - if(!mjs_is_object(params_obj)) { - break; - } - - mjs_val_t text_obj = mjs_get(mjs, params_obj, "header", ~0); - size_t arg_len = 0; - const char* text_str = mjs_get_string(mjs, &text_obj, &arg_len); - if(arg_len == 0) { - text_str = NULL; - } - if(text_str) { - dialog_message_set_header(message, text_str, 64, 3, AlignCenter, AlignTop); - } - - text_obj = mjs_get(mjs, params_obj, "text", ~0); - text_str = mjs_get_string(mjs, &text_obj, &arg_len); - if(arg_len == 0) { - text_str = NULL; - } - if(text_str) { - dialog_message_set_text(message, text_str, 64, 26, AlignCenter, AlignTop); - } - - mjs_val_t btn_obj[3] = { - mjs_get(mjs, params_obj, "button_left", ~0), - mjs_get(mjs, params_obj, "button_center", ~0), - mjs_get(mjs, params_obj, "button_right", ~0), - }; - const char* btn_text[3] = {NULL, NULL, NULL}; - - for(uint8_t i = 0; i < 3; i++) { - if(!mjs_is_string(btn_obj[i])) { - continue; - } - btn_text[i] = mjs_get_string(mjs, &btn_obj[i], &arg_len); - if(arg_len == 0) { - btn_text[i] = NULL; - } - } - - dialog_message_set_buttons(message, btn_text[0], btn_text[1], btn_text[2]); - - DialogMessageButton result = dialog_message_show(dialogs, message); - mjs_val_t return_obj = MJS_UNDEFINED; - if(result == DialogMessageButtonLeft) { - return_obj = mjs_mk_string(mjs, btn_text[0], ~0, true); - } else if(result == DialogMessageButtonCenter) { - return_obj = mjs_mk_string(mjs, btn_text[1], ~0, true); - } else if(result == DialogMessageButtonRight) { - return_obj = mjs_mk_string(mjs, btn_text[2], ~0, true); - } else { - return_obj = mjs_mk_string(mjs, "", ~0, true); - } - - mjs_return(mjs, return_obj); - params_correct = true; - } while(0); - - dialog_message_free(message); - furi_record_close(RECORD_DIALOGS); - - if(!params_correct) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, ""); - mjs_return(mjs, MJS_UNDEFINED); - } -} - -static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) { - mjs_val_t dialog_obj = mjs_mk_object(mjs); - mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message)); - mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom)); - *object = dialog_obj; - - return (void*)1; -} - -static const JsModuleDescriptor js_dialog_desc = { - "dialog", - js_dialog_create, - NULL, -}; - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = PLUGIN_APP_ID, - .ep_api_version = PLUGIN_API_VERSION, - .entry_point = &js_dialog_desc, -}; - -const FlipperAppPluginDescriptor* js_dialog_ep(void) { - return &plugin_descriptor; -} 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 new file mode 100644 index 000000000..70021968f --- /dev/null +++ b/applications/system/js_app/modules/js_gpio.c @@ -0,0 +1,345 @@ +#include "../js_modules.h" // IWYU pragma: keep +#include "./js_event_loop/js_event_loop.h" +#include +#include +#include +#include +#include + +#define INTERRUPT_QUEUE_LEN 16 + +/** + * Per-pin control structure + */ +typedef struct { + const GpioPin* pin; + bool had_interrupt; + FuriSemaphore* interrupt_semaphore; + JsEventLoopContract* interrupt_contract; + FuriHalAdcChannel adc_channel; + FuriHalAdcHandle* adc_handle; +} JsGpioPinInst; + +ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575 + +/** + * Per-module instance control structure + */ +typedef struct { + FuriEventLoop* loop; + ManagedPinsArray_t managed_pins; + FuriHalAdcHandle* adc_handle; +} JsGpioInst; + +/** + * @brief Interrupt callback + */ +static void js_gpio_int_cb(void* arg) { + furi_assert(arg); + FuriSemaphore* semaphore = arg; + furi_semaphore_release(semaphore); +} + +/** + * @brief Initializes a GPIO pin according to the provided mode object + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let led = gpio.get("pc3"); + * led.init({ direction: "out", outMode: "push_pull" }); + * ``` + */ +static void js_gpio_init(struct mjs* mjs) { + // deconstruct mode object + mjs_val_t mode_arg; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg)); + mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0); + mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0); + mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0); + mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0); + mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0); + + // get strings + const char* direction = mjs_get_string(mjs, &direction_arg, NULL); + const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL); + const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL); + const char* edge = mjs_get_string(mjs, &edge_arg, NULL); + const char* pull = mjs_get_string(mjs, &pull_arg, NULL); + if(!direction) + JS_ERROR_AND_RETURN( + mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object"); + if(!out_mode) out_mode = "open_drain"; + if(!in_mode) in_mode = "plain_digital"; + if(!edge) edge = "rising"; + + // convert strings to mode + GpioMode mode; + if(strcmp(direction, "out") == 0) { + if(strcmp(out_mode, "push_pull") == 0) + mode = GpioModeOutputPushPull; + else if(strcmp(out_mode, "open_drain") == 0) + mode = GpioModeOutputOpenDrain; + else + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode"); + } else if(strcmp(direction, "in") == 0) { + if(strcmp(in_mode, "analog") == 0) { + mode = GpioModeAnalog; + } else if(strcmp(in_mode, "plain_digital") == 0) { + mode = GpioModeInput; + } else if(strcmp(in_mode, "interrupt") == 0) { + if(strcmp(edge, "rising") == 0) + mode = GpioModeInterruptRise; + else if(strcmp(edge, "falling") == 0) + mode = GpioModeInterruptFall; + else if(strcmp(edge, "both") == 0) + mode = GpioModeInterruptRiseFall; + else + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); + } else if(strcmp(in_mode, "event") == 0) { + if(strcmp(edge, "rising") == 0) + mode = GpioModeEventRise; + else if(strcmp(edge, "falling") == 0) + mode = GpioModeEventFall; + else if(strcmp(edge, "both") == 0) + mode = GpioModeEventRiseFall; + else + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode"); + } + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction"); + } + + // convert pull + GpioPull pull_mode; + if(!pull) { + pull_mode = GpioPullNo; + } else if(strcmp(pull, "up") == 0) { + pull_mode = GpioPullUp; + } else if(strcmp(pull, "down") == 0) { + pull_mode = GpioPullDown; + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull"); + } + + // init GPIO + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh); + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief Writes a logic value to a GPIO pin + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let led = gpio.get("pc3"); + * led.init({ direction: "out", outMode: "push_pull" }); + * led.write(true); + * ``` + */ +static void js_gpio_write(struct mjs* mjs) { + bool level; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level)); + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + furi_hal_gpio_write(manager_data->pin, level); + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief Reads a logic value from a GPIO pin + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let button = gpio.get("pc1"); + * button.init({ direction: "in" }); + * if(button.read()) + * print("hi button!!!!!"); + * ``` + */ +static void js_gpio_read(struct mjs* mjs) { + // get level + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + bool value = furi_hal_gpio_read(manager_data->pin); + mjs_return(mjs, mjs_mk_boolean(mjs, value)); +} + +/** + * @brief Returns a event loop contract that can be used to listen to interrupts + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let button = gpio.get("pc1"); + * let event_loop = require("event_loop"); + * button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" }); + * event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); }); + * event_loop.run(); + * ``` + */ +static void js_gpio_interrupt(struct mjs* mjs) { + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + + // interrupt handling + if(!manager_data->had_interrupt) { + furi_hal_gpio_add_int_callback( + manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore); + furi_hal_gpio_enable_int_callback(manager_data->pin); + manager_data->had_interrupt = true; + } + + // make contract + JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract)); + *contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeSemaphore, + .object = manager_data->interrupt_semaphore, + .non_timer = + { + .event = FuriEventLoopEventIn, + }, + }; + manager_data->interrupt_contract = contract; + mjs_return(mjs, mjs_mk_foreign(mjs, contract)); +} + +/** + * @brief Reads a voltage from a GPIO pin in analog mode + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let pot = gpio.get("pc0"); + * pot.init({ direction: "in", inMode: "analog" }); + * print("voltage:" pot.read_analog(), "mV"); + * ``` + */ +static void js_gpio_read_analog(struct mjs* mjs) { + // get mV (ADC is configured for 12 bits and 2048 mV max) + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + uint16_t millivolts = + furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2; + mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts)); +} + +/** + * @brief Returns an object that manages a specified pin. + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let led = gpio.get("pc3"); + * ``` + */ +static void js_gpio_get(struct mjs* mjs) { + mjs_val_t name_arg; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg)); + const char* name_string = mjs_get_string(mjs, &name_arg, NULL); + const GpioPinRecord* pin_record = NULL; + + // parse input argument to a pin pointer + if(name_string) { + pin_record = furi_hal_resources_pin_by_name(name_string); + } else if(mjs_is_number(name_arg)) { + int name_int = mjs_get_int(mjs, name_arg); + pin_record = furi_hal_resources_pin_by_number(name_int); + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number"); + } + + if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device"); + if(pin_record->debug) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging"); + + // return pin manager object + JsGpioInst* module = JS_GET_CONTEXT(mjs); + mjs_val_t manager = mjs_mk_object(mjs); + JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst)); + manager_data->pin = pin_record->pin; + manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); + manager_data->adc_handle = module->adc_handle; + manager_data->adc_channel = pin_record->channel; + mjs_own(mjs, &manager); + mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); + mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); + mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); + mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); + mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog)); + mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); + mjs_return(mjs, manager); + + // remember pin + ManagedPinsArray_push_back(module->managed_pins, manager_data); +} + +static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + JsEventLoop* js_loop = js_module_get(modules, "event_loop"); + if(M_UNLIKELY(!js_loop)) return NULL; + FuriEventLoop* loop = js_event_loop_get_loop(js_loop); + + JsGpioInst* module = malloc(sizeof(JsGpioInst)); + ManagedPinsArray_init(module->managed_pins); + module->adc_handle = furi_hal_adc_acquire(); + module->loop = loop; + furi_hal_adc_configure(module->adc_handle); + + mjs_val_t gpio_obj = mjs_mk_object(mjs); + mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module)); + mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get)); + *object = gpio_obj; + + return (void*)module; +} + +static void js_gpio_destroy(void* inst) { + furi_assert(inst); + JsGpioInst* module = (JsGpioInst*)inst; + + // reset pins + ManagedPinsArray_it_t iterator; + for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator); + ManagedPinsArray_next(iterator)) { + JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator); + if(manager_data->had_interrupt) { + furi_hal_gpio_disable_int_callback(manager_data->pin); + furi_hal_gpio_remove_int_callback(manager_data->pin); + } + furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore); + furi_semaphore_free(manager_data->interrupt_semaphore); + free(manager_data->interrupt_contract); + free(manager_data); + } + + // free buffers + furi_hal_adc_release(module->adc_handle); + ManagedPinsArray_clear(module->managed_pins); + free(module); +} + +static const JsModuleDescriptor js_gpio_desc = { + "gpio", + js_gpio_create, + js_gpio_destroy, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_gpio_desc, +}; + +const FlipperAppPluginDescriptor* js_gpio_ep(void) { + return &plugin_descriptor; +} 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_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 234eefb43..7aea92783 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -573,7 +573,8 @@ static void js_serial_expect(struct mjs* mjs) { } } -static void* js_serial_create(struct mjs* mjs, mjs_val_t* object) { +static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); JsSerialInst* js_serial = malloc(sizeof(JsSerialInst)); js_serial->mjs = mjs; mjs_val_t serial_obj = mjs_mk_object(mjs); @@ -606,6 +607,7 @@ static const JsModuleDescriptor js_serial_desc = { "serial", js_serial_create, js_serial_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_storage.c b/applications/system/js_app/modules/js_storage.c new file mode 100644 index 000000000..1d4053a5f --- /dev/null +++ b/applications/system/js_app/modules/js_storage.c @@ -0,0 +1,383 @@ +#include "../js_modules.h" // IWYU pragma: keep +#include + +// ---=== file ops ===--- + +static void js_storage_file_close(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file))); +} + +static void js_storage_file_is_open(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file))); +} + +static void js_storage_file_read(struct mjs* mjs) { + enum { + ReadModeAscii, + ReadModeBinary, + } read_mode; + JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary}); + int32_t length; + JS_FETCH_ARGS_OR_RETURN( + mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length)); + File* file = JS_GET_CONTEXT(mjs); + char buffer[length]; + size_t actually_read = storage_file_read(file, buffer, length); + if(read_mode == ReadModeAscii) { + mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true)); + } else if(read_mode == ReadModeBinary) { + mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read)); + } +} + +static void js_storage_file_write(struct mjs* mjs) { + mjs_val_t data; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data)); + const void* buf; + size_t len; + if(mjs_is_string(data)) { + buf = mjs_get_string(mjs, &data, &len); + } else if(mjs_is_array_buf(data)) { + buf = mjs_array_buf_get_ptr(mjs, data, &len); + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: expected string or ArrayBuffer"); + } + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len))); +} + +static void js_storage_file_seek_relative(struct mjs* mjs) { + int32_t offset; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false))); +} + +static void js_storage_file_seek_absolute(struct mjs* mjs) { + int32_t offset; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true))); +} + +static void js_storage_file_tell(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file))); +} + +static void js_storage_file_truncate(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file))); +} + +static void js_storage_file_size(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file))); +} + +static void js_storage_file_eof(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file))); +} + +static void js_storage_file_copy_to(struct mjs* mjs) { + File* source = JS_GET_CONTEXT(mjs); + mjs_val_t dest_obj; + int32_t bytes; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes)); + File* destination = JS_GET_INST(mjs, dest_obj); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes))); +} + +// ---=== top-level file ops ===--- + +// common destructor for file and dir objects +static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) { + File* file = JS_GET_INST(mjs, obj); + storage_file_free(file); +} + +static void js_storage_open_file(struct mjs* mjs) { + const char* path; + FS_AccessMode access_mode; + FS_OpenMode open_mode; + JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE}); + JS_ENUM_MAP( + open_mode, + {"open_existing", FSOM_OPEN_EXISTING}, + {"open_always", FSOM_OPEN_ALWAYS}, + {"open_append", FSOM_OPEN_APPEND}, + {"create_new", FSOM_CREATE_NEW}, + {"create_always", FSOM_CREATE_ALWAYS}); + JS_FETCH_ARGS_OR_RETURN( + mjs, + JS_EXACTLY, + JS_ARG_STR(&path), + JS_ARG_ENUM(access_mode, "AccessMode"), + JS_ARG_ENUM(open_mode, "OpenMode")); + + Storage* storage = JS_GET_CONTEXT(mjs); + File* file = storage_file_alloc(storage); + if(!storage_file_open(file, path, access_mode, open_mode)) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + mjs_val_t file_obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, file_obj) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, file)); + JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_file_destructor)); + JS_FIELD("close", MJS_MK_FN(js_storage_file_close)); + JS_FIELD("isOpen", MJS_MK_FN(js_storage_file_is_open)); + JS_FIELD("read", MJS_MK_FN(js_storage_file_read)); + JS_FIELD("write", MJS_MK_FN(js_storage_file_write)); + JS_FIELD("seekRelative", MJS_MK_FN(js_storage_file_seek_relative)); + JS_FIELD("seekAbsolute", MJS_MK_FN(js_storage_file_seek_absolute)); + JS_FIELD("tell", MJS_MK_FN(js_storage_file_tell)); + JS_FIELD("truncate", MJS_MK_FN(js_storage_file_truncate)); + JS_FIELD("size", MJS_MK_FN(js_storage_file_size)); + JS_FIELD("eof", MJS_MK_FN(js_storage_file_eof)); + JS_FIELD("copyTo", MJS_MK_FN(js_storage_file_copy_to)); + } + mjs_return(mjs, file_obj); +} + +static void js_storage_file_exists(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path))); +} + +// ---=== dir ops ===--- + +static void js_storage_read_directory(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + + Storage* storage = JS_GET_CONTEXT(mjs); + File* dir = storage_file_alloc(storage); + if(!storage_dir_open(dir, path)) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + FileInfo file_info; + char name[128]; + FuriString* file_path = furi_string_alloc_set_str(path); + size_t path_size = furi_string_size(file_path); + uint32_t timestamp; + + mjs_val_t ret = mjs_mk_array(mjs); + while(storage_dir_read(dir, &file_info, name, sizeof(name))) { + furi_string_left(file_path, path_size); + path_append(file_path, name); + furi_check( + storage_common_timestamp(storage, furi_string_get_cstr(file_path), ×tamp) == + FSE_OK); + mjs_val_t obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, obj) { + JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true)); + JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info))); + JS_FIELD("size", mjs_mk_number(mjs, file_info.size)); + JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp)); + } + mjs_array_push(mjs, ret, obj); + } + + storage_file_free(dir); + furi_string_free(file_path); + mjs_return(mjs, ret); +} + +static void js_storage_directory_exists(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path))); +} + +static void js_storage_make_directory(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path))); +} + +// ---=== common ops ===--- + +static void js_storage_file_or_dir_exists(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path))); +} + +static void js_storage_stat(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + FileInfo file_info; + uint32_t timestamp; + if((storage_common_stat(storage, path, &file_info) | + storage_common_timestamp(storage, path, ×tamp)) != FSE_OK) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + mjs_val_t ret = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, ret) { + JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1)); + JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info))); + JS_FIELD("size", mjs_mk_number(mjs, file_info.size)); + JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp)); + } + mjs_return(mjs, ret); +} + +static void js_storage_remove(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path))); +} + +static void js_storage_rmrf(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path))); +} + +static void js_storage_rename(struct mjs* mjs) { + const char *old, *new; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new)); + Storage* storage = JS_GET_CONTEXT(mjs); + FS_Error status = storage_common_rename(storage, old, new); + mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK)); +} + +static void js_storage_copy(struct mjs* mjs) { + const char *source, *dest; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest)); + Storage* storage = JS_GET_CONTEXT(mjs); + FS_Error status = storage_common_copy(storage, source, dest); + mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST)); +} + +static void js_storage_fs_info(struct mjs* mjs) { + const char* fs; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs)); + Storage* storage = JS_GET_CONTEXT(mjs); + uint64_t total_space, free_space; + if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + mjs_val_t ret = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, ret) { + JS_FIELD("totalSpace", mjs_mk_number(mjs, total_space)); + JS_FIELD("freeSpace", mjs_mk_number(mjs, free_space)); + } + mjs_return(mjs, ret); +} + +static void js_storage_next_available_filename(struct mjs* mjs) { + const char *dir_path, *file_name, *file_ext; + int32_t max_len; + JS_FETCH_ARGS_OR_RETURN( + mjs, + JS_EXACTLY, + JS_ARG_STR(&dir_path), + JS_ARG_STR(&file_name), + JS_ARG_STR(&file_ext), + JS_ARG_INT32(&max_len)); + Storage* storage = JS_GET_CONTEXT(mjs); + FuriString* next_name = furi_string_alloc(); + storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len); + mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(next_name), ~0, true)); + furi_string_free(next_name); +} + +// ---=== path ops ===--- + +static void js_storage_are_paths_equal(struct mjs* mjs) { + const char *path1, *path2; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path1), JS_ARG_STR(&path2)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_equivalent_path(storage, path1, path2))); +} + +static void js_storage_is_subpath_of(struct mjs* mjs) { + const char *parent, *child; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&parent), JS_ARG_STR(&child)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_is_subdir(storage, parent, child))); +} + +// ---=== module ctor & dtor ===--- + +static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + Storage* storage = furi_record_open(RECORD_STORAGE); + UNUSED(storage); + *object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, *object) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, storage)); + + // top-level file ops + JS_FIELD("openFile", MJS_MK_FN(js_storage_open_file)); + JS_FIELD("fileExists", MJS_MK_FN(js_storage_file_exists)); + + // dir ops + JS_FIELD("readDirectory", MJS_MK_FN(js_storage_read_directory)); + JS_FIELD("directoryExists", MJS_MK_FN(js_storage_directory_exists)); + JS_FIELD("makeDirectory", MJS_MK_FN(js_storage_make_directory)); + + // common ops + JS_FIELD("fileOrDirExists", MJS_MK_FN(js_storage_file_or_dir_exists)); + JS_FIELD("stat", MJS_MK_FN(js_storage_stat)); + JS_FIELD("remove", MJS_MK_FN(js_storage_remove)); + JS_FIELD("rmrf", MJS_MK_FN(js_storage_rmrf)); + JS_FIELD("rename", MJS_MK_FN(js_storage_rename)); + JS_FIELD("copy", MJS_MK_FN(js_storage_copy)); + JS_FIELD("fsInfo", MJS_MK_FN(js_storage_fs_info)); + JS_FIELD("nextAvailableFilename", MJS_MK_FN(js_storage_next_available_filename)); + + // path ops + JS_FIELD("arePathsEqual", MJS_MK_FN(js_storage_are_paths_equal)); + JS_FIELD("isSubpathOf", MJS_MK_FN(js_storage_is_subpath_of)); + } + return NULL; +} + +static void js_storage_destroy(void* data) { + UNUSED(data); + furi_record_close(RECORD_STORAGE); +} + +// ---=== boilerplate ===--- + +static const JsModuleDescriptor js_storage_desc = { + "storage", + js_storage_create, + js_storage_destroy, + NULL, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_storage_desc, +}; + +const FlipperAppPluginDescriptor* js_storage_ep(void) { + return &plugin_descriptor; +} 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; diff --git a/documentation/doxygen/Doxyfile.cfg b/documentation/doxygen/Doxyfile.cfg index 90f36415f..e01631749 100644 --- a/documentation/doxygen/Doxyfile.cfg +++ b/documentation/doxygen/Doxyfile.cfg @@ -1108,7 +1108,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = +IMAGE_PATH = $(DOXY_SRC_ROOT)/documentation/images # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program diff --git a/documentation/doxygen/js.dox b/documentation/doxygen/js.dox index 33ac078d9..f5c609dd1 100644 --- a/documentation/doxygen/js.dox +++ b/documentation/doxygen/js.dox @@ -11,12 +11,20 @@ This page contains some information on the Flipper Zero scripting engine, which JS modules use the Flipper app plugin system. Each module is compiled into a `.fal` library file and is located on a microSD card. Here is a list of implemented modules: -- @subpage js_badusb — BadUSB module -- @subpage js_serial — Serial module -- @subpage js_math — Math module -- @subpage js_dialog — Dialog module -- @subpage js_submenu — Submenu module -- @subpage js_textbox — Textbox module -- @subpage js_notification — Notifications module +- @subpage js_badusb - BadUSB module +- @subpage js_serial - Serial module +- @subpage js_math - Math module +- @subpage js_notification - Notifications module +- @subpage js_event_loop - Event Loop module +- @subpage js_gpio - GPIO module +- @subpage js_gui - GUI module and its submodules: + - @subpage js_gui__submenu - Submenu view + - @subpage js_gui__loading - Hourglass (Loading) view + - @subpage js_gui__empty_screen - Empty view + - @subpage js_gui__text_input - Keyboard-like text input + - @subpage js_gui__text_box - Simple multiline text box + - @subpage js_gui__dialog - Dialog with up to 3 options + +All modules have corresponding TypeScript declaration files, so you can set up your IDE to show suggestions when writing JS scripts. */ diff --git a/documentation/images/dialog.png b/documentation/images/dialog.png new file mode 100644 index 000000000..008ae9ce5 Binary files /dev/null and b/documentation/images/dialog.png differ diff --git a/documentation/images/empty.png b/documentation/images/empty.png new file mode 100644 index 000000000..844f45093 Binary files /dev/null and b/documentation/images/empty.png differ diff --git a/documentation/images/loading.png b/documentation/images/loading.png new file mode 100644 index 000000000..f35966f66 Binary files /dev/null and b/documentation/images/loading.png differ diff --git a/documentation/images/submenu.png b/documentation/images/submenu.png new file mode 100644 index 000000000..1cb64e974 Binary files /dev/null and b/documentation/images/submenu.png differ diff --git a/documentation/images/text_box.png b/documentation/images/text_box.png new file mode 100644 index 000000000..5dbec7c77 Binary files /dev/null and b/documentation/images/text_box.png differ diff --git a/documentation/images/text_input.png b/documentation/images/text_input.png new file mode 100644 index 000000000..8720cc79d Binary files /dev/null and b/documentation/images/text_input.png differ diff --git a/documentation/js/js_builtin.md b/documentation/js/js_builtin.md index 3d113807b..9c59b9822 100644 --- a/documentation/js/js_builtin.md +++ b/documentation/js/js_builtin.md @@ -41,16 +41,10 @@ print("string1", "string2", 123); Same as `print`, but output to serial console only, with corresponding log level. ## to_string -Convert a number to string. +Convert a number to string with an optional base. ### Examples: ```js -to_string(123) -``` -## to_hex_string -Convert a number to string(hex format). - -### Examples: -```js -to_hex_string(0xFF) +to_string(123) // "123" +to_string(123, 16) // "0x7b" ``` diff --git a/documentation/js/js_dialog.md b/documentation/js/js_dialog.md deleted file mode 100644 index eb027e6a7..000000000 --- a/documentation/js/js_dialog.md +++ /dev/null @@ -1,49 +0,0 @@ -# js_dialog {#js_dialog} - -# Dialog module -```js -let dialog = require("dialog"); -``` -# Methods - -## message -Show a simple message dialog with header, text and "OK" button. - -### Parameters -- Dialog header text -- Dialog text - -### Returns -true if central button was pressed, false if the dialog was closed by back key press - -### Examples: -```js -dialog.message("Dialog demo", "Press OK to start"); -``` - -## custom -More complex dialog with configurable buttons - -### Parameters -Configuration object with the following fields: -- header: Dialog header text -- text: Dialog text -- button_left: (optional) left button name -- button_right: (optional) right button name -- button_center: (optional) central button name - -### Returns -Name of pressed button or empty string if the dialog was closed by back key press - -### Examples: -```js -let dialog_params = ({ - header: "Dialog header", - text: "Dialog text", - button_left: "Left", - button_right: "Right", - button_center: "OK" -}); - -dialog.custom(dialog_params); -``` diff --git a/documentation/js/js_event_loop.md b/documentation/js/js_event_loop.md new file mode 100644 index 000000000..9519478c0 --- /dev/null +++ b/documentation/js/js_event_loop.md @@ -0,0 +1,144 @@ +# js_event_loop {#js_event_loop} + +# Event Loop module +```js +let eventLoop = require("event_loop"); +``` + +The event loop is central to event-based programming in many frameworks, and our +JS subsystem is no exception. It is a good idea to familiarize yourself with the +event loop first before using any of the advanced modules (e.g. GPIO and GUI). + +## Conceptualizing the event loop +If you ever wrote JavaScript before, you have definitely seen callbacks. It's +when a function accepts another function (usually an anonymous one) as one of +the arguments, which it will call later on, e.g. when an event happens or when +data becomes ready: +```js +setTimeout(function() { console.log("Hello, World!") }, 1000); +``` + +Many JavaScript engines employ a queue that the runtime fetches events from as +they occur, subsequently calling the corresponding callbacks. This is done in a +long-running loop, hence the name "event loop". Here's the pseudocode for a +typical event loop: +```js +while(loop_is_running()) { + if(event_available_in_queue()) { + let event = fetch_event_from_queue(); + let callback = get_callback_associated_with(event); + if(callback) + callback(get_extra_data_for(event)); + } else { + // avoid wasting CPU time + sleep_until_any_event_becomes_available(); + } +} +``` + +Most JS runtimes enclose the event loop within themselves, so that most JS +programmers does not even need to be aware of its existence. This is not the +case with our JS subsystem. + +# Example +This is how one would write something similar to the `setTimeout` example above: +```js +// import module +let eventLoop = require("event_loop"); + +// create an event source that will fire once 1 second after it has been created +let timer = eventLoop.timer("oneshot", 1000); + +// subscribe a callback to the event source +eventLoop.subscribe(timer, function(_subscription, _item, eventLoop) { + print("Hello, World!"); + eventLoop.stop(); +}, eventLoop); // notice this extra argument. we'll come back to this later + +// run the loop until it is stopped +eventLoop.run(); + +// the previous line will only finish executing once `.stop()` is called, hence +// the following line will execute only after "Hello, World!" is printed +print("Stopped"); +``` + +I promised you that we'll come back to the extra argument after the callback +function. Our JavaScript engine does not support closures (anonymous functions +that access values outside of their arguments), so we ask `subscribe` to pass an +outside value (namely, `eventLoop`) as an argument to the callback so that we +can access it. We can modify this extra state: +```js +// this timer will fire every second +let timer = eventLoop.timer("periodic", 1000); +eventLoop.subscribe(timer, function(_subscription, _item, counter, eventLoop) { + print("Counter is at:", counter); + if(counter === 10) + eventLoop.stop(); + // modify the extra arguments that will be passed to us the next time + return [counter + 1, eventLoop]; +}, 0, eventLoop); +``` + +Because we have two extra arguments, if we return anything other than an array +of length 2, the arguments will be kept as-is for the next call. + +The first two arguments that get passed to our callback are: + - The subscription manager that lets us `.cancel()` our subscription + - The event item, used for events that have extra data. Timer events do not, + they just produce `undefined`. + +# API reference +## `run` +Runs the event loop until it is stopped with `stop`. + +## `subscribe` +Subscribes a function to an event. + +### Parameters + - `contract`: an event source identifier + - `callback`: the function to call when the event happens + - extra arguments: will be passed as extra arguments to the callback + +The callback will be called with at least two arguments, plus however many were +passed as extra arguments to `subscribe`. The first argument is the subscription +manager (the same one that `subscribe` itself returns). The second argument is +the event item for events that produce extra data; the ones that don't set this +to `undefined`. The callback may return an array of the same length as the count +of the extra arguments to modify them for the next time that the event handler +is called. Any other returns values are discarded. + +### Returns +A `SubscriptionManager` object: + - `SubscriptionManager.cancel()`: unsubscribes the callback from the event + +### Warning +Each event source may only have one callback associated with it. + +## `stop` +Stops the event loop. + +## `timer` +Produces an event source that fires with a constant interval either once or +indefinitely. + +### Parameters + - `mode`: either `"oneshot"` or `"periodic"` + - `interval`: the timeout (for `"oneshot"`) timers or the period (for + `"periodic"` timers) + +### Returns +A `Contract` object, as expected by `subscribe`'s first parameter. + +## `queue` +Produces a queue that can be used to exchange messages. + +### Parameters + - `length`: the maximum number of items that the queue may contain + +### Returns +A `Queue` object: + - `Queue.send(message)`: + - `message`: a value of any type that will be placed at the end of the queue + - `input`: a `Contract` (event source) that pops items from the front of the + queue diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md new file mode 100644 index 000000000..9791fb4eb --- /dev/null +++ b/documentation/js/js_gpio.md @@ -0,0 +1,77 @@ +# js_gpio {#js_gpio} + +# GPIO module +```js +let eventLoop = require("event_loop"); +let gpio = require("gpio"); +``` + +This module depends on the `event_loop` module, so it _must_ only be imported +after `event_loop` is imported. + +# Example +```js +let eventLoop = require("event_loop"); +let gpio = require("gpio"); + +let led = gpio.get("pc3"); +led.init({ direction: "out", outMode: "push_pull" }); + +led.write(true); +delay(1000); +led.write(false); +delay(1000); +``` + +# API reference +## `get` +Gets a `Pin` object that can be used to manage a pin. + +### Parameters + - `pin`: pin identifier (examples: `"pc3"`, `7`, `"pa6"`, `3`) + +### Returns +A `Pin` object + +## `Pin` object +### `Pin.init()` +Configures a pin + +#### Parameters + - `mode`: `Mode` object: + - `direction` (required): either `"in"` or `"out"` + - `outMode` (required for `direction: "out"`): either `"open_drain"` or + `"push_pull"` + - `inMode` (required for `direction: "in"`): either `"analog"`, + `"plain_digital"`, `"interrupt"` or `"event"` + - `edge` (required for `inMode: "interrupt"` or `"event"`): either + `"rising"`, `"falling"` or `"both"` + - `pull` (optional): either `"up"`, `"down"` or unset + +### `Pin.write()` +Writes a digital value to a pin configured with `direction: "out"` + +#### Parameters + - `value`: boolean logic level to write + +### `Pin.read()` +Reads a digital value from a pin configured with `direction: "in"` and any +`inMode` except `"analog"` + +#### Returns +Boolean logic level + +### `Pin.read_analog()` +Reads an analog voltage level in millivolts from a pin configured with +`direction: "in"` and `inMode: "analog"` + +#### Returns +Voltage on pin in millivolts + +### `Pin.interrupt()` +Attaches an interrupt to a pin configured with `direction: "in"` and +`inMode: "interrupt"` or `"event"` + +#### Returns +An event loop `Contract` object that identifies the interrupt event source. The +event does not produce any extra data. diff --git a/documentation/js/js_gui.md b/documentation/js/js_gui.md new file mode 100644 index 000000000..4d2d2497a --- /dev/null +++ b/documentation/js/js_gui.md @@ -0,0 +1,161 @@ +# js_gui {#js_gui} + +# GUI module +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +``` + +This module depends on the `event_loop` module, so it _must_ only be imported +after `event_loop` is imported. + +## Conceptualizing GUI +### Event loop +It is highly recommended to familiarize yourself with the event loop first +before doing GUI-related things. + +### Canvas +The canvas is just a drawing area with no abstractions over it. Drawing on the +canvas directly (i.e. not through a viewport) is useful in case you want to +implement a custom design element, but this is rather uncommon. + +### Viewport +A viewport is a window into a rectangular portion of the canvas. Applications +always access the canvas through a viewport. + +### View +In Flipper's terminology, a "View" is a fullscreen design element that assumes +control over the entire viewport and all input events. Different types of views +are available (not all of which are unfortunately currently implemented in JS): +| View | Has JS adapter? | +|----------------------|------------------| +| `button_menu` | ❌ | +| `button_panel` | ❌ | +| `byte_input` | ❌ | +| `dialog_ex` | ✅ (as `dialog`) | +| `empty_screen` | ✅ | +| `file_browser` | ❌ | +| `loading` | ✅ | +| `menu` | ❌ | +| `number_input` | ❌ | +| `popup` | ❌ | +| `submenu` | ✅ | +| `text_box` | ✅ | +| `text_input` | ✅ | +| `variable_item_list` | ❌ | +| `widget` | ❌ | + +In JS, each view has its own set of properties (or just "props"). The programmer +can manipulate these properties in two ways: + - Instantiate a `View` using the `makeWith(props)` method, passing an object + with the initial properties + - Call `set(name, value)` to modify a property of an existing `View` + +### View Dispatcher +The view dispatcher holds references to all the views that an application needs +and switches between them as the application makes requests to do so. + +### Scene Manager +The scene manager is an optional add-on to the view dispatcher that makes +managing applications with complex navigation flows easier. It is currently +inaccessible from JS. + +### Approaches +In total, there are three different approaches that you may take when writing +a GUI application: +| Approach | Use cases | Available from JS | +|----------------|------------------------------------------------------------------------------|-------------------| +| ViewPort only | Accessing the graphics API directly, without any of the nice UI abstractions | ❌ | +| ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ | +| SceneManager | Additional navigation flow management for complex applications | ❌ | + +# Example +An example with three different views using the ViewDispatcher approach: +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let loadingView = require("gui/loading"); +let submenuView = require("gui/submenu"); +let emptyView = require("gui/empty_screen"); + +// Common pattern: declare all the views in an object. This is absolutely not +// required, but adds clarity to the script. +let views = { + // the view dispatcher auto-✨magically✨ remembers views as they are created + loading: loadingView.make(), + empty: emptyView.make(), + demos: submenuView.makeWith({ + items: [ + "Hourglass screen", + "Empty screen", + "Exit app", + ], + }), +}; + +// go to different screens depending on what was selected +eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) { + if (index === 0) { + gui.viewDispatcher.switchTo(views.loading); + } else if (index === 1) { + gui.viewDispatcher.switchTo(views.empty); + } else if (index === 2) { + eventLoop.stop(); + } +}, gui, eventLoop, views); + +// go to the demo chooser screen when the back key is pressed +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) { + gui.viewDispatcher.switchTo(views.demos); +}, gui, views); + +// run UI +gui.viewDispatcher.switchTo(views.demos); +eventLoop.run(); +``` + +# API reference +## `viewDispatcher` +The `viewDispatcher` constant holds the `ViewDispatcher` singleton. + +### `viewDispatcher.switchTo(view)` +Switches to a view, giving it control over the display and input + +#### Parameters + - `view`: the `View` to switch to + +### `viewDispatcher.sendTo(direction)` +Sends the viewport that the dispatcher manages to the front of the stackup +(effectively making it visible), or to the back (effectively making it +invisible) + +#### Parameters + - `direction`: either `"front"` or `"back"` + +### `viewDispatcher.sendCustom(event)` +Sends a custom number to the `custom` event handler + +#### Parameters + - `event`: number to send + +### `viewDispatcher.custom` +An event loop `Contract` object that identifies the custom event source, +triggered by `ViewDispatcher.sendCustom(event)` + +### `viewDispatcher.navigation` +An event loop `Contract` object that identifies the navigation event source, +triggered when the back key is pressed + +## `ViewFactory` +When you import a module implementing a view, a `ViewFactory` is instantiated. +For example, in the example above, `loadingView`, `submenuView` and `emptyView` +are view factories. + +### `ViewFactory.make()` +Creates an instance of a `View` + +### `ViewFactory.make(props)` +Creates an instance of a `View` and assigns initial properties from `props` + +#### Parameters + - `props`: simple key-value object, e.g. `{ header: "Header" }` diff --git a/documentation/js/js_gui__dialog.md b/documentation/js/js_gui__dialog.md new file mode 100644 index 000000000..445e71128 --- /dev/null +++ b/documentation/js/js_gui__dialog.md @@ -0,0 +1,53 @@ +# js_gui__dialog {#js_gui__dialog} + +# Dialog GUI view +Displays a dialog with up to three options. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let dialogView = require("gui/dialog"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the `gui.js` example script. + +# View props +## `header` +Text that appears in bold at the top of the screen + +Type: `string` + +## `text` +Text that appears in the middle of the screen + +Type: `string` + +## `left` +Text for the left button. If unset, the left button does not show up. + +Type: `string` + +## `center` +Text for the center button. If unset, the center button does not show up. + +Type: `string` + +## `right` +Text for the right button. If unset, the right button does not show up. + +Type: `string` + +# View events +## `input` +Fires when the user presses on either of the three possible buttons. The item +contains one of the strings `"left"`, `"center"` or `"right"` depending on the +button. + +Item type: `string` diff --git a/documentation/js/js_gui__empty_screen.md b/documentation/js/js_gui__empty_screen.md new file mode 100644 index 000000000..f9fd12553 --- /dev/null +++ b/documentation/js/js_gui__empty_screen.md @@ -0,0 +1,22 @@ +# js_gui__empty_screen {#js_gui__empty_screen} + +# Empty Screen GUI View +Displays nothing. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let emptyView = require("gui/empty_screen"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the GUI example. + +# View props +This view does not have any props. diff --git a/documentation/js/js_gui__loading.md b/documentation/js/js_gui__loading.md new file mode 100644 index 000000000..52f1cea49 --- /dev/null +++ b/documentation/js/js_gui__loading.md @@ -0,0 +1,23 @@ +# js_gui__loading {#js_gui__loading} + +# Loading GUI View +Displays an animated hourglass icon. Suppresses all `navigation` events, making +it impossible for the user to exit the view by pressing the back key. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let loadingView = require("gui/loading"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the GUI example. + +# View props +This view does not have any props. diff --git a/documentation/js/js_gui__submenu.md b/documentation/js/js_gui__submenu.md new file mode 100644 index 000000000..28c1e65af --- /dev/null +++ b/documentation/js/js_gui__submenu.md @@ -0,0 +1,37 @@ +# js_gui__submenu {#js_gui__submenu} + +# Submenu GUI view +Displays a scrollable list of clickable textual entries. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let submenuView = require("gui/submenu"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the GUI example. + +# View props +## `header` +Single line of text that appears above the list + +Type: `string` + +## `items` +The list of options + +Type: `string[]` + +# View events +## `chosen` +Fires when an entry has been chosen by the user. The item contains the index of +the entry. + +Item type: `number` diff --git a/documentation/js/js_gui__text_box.md b/documentation/js/js_gui__text_box.md new file mode 100644 index 000000000..bdad8d8b3 --- /dev/null +++ b/documentation/js/js_gui__text_box.md @@ -0,0 +1,25 @@ +# js_gui__text_box {#js_gui__text_box} + +# Text box GUI view +Displays a scrollable read-only text field. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let textBoxView = require("gui/text_box"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the `gui.js` example script. + +# View props +## `text` +Text to show in the text box. + +Type: `string` diff --git a/documentation/js/js_gui__text_input.md b/documentation/js/js_gui__text_input.md new file mode 100644 index 000000000..030579e2e --- /dev/null +++ b/documentation/js/js_gui__text_input.md @@ -0,0 +1,44 @@ +# js_gui__text_input {#js_gui__text_input} + +# Text input GUI view +Displays a keyboard. + +Sample screenshot of the view + +```js +let eventLoop = require("event_loop"); +let gui = require("gui"); +let textInputView = require("gui/text_input"); +``` + +This module depends on the `gui` module, which in turn depends on the +`event_loop` module, so they _must_ be imported in this order. It is also +recommended to conceptualize these modules first before using this one. + +# Example +For an example refer to the `gui.js` example script. + +# View props +## `minLength` +Smallest allowed text length + +Type: `number` + +## `maxLength` +Biggest allowed text length + +Type: `number` + +Default: `32` + +## `header` +Single line of text that appears above the keyboard + +Type: `string` + +# View events +## `input` +Fires when the user selects the "save" button and the text matches the length +constrained by `minLength` and `maxLength`. + +Item type: `string` diff --git a/documentation/js/js_submenu.md b/documentation/js/js_submenu.md deleted file mode 100644 index 580a43bd5..000000000 --- a/documentation/js/js_submenu.md +++ /dev/null @@ -1,48 +0,0 @@ -# js_submenu {#js_submenu} - -# Submenu module -```js -let submenu = require("submenu"); -``` -# Methods - -## setHeader -Set the submenu header text. - -### Parameters -- header (string): The submenu header text - -### Example -```js -submenu.setHeader("Select an option:"); -``` - -## addItem -Add a new submenu item. - -### Parameters -- label (string): The submenu item label text -- id (number): The submenu item ID, must be a Uint32 number - -### Example -```js -submenu.addItem("Option 1", 1); -submenu.addItem("Option 2", 2); -submenu.addItem("Option 3", 3); -``` - -## show -Show a submenu that was previously configured using `setHeader()` and `addItem()` methods. - -### Returns -The ID of the submenu item that was selected, or `undefined` if the BACK button was pressed. - -### Example -```js -let selected = submenu.show(); -if (selected === undefined) { - // if BACK button was pressed -} else if (selected === 1) { - // if item with ID 1 was selected -} -``` diff --git a/documentation/js/js_textbox.md b/documentation/js/js_textbox.md deleted file mode 100644 index 61652df1a..000000000 --- a/documentation/js/js_textbox.md +++ /dev/null @@ -1,69 +0,0 @@ -# js_textbox {#js_textbox} - -# Textbox module -```js -let textbox = require("textbox"); -``` -# Methods - -## setConfig -Set focus and font for the textbox. - -### Parameters -- focus: "start" to focus on the beginning of the text, or "end" to focus on the end of the text -- font: "text" to use the default proportional font, or "hex" to use a monospaced font, which is convenient for aligned array output in HEX - -### Example -```js -textbox.setConfig("start", "text"); -textbox.addText("Hello world"); -textbox.show(); -``` - -## addText -Add text to the end of the textbox. - -### Parameters -- text (string): The text to add to the end of the textbox - -### Example -```js -textbox.addText("New text 1\nNew text 2"); -``` - -## clearText -Clear the textbox. - -### Example -```js -textbox.clearText(); -``` - -## isOpen -Return true if the textbox is open. - -### Returns -True if the textbox is open, false otherwise. - -### Example -```js -let isOpen = textbox.isOpen(); -``` - -## show -Show the textbox. You can add text to it using the `addText()` method before or after calling the `show()` method. - -### Example -```js -textbox.show(); -``` - -## close -Close the textbox. - -### Example -```js -if (textbox.isOpen()) { - textbox.close(); -} -``` diff --git a/fbt_options.py b/fbt_options.py index e30f7fc2d..a7705335a 100644 --- a/fbt_options.py +++ b/fbt_options.py @@ -75,6 +75,7 @@ FIRMWARE_APPS = { "updater_app", "radio_device_cc1101_ext", "unit_tests", + "js_app", ], } diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index f4f008a71..b622aa7a1 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -418,6 +418,18 @@ void furi_event_loop_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* o FURI_CRITICAL_EXIT(); } +bool furi_event_loop_is_subscribed(FuriEventLoop* instance, FuriEventLoopObject* object) { + furi_check(instance); + furi_check(instance->thread_id == furi_thread_get_current_id()); + FURI_CRITICAL_ENTER(); + + FuriEventLoopItem* const* item = FuriEventLoopTree_cget(instance->tree, object); + bool result = !!item; + + FURI_CRITICAL_EXIT(); + return result; +} + /* * Private Event Loop Item functions */ diff --git a/furi/core/event_loop.h b/furi/core/event_loop.h index af5987101..6c5ba432c 100644 --- a/furi/core/event_loop.h +++ b/furi/core/event_loop.h @@ -289,6 +289,23 @@ void furi_event_loop_subscribe_mutex( */ void furi_event_loop_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* object); +/** + * @brief Checks if the loop is subscribed to an object of any kind + * + * @param instance Event Loop instance + * @param object Object to check + */ +bool furi_event_loop_is_subscribed(FuriEventLoop* instance, FuriEventLoopObject* object); + +/** + * @brief Convenience function for `if(is_subscribed()) unsubscribe()` + */ +static inline void + furi_event_loop_maybe_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* object) { + if(furi_event_loop_is_subscribed(instance, object)) + furi_event_loop_unsubscribe(instance, object); +} + #ifdef __cplusplus } #endif diff --git a/lib/mjs/mjs_core.c b/lib/mjs/mjs_core.c index bcdcb364a..f3e28a5ba 100644 --- a/lib/mjs/mjs_core.c +++ b/lib/mjs/mjs_core.c @@ -103,6 +103,7 @@ struct mjs* mjs_create(void* context) { sizeof(struct mjs_object), MJS_OBJECT_ARENA_SIZE, MJS_OBJECT_ARENA_INC_SIZE); + mjs->object_arena.destructor = mjs_obj_destructor; gc_arena_init( &mjs->property_arena, sizeof(struct mjs_property), diff --git a/lib/mjs/mjs_object.c b/lib/mjs/mjs_object.c index 2aea1bd46..60bacf514 100644 --- a/lib/mjs/mjs_object.c +++ b/lib/mjs/mjs_object.c @@ -9,6 +9,7 @@ #include "mjs_primitive.h" #include "mjs_string.h" #include "mjs_util.h" +#include "furi.h" #include "common/mg_str.h" @@ -20,6 +21,19 @@ MJS_PRIVATE mjs_val_t mjs_object_to_value(struct mjs_object* o) { } } +MJS_PRIVATE void mjs_obj_destructor(struct mjs* mjs, void* cell) { + struct mjs_object* obj = cell; + mjs_val_t obj_val = mjs_object_to_value(obj); + + struct mjs_property* destructor = mjs_get_own_property( + mjs, obj_val, MJS_DESTRUCTOR_PROP_NAME, strlen(MJS_DESTRUCTOR_PROP_NAME)); + if(!destructor) return; + if(!mjs_is_foreign(destructor->value)) return; + + mjs_custom_obj_destructor_t destructor_fn = mjs_get_ptr(mjs, destructor->value); + if(destructor_fn) destructor_fn(mjs, obj_val); +} + MJS_PRIVATE struct mjs_object* get_object_struct(mjs_val_t v) { struct mjs_object* ret = NULL; if(mjs_is_null(v)) { @@ -293,7 +307,8 @@ mjs_val_t * start from the end so the constructed object more closely resembles * the definition. */ - while(def->name != NULL) def++; + while(def->name != NULL) + def++; for(def--; def >= defs; def--) { mjs_val_t v = MJS_UNDEFINED; const char* ptr = (const char*)base + def->offset; diff --git a/lib/mjs/mjs_object.h b/lib/mjs/mjs_object.h index 1c4810385..870486d06 100644 --- a/lib/mjs/mjs_object.h +++ b/lib/mjs/mjs_object.h @@ -50,6 +50,11 @@ MJS_PRIVATE mjs_err_t mjs_set_internal( */ MJS_PRIVATE void mjs_op_create_object(struct mjs* mjs); +/* + * Cell destructor for object arena + */ +MJS_PRIVATE void mjs_obj_destructor(struct mjs* mjs, void* cell); + #define MJS_PROTO_PROP_NAME "__p" /* Make it < 5 chars */ #if defined(__cplusplus) diff --git a/lib/mjs/mjs_object_public.h b/lib/mjs/mjs_object_public.h index f9f06c616..1a021a9d8 100644 --- a/lib/mjs/mjs_object_public.h +++ b/lib/mjs/mjs_object_public.h @@ -119,6 +119,14 @@ int mjs_del(struct mjs* mjs, mjs_val_t obj, const char* name, size_t len); */ mjs_val_t mjs_next(struct mjs* mjs, mjs_val_t obj, mjs_val_t* iterator); +typedef void (*mjs_custom_obj_destructor_t)(struct mjs* mjs, mjs_val_t object); + +/* + * Destructor property name. If set, must be a foreign pointer to a function + * that will be called just before the object is freed. + */ +#define MJS_DESTRUCTOR_PROP_NAME "__d" + #if defined(__cplusplus) } #endif /* __cplusplus */ diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 9b2160d53..7943c4cfc 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.1,, +Version,+,77.2,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1116,6 +1116,7 @@ Function,+,furi_event_flag_set,uint32_t,"FuriEventFlag*, uint32_t" Function,+,furi_event_flag_wait,uint32_t,"FuriEventFlag*, uint32_t, uint32_t, uint32_t" Function,+,furi_event_loop_alloc,FuriEventLoop*, Function,+,furi_event_loop_free,void,FuriEventLoop* +Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObject*" Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* @@ -1372,6 +1373,8 @@ Function,-,furi_hal_resources_deinit_early,void, Function,+,furi_hal_resources_get_ext_pin_number,int32_t,const GpioPin* Function,-,furi_hal_resources_init,void, Function,-,furi_hal_resources_init_early,void, +Function,+,furi_hal_resources_pin_by_name,const GpioPinRecord*,const char* +Function,+,furi_hal_resources_pin_by_number,const GpioPinRecord*,uint8_t Function,-,furi_hal_rtc_deinit_early,void, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,DateTime* @@ -2687,6 +2690,7 @@ Function,+,text_input_get_validator_callback_context,void*,TextInput* Function,+,text_input_get_view,View*,TextInput* Function,+,text_input_reset,void,TextInput* Function,+,text_input_set_header_text,void,"TextInput*, const char*" +Function,+,text_input_set_minimum_length,void,"TextInput*, size_t" Function,+,text_input_set_result_callback,void,"TextInput*, TextInputCallback, void*, char*, size_t, _Bool" Function,+,text_input_set_validator,void,"TextInput*, TextInputValidatorCallback, void*" Function,-,tgamma,double,double @@ -2761,6 +2765,7 @@ Function,+,view_allocate_model,void,"View*, ViewModelType, size_t" Function,+,view_commit_model,void,"View*, _Bool" Function,+,view_dispatcher_add_view,void,"ViewDispatcher*, uint32_t, View*" Function,+,view_dispatcher_alloc,ViewDispatcher*, +Function,+,view_dispatcher_alloc_ex,ViewDispatcher*,FuriEventLoop* Function,+,view_dispatcher_attach_to_gui,void,"ViewDispatcher*, Gui*, ViewDispatcherType" Function,+,view_dispatcher_enable_queue,void,ViewDispatcher* Function,+,view_dispatcher_free,void,ViewDispatcher* diff --git a/targets/f18/furi_hal/furi_hal_resources.c b/targets/f18/furi_hal/furi_hal_resources.c index 45ca3e6c4..2e3654435 100644 --- a/targets/f18/furi_hal/furi_hal_resources.c +++ b/targets/f18/furi_hal/furi_hal_resources.c @@ -354,3 +354,19 @@ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio) { } return -1; } + +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(strcasecmp(name, record->name) == 0) return record; + } + return NULL; +} + +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(record->number == number) return record; + } + return NULL; +} diff --git a/targets/f18/furi_hal/furi_hal_resources.h b/targets/f18/furi_hal/furi_hal_resources.h index 8f6173eb9..9a0d04cb6 100644 --- a/targets/f18/furi_hal/furi_hal_resources.h +++ b/targets/f18/furi_hal/furi_hal_resources.h @@ -121,6 +121,26 @@ void furi_hal_resources_init(void); */ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio); +/** + * @brief Finds a pin by its name + * + * @param name case-insensitive pin name to look for (e.g. `"Pc3"`, `"pA4"`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name); + +/** + * @brief Finds a pin by its number + * + * @param name pin number to look for (e.g. `7`, `4`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number); + #ifdef __cplusplus } #endif diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index b5dab0f28..c121fc716 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.1,, +Version,+,77.2,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1221,6 +1221,7 @@ Function,+,furi_event_flag_set,uint32_t,"FuriEventFlag*, uint32_t" Function,+,furi_event_flag_wait,uint32_t,"FuriEventFlag*, uint32_t, uint32_t, uint32_t" Function,+,furi_event_loop_alloc,FuriEventLoop*, Function,+,furi_event_loop_free,void,FuriEventLoop* +Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObject*" Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* @@ -1536,6 +1537,8 @@ Function,-,furi_hal_resources_deinit_early,void, Function,+,furi_hal_resources_get_ext_pin_number,int32_t,const GpioPin* Function,-,furi_hal_resources_init,void, Function,-,furi_hal_resources_init_early,void, +Function,+,furi_hal_resources_pin_by_name,const GpioPinRecord*,const char* +Function,+,furi_hal_resources_pin_by_number,const GpioPinRecord*,uint8_t Function,+,furi_hal_rfid_comp_set_callback,void,"FuriHalRfidCompCallback, void*" Function,+,furi_hal_rfid_comp_start,void, Function,+,furi_hal_rfid_comp_stop,void, @@ -3531,6 +3534,7 @@ Function,+,text_input_get_validator_callback_context,void*,TextInput* Function,+,text_input_get_view,View*,TextInput* Function,+,text_input_reset,void,TextInput* Function,+,text_input_set_header_text,void,"TextInput*, const char*" +Function,+,text_input_set_minimum_length,void,"TextInput*, size_t" Function,+,text_input_set_result_callback,void,"TextInput*, TextInputCallback, void*, char*, size_t, _Bool" Function,+,text_input_set_validator,void,"TextInput*, TextInputValidatorCallback, void*" Function,-,tgamma,double,double @@ -3605,6 +3609,7 @@ Function,+,view_allocate_model,void,"View*, ViewModelType, size_t" Function,+,view_commit_model,void,"View*, _Bool" Function,+,view_dispatcher_add_view,void,"ViewDispatcher*, uint32_t, View*" Function,+,view_dispatcher_alloc,ViewDispatcher*, +Function,+,view_dispatcher_alloc_ex,ViewDispatcher*,FuriEventLoop* Function,+,view_dispatcher_attach_to_gui,void,"ViewDispatcher*, Gui*, ViewDispatcherType" Function,+,view_dispatcher_enable_queue,void,ViewDispatcher* Function,+,view_dispatcher_free,void,ViewDispatcher* diff --git a/targets/f7/furi_hal/furi_hal_resources.c b/targets/f7/furi_hal/furi_hal_resources.c index 486c24230..123ebc420 100644 --- a/targets/f7/furi_hal/furi_hal_resources.c +++ b/targets/f7/furi_hal/furi_hal_resources.c @@ -288,3 +288,19 @@ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio) { } return -1; } + +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(strcasecmp(name, record->name) == 0) return record; + } + return NULL; +} + +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(record->number == number) return record; + } + return NULL; +} diff --git a/targets/f7/furi_hal/furi_hal_resources.h b/targets/f7/furi_hal/furi_hal_resources.h index c01b2207f..ec8794cc1 100644 --- a/targets/f7/furi_hal/furi_hal_resources.h +++ b/targets/f7/furi_hal/furi_hal_resources.h @@ -227,6 +227,26 @@ void furi_hal_resources_init(void); */ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio); +/** + * @brief Finds a pin by its name + * + * @param name case-insensitive pin name to look for (e.g. `"Pc3"`, `"pA4"`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name); + +/** + * @brief Finds a pin by its number + * + * @param name pin number to look for (e.g. `7`, `4`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number); + #ifdef __cplusplus } #endif diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..2655a8b97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "CommonJS", + "typeRoots": [ + "./applications/system/js_app/types" + ], + "noLib": true, + }, + "include": [ + "./applications/system/js_app/examples/apps/Scripts", + "./applications/debug/unit_tests/resources/unit_tests/js", + "./applications/system/js_app/types/global.d.ts", + ] +} \ No newline at end of file