From 4b9b1769f77ab4ac2da65ab8289c8daaae8219ec Mon Sep 17 00:00:00 2001 From: MX <10697207+xMasterX@users.noreply.github.com> Date: Tue, 15 Oct 2024 00:08:47 +0300 Subject: [PATCH] Merge remote-tracking branch 'OFW/dev' into dev --- applications/debug/unit_tests/application.fam | 8 + .../resources/unit_tests/js/basic.js | 4 + .../resources/unit_tests/js/event_loop.js | 30 +++ .../resources/unit_tests/js/math.js | 34 +++ .../resources/unit_tests/js/storage.js | 136 ++++++++++ .../debug/unit_tests/tests/js/js_test.c | 88 +++++++ applications/debug/unit_tests/tests/minunit.h | 5 +- .../debug/unit_tests/unit_test_api_table_i.h | 14 +- applications/main/nfc/application.fam | 9 + .../main/nfc/plugins/supported_cards/hworld.c | 243 ++++++++++++++++++ applications/services/gui/canvas.h | 15 +- .../services/gui/modules/text_input.h | 7 + applications/services/gui/view_dispatcher.c | 22 +- applications/services/gui/view_dispatcher.h | 13 + applications/services/gui/view_dispatcher_i.h | 1 + applications/services/storage/storage.h | 4 +- documentation/Doxyfile | 2 +- documentation/images/dialog.png | Bin 0 -> 1377 bytes documentation/images/empty.png | Bin 0 -> 1005 bytes documentation/images/loading.png | Bin 0 -> 1173 bytes documentation/images/submenu.png | Bin 0 -> 1774 bytes documentation/images/text_box.png | Bin 0 -> 2350 bytes documentation/images/text_input.png | Bin 0 -> 2044 bytes documentation/js/js_builtin.md | 12 +- documentation/js/js_dialog.md | 49 ---- documentation/js/js_event_loop.md | 144 +++++++++++ documentation/js/js_gpio.md | 77 ++++++ documentation/js/js_gui.md | 161 ++++++++++++ documentation/js/js_gui__dialog.md | 53 ++++ documentation/js/js_gui__empty_screen.md | 22 ++ documentation/js/js_gui__loading.md | 23 ++ documentation/js/js_gui__submenu.md | 37 +++ documentation/js/js_gui__text_box.md | 25 ++ documentation/js/js_gui__text_input.md | 44 ++++ documentation/js/js_submenu.md | 48 ---- documentation/js/js_textbox.md | 69 ----- fbt_options.py | 1 + firmware.scons | 1 + furi/core/event_loop.c | 12 + furi/core/event_loop.h | 17 ++ lib/mjs/mjs_core.c | 1 + lib/mjs/mjs_object.c | 17 +- lib/mjs/mjs_object.h | 5 + lib/mjs/mjs_object_public.h | 8 + .../iso14443_4a/iso14443_4a_listener.c | 5 +- targets/f18/api_symbols.csv | 7 +- targets/f18/furi_hal/furi_hal_resources.c | 16 ++ targets/f18/furi_hal/furi_hal_resources.h | 20 ++ targets/f7/api_symbols.csv | 6 +- targets/f7/furi_hal/furi_hal_resources.c | 16 ++ targets/f7/furi_hal/furi_hal_resources.h | 20 ++ targets/f7/stm32wb55xx_flash.ld | 2 +- targets/f7/stm32wb55xx_ram_fw.ld | 2 +- tsconfig.json | 15 ++ 54 files changed, 1357 insertions(+), 213 deletions(-) create mode 100644 applications/debug/unit_tests/resources/unit_tests/js/basic.js create mode 100644 applications/debug/unit_tests/resources/unit_tests/js/event_loop.js create mode 100644 applications/debug/unit_tests/resources/unit_tests/js/math.js create mode 100644 applications/debug/unit_tests/resources/unit_tests/js/storage.js create mode 100644 applications/debug/unit_tests/tests/js/js_test.c create mode 100644 applications/main/nfc/plugins/supported_cards/hworld.c create mode 100644 documentation/images/dialog.png create mode 100644 documentation/images/empty.png create mode 100644 documentation/images/loading.png create mode 100644 documentation/images/submenu.png create mode 100644 documentation/images/text_box.png create mode 100644 documentation/images/text_input.png delete mode 100644 documentation/js/js_dialog.md create mode 100644 documentation/js/js_event_loop.md create mode 100644 documentation/js/js_gpio.md create mode 100644 documentation/js/js_gui.md create mode 100644 documentation/js/js_gui__dialog.md create mode 100644 documentation/js/js_gui__empty_screen.md create mode 100644 documentation/js/js_gui__loading.md create mode 100644 documentation/js/js_gui__submenu.md create mode 100644 documentation/js/js_gui__text_box.md create mode 100644 documentation/js/js_gui__text_input.md delete mode 100644 documentation/js/js_submenu.md delete mode 100644 documentation/js/js_textbox.md create mode 100644 tsconfig.json 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/main/nfc/application.fam b/applications/main/nfc/application.fam index ae5372910..58660253f 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -272,6 +272,15 @@ App( sources=["plugins/supported_cards/skylanders.c"], ) +App( + appid="hworld_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="hworld_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/hworld.c"], +) + App( appid="nfc_cli", targets=["f7"], diff --git a/applications/main/nfc/plugins/supported_cards/hworld.c b/applications/main/nfc/plugins/supported_cards/hworld.c new file mode 100644 index 000000000..674e7b955 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/hworld.c @@ -0,0 +1,243 @@ +// Flipper Zero parser for H World Hotel Key Cards +// H World operates around 10,000 hotels, most of which in mainland China +// Reverse engineering and parser written by @Torron (Github: @zinongli) +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include + +#define TAG "H World" +#define ROOM_SECTOR 1 +#define VIP_SECTOR 5 +#define ROOM_SECTOR_KEY_BLOCK 7 +#define VIP_SECTOR_KEY_BLOCK 23 +#define ACCESS_INFO_BLOCK 5 +#define ROOM_NUM_DECIMAL_BLOCK 6 +#define H_WORLD_YEAR_OFFSET 2000 + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +static MfClassicKeyPair hworld_standard_keys[] = { + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 000 + {.a = 0x543071543071, .b = 0x5F01015F0101}, // 001 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 005 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 006 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 007 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 008 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 009 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 010 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 011 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 012 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015 +}; + +static MfClassicKeyPair hworld_vip_keys[] = { + {.a = 0x000000000000, .b = 0xFFFFFFFFFFFF}, // 000 + {.a = 0x543071543071, .b = 0x5F01015F0101}, // 001 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 005 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 006 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 007 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 008 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 009 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 010 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 011 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 012 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015 +}; + +static bool hworld_verify(Nfc* nfc) { + bool verified = false; + + do { + const uint8_t block_num = mf_classic_get_first_block_num_of_sector(ROOM_SECTOR); + + MfClassicKey standard_key = {0}; + bit_lib_num_to_bytes_be( + hworld_standard_keys[ROOM_SECTOR].a, COUNT_OF(standard_key.data), standard_key.data); + + MfClassicAuthContext auth_context; + MfClassicError standard_error = mf_classic_poller_sync_auth( + nfc, block_num, &standard_key, MfClassicKeyTypeA, &auth_context); + + if(standard_error != MfClassicErrorNone) { + FURI_LOG_D(TAG, "Failed static key check for block %u", block_num); + break; + } + + MfClassicKey vip_key = {0}; + bit_lib_num_to_bytes_be( + hworld_vip_keys[VIP_SECTOR].b, COUNT_OF(vip_key.data), vip_key.data); + + MfClassicError vip_error = mf_classic_poller_sync_auth( + nfc, block_num, &vip_key, MfClassicKeyTypeB, &auth_context); + + if(vip_error == MfClassicErrorNone) { + FURI_LOG_D(TAG, "VIP card detected"); + } else { + FURI_LOG_D(TAG, "Standard card detected"); + } + + verified = true; + } while(false); + + return verified; +} + +static bool hworld_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + bool is_read = false; + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + do { + MfClassicType type = MfClassicType1k; + MfClassicError standard_error = mf_classic_poller_sync_detect_type(nfc, &type); + MfClassicError vip_error = MfClassicErrorNotPresent; + if(standard_error != MfClassicErrorNone) break; + data->type = type; + + MfClassicDeviceKeys standard_keys = {}; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be( + hworld_standard_keys[i].a, sizeof(MfClassicKey), standard_keys.key_a[i].data); + FURI_BIT_SET(standard_keys.key_a_mask, i); + bit_lib_num_to_bytes_be( + hworld_standard_keys[i].b, sizeof(MfClassicKey), standard_keys.key_b[i].data); + FURI_BIT_SET(standard_keys.key_b_mask, i); + } + + standard_error = mf_classic_poller_sync_read(nfc, &standard_keys, data); + if(standard_error == MfClassicErrorNone) { + FURI_LOG_I(TAG, "Standard card successfully read"); + } else { + MfClassicDeviceKeys vip_keys = {}; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be( + hworld_vip_keys[i].a, sizeof(MfClassicKey), vip_keys.key_a[i].data); + FURI_BIT_SET(vip_keys.key_a_mask, i); + bit_lib_num_to_bytes_be( + hworld_vip_keys[i].b, sizeof(MfClassicKey), vip_keys.key_b[i].data); + FURI_BIT_SET(vip_keys.key_b_mask, i); + } + + vip_error = mf_classic_poller_sync_read(nfc, &vip_keys, data); + + if(vip_error == MfClassicErrorNone) { + FURI_LOG_I(TAG, "VIP card successfully read"); + } else { + break; + } + } + + nfc_device_set_data(device, NfcProtocolMfClassic, data); + + is_read = (standard_error == MfClassicErrorNone) | (vip_error == MfClassicErrorNone); + } while(false); + + mf_classic_free(data); + + return is_read; +} + +bool hworld_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + bool parsed = false; + + do { + // Check card type + if(data->type != MfClassicType1k) break; + + // Check static key for verificaiton + const uint8_t* data_room_sec_key_a_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[0]; + const uint8_t* data_room_sec_key_b_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[10]; + uint64_t data_room_sec_key_a = bit_lib_get_bits_64(data_room_sec_key_a_ptr, 0, 48); + uint64_t data_room_sec_key_b = bit_lib_get_bits_64(data_room_sec_key_b_ptr, 0, 48); + if((data_room_sec_key_a != hworld_standard_keys[ROOM_SECTOR].a) | + (data_room_sec_key_b != hworld_standard_keys[ROOM_SECTOR].b)) + break; + + // Check whether this card is VIP + const uint8_t* data_vip_sec_key_b_ptr = &data->block[VIP_SECTOR_KEY_BLOCK].data[10]; + uint64_t data_vip_sec_key_b = bit_lib_get_bits_64(data_vip_sec_key_b_ptr, 0, 48); + bool is_hworld_vip = (data_vip_sec_key_b == hworld_vip_keys[VIP_SECTOR].b); + uint8_t room_floor = data->block[ACCESS_INFO_BLOCK].data[13]; + uint8_t room_num = data->block[ACCESS_INFO_BLOCK].data[14]; + + // Check in date & time + uint16_t check_in_year = data->block[ACCESS_INFO_BLOCK].data[2] + H_WORLD_YEAR_OFFSET; + uint8_t check_in_month = data->block[ACCESS_INFO_BLOCK].data[3]; + uint8_t check_in_day = data->block[ACCESS_INFO_BLOCK].data[4]; + uint8_t check_in_hour = data->block[ACCESS_INFO_BLOCK].data[5]; + uint8_t check_in_minute = data->block[ACCESS_INFO_BLOCK].data[6]; + + // Expire date & time + uint16_t expire_year = data->block[ACCESS_INFO_BLOCK].data[7] + H_WORLD_YEAR_OFFSET; + uint8_t expire_month = data->block[ACCESS_INFO_BLOCK].data[8]; + uint8_t expire_day = data->block[ACCESS_INFO_BLOCK].data[9]; + uint8_t expire_hour = data->block[ACCESS_INFO_BLOCK].data[10]; + uint8_t expire_minute = data->block[ACCESS_INFO_BLOCK].data[11]; + + furi_string_cat_printf(parsed_data, "\e#H World Card\n"); + furi_string_cat_printf( + parsed_data, "%s\n", is_hworld_vip ? "VIP card" : "Standard room key"); + furi_string_cat_printf(parsed_data, "Room Num: %u%02u\n", room_floor, room_num); + furi_string_cat_printf( + parsed_data, + "Check-in Date: \n%04u-%02d-%02d\n%02d:%02d:00\n", + check_in_year, + check_in_month, + check_in_day, + check_in_hour, + check_in_minute); + furi_string_cat_printf( + parsed_data, + "Expiration Date: \n%04u-%02d-%02d\n%02d:%02d:00", + expire_year, + expire_month, + expire_day, + expire_hour, + expire_minute); + parsed = true; + } while(false); + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin hworld_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = hworld_verify, + .read = hworld_read, + .parse = hworld_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor hworld_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &hworld_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* hworld_plugin_ep(void) { + return &hworld_plugin_descriptor; +} diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index 86f7e9808..efd314687 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -298,15 +298,14 @@ void canvas_draw_xbm( /** Draw rotated XBM bitmap * - * @param canvas Canvas instance - * @param x x coordinate - * @param y y coordinate - * @param[in] width bitmap width - * @param[in] height bitmap height - * @param[in] rotation bitmap rotation - * @param bitmap pointer to XBM bitmap data + * @param canvas Canvas instance + * @param x x coordinate + * @param y y coordinate + * @param[in] width bitmap width + * @param[in] height bitmap height + * @param[in] rotation bitmap rotation + * @param bitmap_data pointer to XBM bitmap data */ - void canvas_draw_xbm_ex( Canvas* canvas, int32_t x, diff --git a/applications/services/gui/modules/text_input.h b/applications/services/gui/modules/text_input.h index 6733c01cc..1c020d186 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/documentation/Doxyfile b/documentation/Doxyfile index 5031f8d59..e3cc3f6fa 100644 --- a/documentation/Doxyfile +++ b/documentation/Doxyfile @@ -1106,7 +1106,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/images/dialog.png b/documentation/images/dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..008ae9ce524fc2e34f234bb64f7bc4fee216a1eb GIT binary patch literal 1377 zcmeAS@N?(olHy`uVBq!ia0y~yU;;8388|?c*QZDWAjMhW5n0T@z;^_M8K-LVNi#68 z3VXUZhE&XXd)LwLwu3~&MYckjbH^1e3urW;@tOptGJV_eUB{v*>16Y zegF1&hA(^O)G{*6ab{px#KNH9HA>Mf2wr`C!_DxlqVoTr-|vA@A-uk2^#&$}4k3mH z4Mv8JT{A^LUA|}e>DqbuneP8<-b_ocy1#D5-P`;LEust#Qy3UrIDyo8#tla%?)*P- zFZ;h4TjdWIZ$=ofiT!|3`q{btHEjvkp7V#*_Z{?^AGrn*zi${fWTs5}d*Zq5%&+-> z&)hrSCsX|ObJJclCD-oniGR8$AeogxK$U?ZkcmMcm7l?H+ppci3EfBzv1OQ%@Oqzv z^0#weYSWE2M=(O%2XyA$Gwcs7XZ+qg9YuA6hBm~XQ11>eYQ^=<`VIAQ_w3K_V~Ke8 z`tjR0-VbIY{D0-F&AXUq|6(qlzc1~6?{MA!Ij6bcdGD0m0nW6uf8FDDM}EFJ@o~=I zKNBCnF)g|m`T4bW)Wl=e! z!jtzmx7!@5`F8Qyq0Rs7xA*P*zESyjWbM6)uPa-w_w7W9aDyjnes5VbukgL?vlBiw z?~~p8Dxd6epYhw?94Wr48F(UV<^L-`uT^{3dEPb>5!pAHcQi!aUmrbtdHls!*E8OK z+*>~n$>URO8@gwf{;&F%|GxHTzfIW_)45-*-pze^JH09o$esDBZ3c@>KS&6ezW94aruGm%Eh_Hh3ifrg7TGlixSt;(kEIWz%rA;)78&q Iol`;+0D;-4I{*Lx literal 0 HcmV?d00001 diff --git a/documentation/images/empty.png b/documentation/images/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..844f45093f96cdc2120fcf9d040934888bfa89ac GIT binary patch literal 1005 zcmeAS@N?(olHy`uVBq!ia0y~yU;;8388|?c*QZDWAjMhW5n0T@z;^_M8K-LVNi#4o z&-Zk345^s&_NpN-g95_=0|$ODLEi}nH-;VDceeT)!-Dj`d<+lxnHf5a7#cK2snH-9 gO$Bt!2rJl^Su$_wn)*l!n12~OUHx3vIVCg!0K_`c6#xJL literal 0 HcmV?d00001 diff --git a/documentation/images/loading.png b/documentation/images/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..f35966f66a809f796d17a1ec1c9a097d2fe767f0 GIT binary patch literal 1173 zcmeAS@N?(olHy`uVBq!ia0y~yU;;8388|?c*QZDWAjMhW5n0T@z;^_M8K-LVNi#68 zxO=)dhE&XXd)G0G$x*=JqK4e@&rb#B=|7*SbB818M8mDifA2@L8+Et%m%jY+u*$NY z@j|i8Uj~LAMFxgICI$i3Q3`7iluG?&_z-{m{rrpl8*Y9rlKac?VG1XMf)@kBA{GXP zQ+y4YZ-3_09*br5Rbyz-U}WeJVrW=#c;5M-hcj*GZ~i?$Nem&bVa>3td_Vt>C-*Ul*j^dJ{JM0YRNForY!+dc01$%@4jbAtG?{h9VzyePmPh%Z)PR~8? z!CaWZVG09-3n!40W$>>2es}*j#x+L}3RBq+d~ut5`Zl|PHImZb3?~Z9_>pb3W)Ql= z$2eT9G{Vongi?o%zS-KearFPzIW6$*a*Su*vtn$xBA6U*Xc)I$ztaD0e0sz9R@tyzx literal 0 HcmV?d00001 diff --git a/documentation/images/submenu.png b/documentation/images/submenu.png new file mode 100644 index 0000000000000000000000000000000000000000..1cb64e9748fd70ceeec3b5f62d0ff0af427b499a GIT binary patch literal 1774 zcmb_dYfw{16h4=T5DP(Su@DG_VnqkS2o7&hLY9}tlVgw$!l5fIuYMuJJOqh?Hv;bDXlz+R@QES-+O`s2>t-95W!&-d6Z zjtC1jT|-_20H(B%U6B9=_%8;=OHJq$eGA?YGeYQ@0HmLmKJ0rZk~P3;E^XJ2k8*3( zvc2Cu-e_q!(%pQzQk!5-KX`AQmg^W~E{@=obtwlKQ)VwSB^Kjmg^N1nmF?g)aIWx` zPRC43HUjIn0nV?2fadokg&n0<*hDKmvy=Vz*d`3ts-}+;U{)Z_=lB3z#silILjg|c z*ZoIV)vxh9lo?xIpI}-dSBIa>R|~hj00<-kC&B>iP5~Hyu-p}O_;4)Py;-AE1;>~* z(k}M!7(ace#8x*Z*KZMnAbhW^W|d7qKE2__v0ii&HC=I!^~R6S-6{rM@> zCJQBVG*R0X-T50k;#q*%axUr_f+ef7TJx0B`&?FVkCj*MJ{V>LT#%GawG$}S z)0mUILct6($mSX?S9+s{kW6pr&2i5A%0fx@ezISHZJE-&WGpzJu^v|=)rpi(SJw_! zYlJC13a-nd3-ypWjIGn-nF;H1!)?5Vs>Nwsqi4bW2hzAtpXiS= zo-xnWozppj@AnTgdsI+bOn9~v*-n~9erA@ci|&_@Qw)Pt#Kc(L z*_q3a$(qBC8=4mtU0oD79H>`G)gU%VuB2FbrjIt|DCCn17VKgET-=b4B&v7wh6p1F z*rAVXHGJXb^u#`TKF&c=WI6io?Di~xcl4r5;qy4jo~hg7fA*c?577eLkU7;g9;dC2 zJWZju3w96Wyv5euU_BZm_5|I{O7gWYLu%I2tYc)%*jbsBP#cUCcYXcH-tIAy-)>~A zq=f(>4G7?8{snle;i?QTL%RCHwNi>e!@IW$dJH-Q3}cIAnPWgCacp=_TJiH~ZqK!@?3}<6|%0 z^c=;z7%7;iWd!tSO3O>#C3nTX9nL&*WIqFMhkVnE+fny_3g=umfeS zY5`lulKQ(}ANjh@k4}i+*lJXuOw^@P2)+7w*1rO*6n;@ ziFPvP!Ei-VL6PG00X9OZqm^$DOBBL zP4Nf^qfn6R2p>=S%cB$x;ezb(R78=Y>`ZX%8J3SNR<=>@7a_XQx;B|)cpy3M*sK&c gPsRv%^LQd=Y35d0T5&{E4z$Czwrv?I!B!Q#^Ent8U0)!-xkYp}nX3cf_V}9Iq?_KBabN2qe zZ-4vVI2s#iW9?`S0I-RUiueM64bUS3SgqfVEFrz1+s2DgNE`sRUF#R(u7|A?09LP} zBR);YEtQX@7D_&HcyH+HvF~51ayF-?3*%KEK0dwG`x@f>0**HpVfe?$?MO57qIA67 zxVG3{o~KuuQE(|nyLR-daZNhc11`RY0Go~g@U=E2uexA2qP|LYJWo-|Xb0fXMnE~b z0qh)hjq?YTRkRY(-99e*5H#;Ht-S#tjqx;XXh055d;ooT2TG0qiKW*PjZ%z1Miked z9C*O*&HPv+!+2<=U7^@>N?W07*oZZ309LzAo5bKi5S2zFUk#v*-@-jI1NV2DR*GsZ z5^6jhO0d%*Z)m;Pz*vQMCTAk}(tb2S7M9q~Ov5B|)t)*=KrGigxeXDtG%!->H{~y5 zO2}tN@rQMhqS4P?0I)C>NH}kMLU4S+9b;ZV4P$s=QpikvXHt3+H(-UX6YJ&*=qx{3 z==>1YL;KZXU1sfV!68`mLBqX{n%)=l&Q-+cG@RX&&vVr+`3C{8Hr1W8=G<|2m7)mb z=r>BVbJ=z|0Bkh};d`OeXIBC#NX&9wC|RMMX)GLV5Dd(Qrew?Aq$rNyIUU1p)W@|d zM~}b0qppF~UoNx5Iwnu<+o)*XlQLngm1ErW6Z_wCvJQ35RTq#Moy!b)?U*!#l~uz+ zXCbvz+5*Z2Q+7Yuf>%?+2yk>002NSH)#q?PFxS@0)3b46G)lx4=M-EeUd~sitJHZn z#~9g`{5DRj3pjrWQpOf2W*E`&RcuMk@rtRZTeKD5!wTnzK!n>~Quv1@^S-2J+FXtQ zbB_2=ykv3ZL5BZOGm;d#MAm$(<2NXHS!r35tBSFVhsMlSB_UZF+lIO}r%t|I`F%KLW5zVKAQgUxq3JDaESFI3cm1YWZ0o>UG@q z)%j&>R#R2RWazy5{ZfWjqW?@!he*C=I>c`P;qbwvJ8PhKPO#%_1#Bgz#;ls-5wbMD zdP7m4Ua%a^Rdb}N*{+%LdzC*m-`DIIKzGhcA2LHc41=`2qgW9eTT+1ZnGCxe{t2=n*yO|6ihI{7FV(J|0 zSE3n5IvaxJs@;Swf914LTl zM(;2p-15{=X%tLMvt?#2g}YOmsQChQRv@qMO0p=Pdee`pWQ}+ZOce$W%_0tP6!A{3Ydc7N?!h(faG4h8+1cw3j@i=5-ePTC71BhIb5XiEq_ohM805RpQgKEo-4UL{g1HI z9+{2wU6{B&xiH7M2di2&;@qL)4A&InzrzqTv>b+HLYOelcWF_J7BRX)ww)oNoi93o z8Ml`s7fXwtRA|Zy*~(B^;LI3VporqCS$uQ2r!skneiZg0ejm2GwG0(>=5&gI_xHXa zU9IMCtftJ#K-Y7h-9!%lvoGW^^tRIYhI4r(6$9_W!*q*JmM*N!#BTrb^v4=KQKHTJ z29~eAx8;dnl-nkLF8M`s8Lxgu)4=Bsq@HO~T=SVRsz~iNA1tJ0)Fw$94E^qkh@vM~ zx^WGijZCp`yAs#bsYk`Y5QRJ`fK)20WwUG)^}ph;Ob6M-qd-TtX*T`wq~}hGOR!%) zoWByY01Dptm(6W3MgKp;`gh;Fy9vk*gh}@|eN=ujc-kk{yu*N2tS}e0y`7au)_+Cp ZMk}^Nr5&Jr2mJtn=)=O(d;sRX_+aAtJ?gO0Xj! z7%;&>7FmLdfDj-|wV_rCC|Otx0V4uQAPPuWOlTNj`tV2p_Q#uh-@WhNbI!M%_p7J7 zi~h!)8vy|NCr>zi55Q-dDGcZ=4X%@c2Q-83PbUK70oeRx>4D{1ZZ-yB1M{TQF`wkZ zagkeAzs>gbT^DcP$o_D4YhXyV#pp2)lSBEx4}~ZT!@}KiswUTqYs-b5W@0tWyt91* z2JT%2Kxa1q$4NKT63K!vBrd@@WQb%x4+pB}nq^%;@%#*!``q8~&b`BQkN{otH7IiF zP^~LLIF7ZTlvJrC6eTU-H1-v+>K1PkujIi*+hB=OJeeAx2ZI^a+~ z76lu>a7LUq)yPcVhFnfGM^g?edS6izOM0hRaEXD=peqGcAqkQ=z6WAaSNQpwn}g5g z$pQ}}Y%UuChdr?6h$(W6gT=do^MetQn%J5+OB;WaG;>H1OJSB!TZQNRzM^jWvyE-y zy&}a~Nb2MjRacbC`iaWA_AVs1cR@OqC%0qfGKASoszAkTws!t79%<&^LB=c1J#0916%8VY zaD%}XG7l5iG)(xzQsI@4V>1c$wZwzuxMAC%-u~n=e0Q7>|LqwzgxLz|5kT>B=1)YW zSR>`qlGBZh&{;U_QQ8W_rXsC;mGMljELo00Mr6E1o1qrD!4_4xcnR9E+CAXfUl<3l zeMJYn7idZ^{UN+-zf_$RIM;XwXTZv8!0TH=)X9Ifbm725@utV#cNZ9JE6AR&9LD1n zMDj-cCn^&U>!Lckr|k=xd^=`{0^Ht~BVo77X~G7~9DyGlKl7ez(*UVMimIrOJ^clR z1$9VRr`A0`gEkyQxx8nJY1qM=|Dv|4`z5C>xhgCQ1{-0W?bxrhs1AcDfJOwh3?NkDzG9vgYwE~H*@qthuUuA4T#*7w z?E08?qH-0zz6N0-9A@kqY8H_*Eft9Y1aj##G9!9DMW(ZCV_t3)Ocao3PmK>y%+Qjl zSb@%0cgMWB194BED%~L2Yt&X$I$f0Af8y^WR)Vj*96d(8XH)NxBi5jX%JfL)mcH>HWHx^ zDO97U>5quUx=0~LVeD&GWy|sEXOM!b8A57%-DKb3B};a33g>{t&ZI76?Xl;~V~XpZ z-UED>n^D{aCHV?xTTOeKnAbrI<}W_{3C}svlLt zQ{6og_Zz(xJ*KIia88vl>MUWPf>nlqn zPvajF0g!*W(l~w5lx_jhwz2eLnxVjkCNenDsvSGW>Os+aMgyvNC4%|e2@Oo^xE|@g#QCC zRT*}G_`tB$(Ro~`r=ew~AGFXhWmm9|tllgZW}_hSauC4eP5Q_vy-oRY=YLQ-zhmfH Wr2rOx&q?z;2PeOCcVZq7P5TEYI33~u literal 0 HcmV?d00001 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 8fbd78faa..f3396f98c 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/firmware.scons b/firmware.scons index 62b1184eb..4c5e05873 100644 --- a/firmware.scons +++ b/firmware.scons @@ -216,6 +216,7 @@ fwelf = fwenv["FW_ELF"] = fwenv.Program( sources, LIBS=fwenv["TARGET_CFG"].linker_dependencies, ) +Depends(fwelf, fwenv["LINKER_SCRIPT_PATH"]) # Firmware depends on everything child builders returned # Depends(fwelf, lib_targets) 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/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c b/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c index 32cc8f198..2519fb90c 100644 --- a/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c +++ b/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c @@ -65,10 +65,8 @@ static NfcCommand iso14443_4a_listener_run(NfcGenericEvent event, void* context) if(instance->state == Iso14443_4aListenerStateIdle) { if(bit_buffer_get_size_bytes(rx_buffer) == 2 && bit_buffer_get_byte(rx_buffer, 0) == ISO14443_4A_CMD_READ_ATS) { - if(iso14443_4a_listener_send_ats(instance, &instance->data->ats_data) != + if(iso14443_4a_listener_send_ats(instance, &instance->data->ats_data) == Iso14443_4aErrorNone) { - command = NfcCommandContinue; - } else { instance->state = Iso14443_4aListenerStateActive; } } @@ -93,7 +91,6 @@ static NfcCommand iso14443_4a_listener_run(NfcGenericEvent event, void* context) if(instance->callback) { command = instance->callback(instance->generic_event, instance->context); } - command = NfcCommandContinue; } return command; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index e6b025c8c..7943c4cfc 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,78.0,, +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 b6128aa30..8358958f4 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,78.0,, +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,, @@ -1256,6 +1256,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* @@ -1576,6 +1577,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, @@ -3691,6 +3694,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/targets/f7/stm32wb55xx_flash.ld b/targets/f7/stm32wb55xx_flash.ld index 3fb789645..524da6fc3 100644 --- a/targets/f7/stm32wb55xx_flash.ld +++ b/targets/f7/stm32wb55xx_flash.ld @@ -3,7 +3,7 @@ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _stack_end = 0x20030000; /* end of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ -_stack_size = 0x1000; /* required amount of stack */ +_stack_size = 0x200; /* required amount of stack */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K diff --git a/targets/f7/stm32wb55xx_ram_fw.ld b/targets/f7/stm32wb55xx_ram_fw.ld index cae30b6e9..f0e8ad678 100644 --- a/targets/f7/stm32wb55xx_ram_fw.ld +++ b/targets/f7/stm32wb55xx_ram_fw.ld @@ -3,7 +3,7 @@ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _stack_end = 0x20030000; /* end of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ -_stack_size = 0x1000; /* required amount of stack */ +_stack_size = 0x200; /* required amount of stack */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K 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