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/external b/applications/external index 36671ed58..6e157c28b 160000 --- a/applications/external +++ b/applications/external @@ -1 +1 @@ -Subproject commit 36671ed586d0d47d9a95f07d6775986117c2b7df +Subproject commit 6e157c28b3bea31122f0fcb4282a81cd4fcafa04 diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index cdb215334..6e4d16263 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -281,6 +281,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="sonicare_parser", apptype=FlipperAppType.PLUGIN, 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/main/subghz/subghz_cli.c b/applications/main/subghz/subghz_cli.c index 9c8f551cb..cf7300d59 100644 --- a/applications/main/subghz/subghz_cli.c +++ b/applications/main/subghz/subghz_cli.c @@ -998,13 +998,12 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { chat_event = subghz_chat_worker_get_event_chat(subghz_chat); switch(chat_event.event) { case SubGhzChatEventInputData: - if(chat_event.c == CliSymbolAsciiETX) { + if(chat_event.c == CliKeyETX) { printf("\r\n"); chat_event.event = SubGhzChatEventUserExit; subghz_chat_worker_put_event_chat(subghz_chat, &chat_event); break; - } else if( - (chat_event.c == CliSymbolAsciiBackspace) || (chat_event.c == CliSymbolAsciiDel)) { + } else if((chat_event.c == CliKeyBackspace) || (chat_event.c == CliKeyDEL)) { size_t len = furi_string_utf8_length(input); if(len > furi_string_utf8_length(name)) { printf("%s", "\e[D\e[1P"); @@ -1026,7 +1025,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { } furi_string_set(input, sysmsg); } - } else if(chat_event.c == CliSymbolAsciiCR) { + } else if(chat_event.c == CliKeyCR) { printf("\r\n"); furi_string_push_back(input, '\r'); furi_string_push_back(input, '\n'); @@ -1040,7 +1039,7 @@ static void subghz_cli_command_chat(Cli* cli, FuriString* args) { furi_string_printf(input, "%s", furi_string_get_cstr(name)); printf("%s", furi_string_get_cstr(input)); fflush(stdout); - } else if(chat_event.c == CliSymbolAsciiLF) { + } else if(chat_event.c == CliKeyLF) { //cut out the symbol \n } else { putc(chat_event.c, stdout); diff --git a/applications/main/subghz/subghz_cli.h b/applications/main/subghz/subghz_cli.h index f6388218f..18c84c3e0 100644 --- a/applications/main/subghz/subghz_cli.h +++ b/applications/main/subghz/subghz_cli.h @@ -1,5 +1,6 @@ #pragma once #include +#include void subghz_on_system_start(void); diff --git a/applications/services/bt/bt_service/bt.c b/applications/services/bt/bt_service/bt.c index 8188f8513..a2bb29e11 100644 --- a/applications/services/bt/bt_service/bt.c +++ b/applications/services/bt/bt_service/bt.c @@ -445,13 +445,11 @@ static void bt_change_profile(Bt* bt, BtMessage* message) { *message->profile_instance = NULL; } } - if(message->lock) api_lock_unlock(message->lock); } -static void bt_close_connection(Bt* bt, BtMessage* message) { +static void bt_close_connection(Bt* bt) { bt_close_rpc_connection(bt); furi_hal_bt_stop_advertising(); - if(message->lock) api_lock_unlock(message->lock); } static void bt_apply_settings(Bt* bt) { @@ -499,19 +497,13 @@ static void bt_load_settings(Bt* bt) { } static void bt_handle_get_settings(Bt* bt, BtMessage* message) { - furi_assert(message->lock); *message->data.settings = bt->bt_settings; - api_lock_unlock(message->lock); } static void bt_handle_set_settings(Bt* bt, BtMessage* message) { - furi_assert(message->lock); bt->bt_settings = *message->data.csettings; - bt_apply_settings(bt); bt_settings_save(&bt->bt_settings); - - api_lock_unlock(message->lock); } static void bt_handle_reload_keys_settings(Bt* bt) { @@ -576,6 +568,12 @@ int32_t bt_srv(void* p) { while(1) { furi_check( furi_message_queue_get(bt->message_queue, &message, FuriWaitForever) == FuriStatusOk); + FURI_LOG_D( + TAG, + "call %d, lock 0x%p, result 0x%p", + message.type, + (void*)message.lock, + (void*)message.result); if(message.type == BtMessageTypeUpdateStatus) { // Update view ports bt_statusbar_update(bt); @@ -599,7 +597,7 @@ int32_t bt_srv(void* p) { } else if(message.type == BtMessageTypeSetProfile) { bt_change_profile(bt, &message); } else if(message.type == BtMessageTypeDisconnect) { - bt_close_connection(bt, &message); + bt_close_connection(bt); } else if(message.type == BtMessageTypeForgetBondedDevices) { bt_keys_storage_delete(bt->keys_storage); } else if(message.type == BtMessageTypeGetSettings) { @@ -609,6 +607,8 @@ int32_t bt_srv(void* p) { } else if(message.type == BtMessageTypeReloadKeysSettings) { bt_handle_reload_keys_settings(bt); } + + if(message.lock) api_lock_unlock(message.lock); } return 0; diff --git a/applications/services/cli/cli.c b/applications/services/cli/cli.c index 6837f4e18..3ca59bcc2 100644 --- a/applications/services/cli/cli.c +++ b/applications/services/cli/cli.c @@ -1,6 +1,7 @@ #include "cli_i.h" #include "cli_commands.h" #include "cli_vcp.h" +#include "cli_ansi.h" #include #include @@ -11,6 +12,8 @@ #define TAG "CliSrv" #define CLI_INPUT_LEN_LIMIT 256 +#define CLI_PROMPT ">: " // qFlipper does not recognize us if we use escape sequences :( +#define CLI_PROMPT_LENGTH 3 // printable characters Cli* cli_alloc(void) { Cli* cli = malloc(sizeof(Cli)); @@ -89,7 +92,7 @@ bool cli_cmd_interrupt_received(Cli* cli) { char c = '\0'; if(cli_is_connected(cli)) { if(cli->session->rx((uint8_t*)&c, 1, 0) == 1) { - return c == CliSymbolAsciiETX; + return c == CliKeyETX; } } else { return true; @@ -106,7 +109,8 @@ void cli_print_usage(const char* cmd, const char* usage, const char* arg) { } void cli_motd(void) { - printf("\r\n" + printf(ANSI_FLIPPER_BRAND_ORANGE + "\r\n" " _.-------.._ -,\r\n" " .-\"```\"--..,,_/ /`-, -, \\ \r\n" " .:\" /:/ /'\\ \\ ,_..., `. | |\r\n" @@ -120,12 +124,11 @@ void cli_motd(void) { " _L_ _ ___ ___ ___ ___ ____--\"`___ _ ___\r\n" "| __|| | |_ _|| _ \\| _ \\| __|| _ \\ / __|| | |_ _|\r\n" "| _| | |__ | | | _/| _/| _| | / | (__ | |__ | |\r\n" - "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" - "\r\n" - "Welcome to Flipper Zero Command Line Interface!\r\n" + "|_| |____||___||_| |_| |___||_|_\\ \\___||____||___|\r\n" ANSI_RESET + "\r\n" ANSI_FG_BR_WHITE "Welcome to " ANSI_FLIPPER_BRAND_ORANGE + "Flipper Zero" ANSI_FG_BR_WHITE " Command Line Interface!\r\n" "Read the manual: https://docs.flipper.net/development/cli\r\n" - "Run `help` or `?` to list available commands\r\n" - "\r\n"); + "Run `help` or `?` to list available commands\r\n" ANSI_RESET "\r\n"); const Version* firmware_version = furi_hal_version_get_firmware_version(); if(firmware_version) { @@ -146,7 +149,7 @@ void cli_nl(Cli* cli) { void cli_prompt(Cli* cli) { UNUSED(cli); - printf("\r\n>: %s", furi_string_get_cstr(cli->line)); + printf("\r\n" CLI_PROMPT "%s", furi_string_get_cstr(cli->line)); fflush(stdout); } @@ -169,7 +172,7 @@ static void cli_handle_backspace(Cli* cli) { cli->cursor_position--; } else { - cli_putc(cli, CliSymbolAsciiBell); + cli_putc(cli, CliKeyBell); } } @@ -245,7 +248,7 @@ static void cli_handle_enter(Cli* cli) { printf( "`%s` command not found, use `help` or `?` to list all available commands", furi_string_get_cstr(command)); - cli_putc(cli, CliSymbolAsciiBell); + cli_putc(cli, CliKeyBell); } cli_reset(cli); @@ -309,8 +312,85 @@ static void cli_handle_autocomplete(Cli* cli) { cli_prompt(cli); } -static void cli_handle_escape(Cli* cli, char c) { - if(c == 'A') { +typedef enum { + CliCharClassWord, + CliCharClassSpace, + CliCharClassOther, +} CliCharClass; + +/** + * @brief Determines the class that a character belongs to + * + * The return value of this function should not be used on its own; it should + * only be used for comparing it with other values returned by this function. + * This function is used internally in `cli_skip_run`. + */ +static CliCharClass cli_char_class(char c) { + if((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') { + return CliCharClassWord; + } else if(c == ' ') { + return CliCharClassSpace; + } else { + return CliCharClassOther; + } +} + +typedef enum { + CliSkipDirectionLeft, + CliSkipDirectionRight, +} CliSkipDirection; + +/** + * @brief Skips a run of a class of characters + * + * @param string Input string + * @param original_pos Position to start the search at + * @param direction Direction in which to perform the search + * @returns The position at which the run ends + */ +static size_t cli_skip_run(FuriString* string, size_t original_pos, CliSkipDirection direction) { + if(furi_string_size(string) == 0) return original_pos; + if(direction == CliSkipDirectionLeft && original_pos == 0) return original_pos; + if(direction == CliSkipDirectionRight && original_pos == furi_string_size(string)) + return original_pos; + + int8_t look_offset = (direction == CliSkipDirectionLeft) ? -1 : 0; + int8_t increment = (direction == CliSkipDirectionLeft) ? -1 : 1; + int32_t position = original_pos; + CliCharClass start_class = + cli_char_class(furi_string_get_char(string, position + look_offset)); + + while(true) { + position += increment; + if(position < 0) break; + if(position >= (int32_t)furi_string_size(string)) break; + if(cli_char_class(furi_string_get_char(string, position + look_offset)) != start_class) + break; + } + + return MAX(0, position); +} + +void cli_process_input(Cli* cli) { + CliKeyCombo combo = cli_read_ansi_key_combo(cli); + FURI_LOG_T(TAG, "code=0x%02x, mod=0x%x\r\n", combo.key, combo.modifiers); + + if(combo.key == CliKeyTab) { + cli_handle_autocomplete(cli); + + } else if(combo.key == CliKeySOH) { + furi_delay_ms(33); // We are too fast, Minicom is not ready yet + cli_motd(); + cli_prompt(cli); + + } else if(combo.key == CliKeyETX) { + cli_reset(cli); + cli_prompt(cli); + + } else if(combo.key == CliKeyEOT) { + cli_reset(cli); + + } else if(combo.key == CliKeyUp && combo.modifiers == CliModKeyNo) { // Use previous command if line buffer is empty if(furi_string_size(cli->line) == 0 && furi_string_cmp(cli->line, cli->last_line) != 0) { // Set line buffer and cursor position @@ -319,67 +399,85 @@ static void cli_handle_escape(Cli* cli, char c) { // Show new line to user printf("%s", furi_string_get_cstr(cli->line)); } - } else if(c == 'B') { - } else if(c == 'C') { + + } else if(combo.key == CliKeyDown && combo.modifiers == CliModKeyNo) { + // Clear input buffer + furi_string_reset(cli->line); + cli->cursor_position = 0; + printf("\r" CLI_PROMPT "\e[0K"); + + } else if(combo.key == CliKeyRight && combo.modifiers == CliModKeyNo) { + // Move right if(cli->cursor_position < furi_string_size(cli->line)) { cli->cursor_position++; printf("\e[C"); } - } else if(c == 'D') { + + } else if(combo.key == CliKeyLeft && combo.modifiers == CliModKeyNo) { + // Move left if(cli->cursor_position > 0) { cli->cursor_position--; printf("\e[D"); } - } - fflush(stdout); -} -void cli_process_input(Cli* cli) { - char in_chr = cli_getc(cli); - size_t rx_len; + } else if(combo.key == CliKeyHome && combo.modifiers == CliModKeyNo) { + // Move to beginning of line + cli->cursor_position = 0; + printf("\e[%uG", CLI_PROMPT_LENGTH + 1); // columns start at 1 \(-_-)/ + + } else if(combo.key == CliKeyEnd && combo.modifiers == CliModKeyNo) { + // Move to end of line + cli->cursor_position = furi_string_size(cli->line); + printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); - if(in_chr == CliSymbolAsciiTab) { - cli_handle_autocomplete(cli); - } else if(in_chr == CliSymbolAsciiSOH) { - furi_delay_ms(33); // We are too fast, Minicom is not ready yet - cli_motd(); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiETX) { - cli_reset(cli); - cli_prompt(cli); - } else if(in_chr == CliSymbolAsciiEOT) { - cli_reset(cli); - } else if(in_chr == CliSymbolAsciiEsc) { - rx_len = cli_read(cli, (uint8_t*)&in_chr, 1); - if((rx_len > 0) && (in_chr == '[')) { - cli_read(cli, (uint8_t*)&in_chr, 1); - cli_handle_escape(cli, in_chr); - } else { - cli_putc(cli, CliSymbolAsciiBell); - } - } else if(in_chr == CliSymbolAsciiBackspace || in_chr == CliSymbolAsciiDel) { - cli_handle_backspace(cli); - } else if(in_chr == CliSymbolAsciiCR) { - cli_handle_enter(cli); } else if( - (in_chr >= 0x20 && in_chr < 0x7F) && //-V560 + combo.modifiers == CliModKeyCtrl && + (combo.key == CliKeyLeft || combo.key == CliKeyRight)) { + // Skip run of similar chars to the left or right + CliSkipDirection direction = (combo.key == CliKeyLeft) ? CliSkipDirectionLeft : + CliSkipDirectionRight; + cli->cursor_position = cli_skip_run(cli->line, cli->cursor_position, direction); + printf("\e[%zuG", CLI_PROMPT_LENGTH + cli->cursor_position + 1); + + } else if(combo.key == CliKeyBackspace || combo.key == CliKeyDEL) { + cli_handle_backspace(cli); + + } else if(combo.key == CliKeyETB) { // Ctrl + Backspace + // Delete run of similar chars to the left + size_t run_start = cli_skip_run(cli->line, cli->cursor_position, CliSkipDirectionLeft); + furi_string_replace_at(cli->line, run_start, cli->cursor_position - run_start, ""); + cli->cursor_position = run_start; + printf( + "\e[%zuG%s\e[0K\e[%zuG", // move cursor, print second half of line, erase remains, move cursor again + CLI_PROMPT_LENGTH + cli->cursor_position + 1, + furi_string_get_cstr(cli->line) + run_start, + CLI_PROMPT_LENGTH + run_start + 1); + + } else if(combo.key == CliKeyCR) { + cli_handle_enter(cli); + + } else if( + (combo.key >= 0x20 && combo.key < 0x7F) && //-V560 (furi_string_size(cli->line) < CLI_INPUT_LEN_LIMIT)) { if(cli->cursor_position == furi_string_size(cli->line)) { - furi_string_push_back(cli->line, in_chr); - cli_putc(cli, in_chr); + furi_string_push_back(cli->line, combo.key); + cli_putc(cli, combo.key); } else { // Insert character to line buffer - const char in_str[2] = {in_chr, 0}; + const char in_str[2] = {combo.key, 0}; furi_string_replace_at(cli->line, cli->cursor_position, 0, in_str); // Print character in replace mode - printf("\e[4h%c\e[4l", in_chr); + printf("\e[4h%c\e[4l", combo.key); fflush(stdout); } cli->cursor_position++; + } else { - cli_putc(cli, CliSymbolAsciiBell); + cli_putc(cli, CliKeyBell); } + + fflush(stdout); } void cli_add_command( diff --git a/applications/services/cli/cli.h b/applications/services/cli/cli.h index bb84670a7..c91f71c44 100644 --- a/applications/services/cli/cli.h +++ b/applications/services/cli/cli.h @@ -10,26 +10,12 @@ extern "C" { #endif -typedef enum { - CliSymbolAsciiSOH = 0x01, - CliSymbolAsciiETX = 0x03, - CliSymbolAsciiEOT = 0x04, - CliSymbolAsciiBell = 0x07, - CliSymbolAsciiBackspace = 0x08, - CliSymbolAsciiTab = 0x09, - CliSymbolAsciiLF = 0x0A, - CliSymbolAsciiCR = 0x0D, - CliSymbolAsciiEsc = 0x1B, - CliSymbolAsciiUS = 0x1F, - CliSymbolAsciiSpace = 0x20, - CliSymbolAsciiDel = 0x7F, -} CliSymbols; - typedef enum { CliCommandFlagDefault = 0, /**< Default, loader lock is used */ CliCommandFlagParallelSafe = (1 << 0), /**< Safe to run in parallel with other apps, loader lock is not used */ CliCommandFlagInsomniaSafe = (1 << 1), /**< Safe to run with insomnia mode on */ + CliCommandFlagHidden = (1 << 2), /**< Not shown in `help` */ } CliCommandFlag; #define RECORD_CLI "cli" diff --git a/applications/services/cli/cli_ansi.c b/applications/services/cli/cli_ansi.c new file mode 100644 index 000000000..d27c20bad --- /dev/null +++ b/applications/services/cli/cli_ansi.c @@ -0,0 +1,76 @@ +#include "cli_ansi.h" + +/** + * @brief Converts a single character representing a special key into the enum + * representation + */ +static CliKey cli_ansi_key_from_mnemonic(char c) { + switch(c) { + case 'A': + return CliKeyUp; + case 'B': + return CliKeyDown; + case 'C': + return CliKeyRight; + case 'D': + return CliKeyLeft; + case 'F': + return CliKeyEnd; + case 'H': + return CliKeyHome; + default: + return CliKeyUnrecognized; + } +} + +CliKeyCombo cli_read_ansi_key_combo(Cli* cli) { + char ch = cli_getc(cli); + + if(ch != CliKeyEsc) + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = ch, + }; + + ch = cli_getc(cli); + + // ESC ESC -> ESC + if(ch == '\e') + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = '\e', + }; + + // ESC -> Alt + + if(ch != '[') + return (CliKeyCombo){ + .modifiers = CliModKeyAlt, + .key = cli_getc(cli), + }; + + ch = cli_getc(cli); + + // ESC [ 1 + if(ch == '1') { + // ESC [ 1 ; + if(cli_getc(cli) == ';') { + CliModKey modifiers = (cli_getc(cli) - '0'); // convert following digit to a number + modifiers &= ~1; + return (CliKeyCombo){ + .modifiers = modifiers, + .key = cli_ansi_key_from_mnemonic(cli_getc(cli)), + }; + } + + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = CliKeyUnrecognized, + }; + } + + // ESC [ + return (CliKeyCombo){ + .modifiers = CliModKeyNo, + .key = cli_ansi_key_from_mnemonic(ch), + }; +} diff --git a/applications/services/cli/cli_ansi.h b/applications/services/cli/cli_ansi.h new file mode 100644 index 000000000..110d8a5fc --- /dev/null +++ b/applications/services/cli/cli_ansi.h @@ -0,0 +1,94 @@ +#pragma once + +#include "cli.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ANSI_RESET "\e[0m" +#define ANSI_BOLD "\e[1m" +#define ANSI_FAINT "\e[2m" + +#define ANSI_FG_BLACK "\e[30m" +#define ANSI_FG_RED "\e[31m" +#define ANSI_FG_GREEN "\e[32m" +#define ANSI_FG_YELLOW "\e[33m" +#define ANSI_FG_BLUE "\e[34m" +#define ANSI_FG_MAGENTA "\e[35m" +#define ANSI_FG_CYAN "\e[36m" +#define ANSI_FG_WHITE "\e[37m" +#define ANSI_FG_BR_BLACK "\e[90m" +#define ANSI_FG_BR_RED "\e[91m" +#define ANSI_FG_BR_GREEN "\e[92m" +#define ANSI_FG_BR_YELLOW "\e[93m" +#define ANSI_FG_BR_BLUE "\e[94m" +#define ANSI_FG_BR_MAGENTA "\e[95m" +#define ANSI_FG_BR_CYAN "\e[96m" +#define ANSI_FG_BR_WHITE "\e[97m" + +#define ANSI_BG_BLACK "\e[40m" +#define ANSI_BG_RED "\e[41m" +#define ANSI_BG_GREEN "\e[42m" +#define ANSI_BG_YELLOW "\e[43m" +#define ANSI_BG_BLUE "\e[44m" +#define ANSI_BG_MAGENTA "\e[45m" +#define ANSI_BG_CYAN "\e[46m" +#define ANSI_BG_WHITE "\e[47m" +#define ANSI_BG_BR_BLACK "\e[100m" +#define ANSI_BG_BR_RED "\e[101m" +#define ANSI_BG_BR_GREEN "\e[102m" +#define ANSI_BG_BR_YELLOW "\e[103m" +#define ANSI_BG_BR_BLUE "\e[104m" +#define ANSI_BG_BR_MAGENTA "\e[105m" +#define ANSI_BG_BR_CYAN "\e[106m" +#define ANSI_BG_BR_WHITE "\e[107m" + +#define ANSI_FLIPPER_BRAND_ORANGE "\e[38;2;255;130;0m" + +typedef enum { + CliKeyUnrecognized = 0, + + CliKeySOH = 0x01, + CliKeyETX = 0x03, + CliKeyEOT = 0x04, + CliKeyBell = 0x07, + CliKeyBackspace = 0x08, + CliKeyTab = 0x09, + CliKeyLF = 0x0A, + CliKeyCR = 0x0D, + CliKeyETB = 0x17, + CliKeyEsc = 0x1B, + CliKeyUS = 0x1F, + CliKeySpace = 0x20, + CliKeyDEL = 0x7F, + + CliKeySpecial = 0x80, + CliKeyLeft, + CliKeyRight, + CliKeyUp, + CliKeyDown, + CliKeyHome, + CliKeyEnd, +} CliKey; + +typedef enum { + CliModKeyNo = 0, + CliModKeyAlt = 2, + CliModKeyCtrl = 4, + CliModKeyMeta = 8, +} CliModKey; + +typedef struct { + CliModKey modifiers; + CliKey key; +} CliKeyCombo; + +/** + * @brief Reads a key or key combination + */ +CliKeyCombo cli_read_ansi_key_combo(Cli* cli); + +#ifdef __cplusplus +} +#endif diff --git a/applications/services/cli/cli_commands.c b/applications/services/cli/cli_commands.c index 01ce90dbb..6b53c1626 100644 --- a/applications/services/cli/cli_commands.c +++ b/applications/services/cli/cli_commands.c @@ -1,5 +1,6 @@ #include "cli_commands.h" #include "cli_command_gpio.h" +#include "cli_ansi.h" #include #include @@ -10,6 +11,7 @@ #include #include #include +#include // Close to ISO, `date +'%Y-%m-%d %H:%M:%S %u'` #define CLI_DATE_FORMAT "%.4d-%.2d-%.2d %.2d:%.2d:%.2d %d" @@ -52,35 +54,194 @@ void cli_command_info(Cli* cli, FuriString* args, void* context) { } } -void cli_command_help(Cli* cli, FuriString* args, void* context) { +// Lil Easter egg :> +void cli_command_neofetch(Cli* cli, FuriString* args, void* context) { + UNUSED(cli); UNUSED(args); UNUSED(context); + + static const char* const neofetch_logo[] = { + " _.-------.._ -,", + " .-\"```\"--..,,_/ /`-, -, \\ ", + " .:\" /:/ /'\\ \\ ,_..., `. | |", + " / ,----/:/ /`\\ _\\~`_-\"` _;", + " ' / /`\"\"\"'\\ \\ \\.~`_-' ,-\"'/ ", + " | | | 0 | | .-' ,/` /", + " | ,..\\ \\ ,.-\"` ,/` /", + "; : `/`\"\"\\` ,/--==,/-----,", + "| `-...| -.___-Z:_______J...---;", + ": ` _-'", + }; +#define NEOFETCH_COLOR ANSI_FLIPPER_BRAND_ORANGE + + // Determine logo parameters + size_t logo_height = COUNT_OF(neofetch_logo), logo_width = 0; + for(size_t i = 0; i < logo_height; i++) + logo_width = MAX(logo_width, strlen(neofetch_logo[i])); + logo_width += 4; // space between logo and info + + // Format hostname delimiter + const size_t size_of_hostname = 4 + strlen(furi_hal_version_get_name_ptr()); + char delimiter[64]; + memset(delimiter, '-', size_of_hostname); + delimiter[size_of_hostname] = '\0'; + + // Get heap info + size_t heap_total = memmgr_get_total_heap(); + size_t heap_used = heap_total - memmgr_get_free_heap(); + uint16_t heap_percent = (100 * heap_used) / heap_total; + + // Get storage info + Storage* storage = furi_record_open(RECORD_STORAGE); + uint64_t ext_total, ext_free, ext_used, ext_percent; + storage_common_fs_info(storage, "/ext", &ext_total, &ext_free); + ext_used = ext_total - ext_free; + ext_percent = (100 * ext_used) / ext_total; + ext_used /= 1024 * 1024; + ext_total /= 1024 * 1024; + furi_record_close(RECORD_STORAGE); + + // Get battery info + uint16_t charge_percent = furi_hal_power_get_pct(); + const char* charge_state; + if(furi_hal_power_is_charging()) { + if((charge_percent < 100) && (!furi_hal_power_is_charging_done())) { + charge_state = "charging"; + } else { + charge_state = "charged"; + } + } else { + charge_state = "discharging"; + } + + // Get misc info + uint32_t uptime = furi_get_tick() / furi_kernel_get_tick_frequency(); + const Version* version = version_get(); + uint16_t major, minor; + furi_hal_info_get_api_version(&major, &minor); + + // Print ASCII art with info + const size_t info_height = 16; + for(size_t i = 0; i < MAX(logo_height, info_height); i++) { + printf(NEOFETCH_COLOR "%-*s", logo_width, (i < logo_height) ? neofetch_logo[i] : ""); + switch(i) { + case 0: // you@ + printf("you" ANSI_RESET "@" NEOFETCH_COLOR "%s", furi_hal_version_get_name_ptr()); + break; + case 1: // delimiter + printf(ANSI_RESET "%s", delimiter); + break; + case 2: // OS: FURI (SDK .) + printf( + "OS" ANSI_RESET ": FURI %s %s %s %s (SDK %hu.%hu)", + version_get_version(version), + version_get_gitbranch(version), + version_get_version(version), + version_get_githash(version), + major, + minor); + break; + case 3: // Host: + printf( + "Host" ANSI_RESET ": %s %s", + furi_hal_version_get_model_code(), + furi_hal_version_get_device_name_ptr()); + break; + case 4: // Kernel: FreeRTOS .. + printf( + "Kernel" ANSI_RESET ": FreeRTOS %d.%d.%d", + tskKERNEL_VERSION_MAJOR, + tskKERNEL_VERSION_MINOR, + tskKERNEL_VERSION_BUILD); + break; + case 5: // Uptime: ?h?m?s + printf( + "Uptime" ANSI_RESET ": %luh%lum%lus", + uptime / 60 / 60, + uptime / 60 % 60, + uptime % 60); + break; + case 6: // ST7567 128x64 @ 1 bpp in 1.4" + printf("Display" ANSI_RESET ": ST7567 128x64 @ 1 bpp in 1.4\""); + break; + case 7: // DE: GuiSrv + printf("DE" ANSI_RESET ": GuiSrv"); + break; + case 8: // Shell: CliSrv + printf("Shell" ANSI_RESET ": CliSrv"); + break; + case 9: // CPU: STM32WB55RG @ 64 MHz + printf("CPU" ANSI_RESET ": STM32WB55RG @ 64 MHz"); + break; + case 10: // Memory: / B (??%) + printf( + "Memory" ANSI_RESET ": %zu / %zu B (%hu%%)", heap_used, heap_total, heap_percent); + break; + case 11: // Disk (/ext): / MiB (??%) + printf( + "Disk (/ext)" ANSI_RESET ": %llu / %llu MiB (%llu%%)", + ext_used, + ext_total, + ext_percent); + break; + case 12: // Battery: ??% () + printf("Battery" ANSI_RESET ": %hu%% (%s)" ANSI_RESET, charge_percent, charge_state); + break; + case 13: // empty space + break; + case 14: // Colors (line 1) + for(size_t j = 30; j <= 37; j++) + printf("\e[%dm███", j); + break; + case 15: // Colors (line 2) + for(size_t j = 90; j <= 97; j++) + printf("\e[%dm███", j); + break; + default: + break; + } + printf("\r\n"); + } + printf(ANSI_RESET); +#undef NEOFETCH_COLOR +} + +void cli_command_help(Cli* cli, FuriString* args, void* context) { + UNUSED(context); printf("Commands available:"); - // Command count - const size_t commands_count = CliCommandTree_size(cli->commands); - const size_t commands_count_mid = commands_count / 2 + commands_count % 2; + // Count non-hidden commands + CliCommandTree_it_t it_count; + CliCommandTree_it(it_count, cli->commands); + size_t commands_count = 0; + while(!CliCommandTree_end_p(it_count)) { + if(!(CliCommandTree_cref(it_count)->value_ptr->flags & CliCommandFlagHidden)) + commands_count++; + CliCommandTree_next(it_count); + } - // Use 2 iterators from start and middle to show 2 columns - CliCommandTree_it_t it_left; - CliCommandTree_it(it_left, cli->commands); - CliCommandTree_it_t it_right; - CliCommandTree_it(it_right, cli->commands); - for(size_t i = 0; i < commands_count_mid; i++) - CliCommandTree_next(it_right); + // Create iterators starting at different positions + const size_t columns = 3; + const size_t commands_per_column = (commands_count / columns) + (commands_count % columns); + CliCommandTree_it_t iterators[columns]; + for(size_t c = 0; c < columns; c++) { + CliCommandTree_it(iterators[c], cli->commands); + for(size_t i = 0; i < c * commands_per_column; i++) + CliCommandTree_next(iterators[c]); + } - // Iterate throw tree - for(size_t i = 0; i < commands_count_mid; i++) { + // Print commands + for(size_t r = 0; r < commands_per_column; r++) { printf("\r\n"); - // Left Column - if(!CliCommandTree_end_p(it_left)) { - printf("%-30s", furi_string_get_cstr(*CliCommandTree_ref(it_left)->key_ptr)); - CliCommandTree_next(it_left); - } - // Right Column - if(!CliCommandTree_end_p(it_right)) { - printf("%s", furi_string_get_cstr(*CliCommandTree_ref(it_right)->key_ptr)); - CliCommandTree_next(it_right); + + for(size_t c = 0; c < columns; c++) { + if(!CliCommandTree_end_p(iterators[c])) { + const CliCommandTree_itref_t* item = CliCommandTree_cref(iterators[c]); + if(!(item->value_ptr->flags & CliCommandFlagHidden)) { + printf("%-30s", furi_string_get_cstr(*item->key_ptr)); + } + CliCommandTree_next(iterators[c]); + } } } @@ -401,16 +562,18 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { int interval = 1000; args_read_int_and_trim(args, &interval); + if(interval) printf("\e[2J\e[?25l"); // Clear display, hide cursor + FuriThreadList* thread_list = furi_thread_list_alloc(); while(!cli_cmd_interrupt_received(cli)) { uint32_t tick = furi_get_tick(); furi_thread_enumerate(thread_list); - if(interval) printf("\e[2J\e[0;0f"); // Clear display and return to 0 + if(interval) printf("\e[0;0f"); // Return to 0,0 uint32_t uptime = tick / furi_kernel_get_tick_frequency(); printf( - "Threads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\r\n", + "\rThreads: %zu, ISR Time: %0.2f%%, Uptime: %luh%lum%lus\e[0K\r\n", furi_thread_list_size(thread_list), (double)furi_thread_list_get_isr_time(thread_list), uptime / 60 / 60, @@ -418,14 +581,14 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { uptime % 60); printf( - "Heap: total %zu, free %zu, minimum %zu, max block %zu\r\n\r\n", + "\rHeap: total %zu, free %zu, minimum %zu, max block %zu\e[0K\r\n\r\n", memmgr_get_total_heap(), memmgr_get_free_heap(), memmgr_get_minimum_free_heap(), memmgr_heap_get_max_free_block()); printf( - "%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\r\n", + "\r%-17s %-20s %-10s %5s %12s %6s %10s %7s %5s\e[0K\r\n", "AppID", "Name", "State", @@ -439,7 +602,7 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { for(size_t i = 0; i < furi_thread_list_size(thread_list); i++) { const FuriThreadListItem* item = furi_thread_list_get_at(thread_list, i); printf( - "%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\r\n", + "\r%-17s %-20s %-10s %5d 0x%08lx %6lu %10lu %7zu %5.1f\e[0K\r\n", item->app_id, item->name, item->state, @@ -458,6 +621,8 @@ static void cli_command_top(Cli* cli, FuriString* args, void* context) { } } furi_thread_list_free(thread_list); + + if(interval) printf("\e[?25h"); // Show cursor } void cli_command_free(Cli* cli, FuriString* args, void* context) { @@ -509,6 +674,12 @@ void cli_commands_init(Cli* cli) { cli_add_command(cli, "!", CliCommandFlagParallelSafe, cli_command_info, (void*)true); cli_add_command(cli, "info", CliCommandFlagParallelSafe, cli_command_info, NULL); cli_add_command(cli, "device_info", CliCommandFlagParallelSafe, cli_command_info, (void*)true); + cli_add_command( + cli, + "neofetch", + CliCommandFlagParallelSafe | CliCommandFlagHidden, + cli_command_neofetch, + NULL); cli_add_command(cli, "src", CliCommandFlagParallelSafe, cli_command_src, NULL); cli_add_command(cli, "source", CliCommandFlagParallelSafe, cli_command_src, NULL); diff --git a/applications/services/crypto/crypto_cli.c b/applications/services/crypto/crypto_cli.c index 1af0ac39c..5ef704b8b 100644 --- a/applications/services/crypto/crypto_cli.c +++ b/applications/services/crypto/crypto_cli.c @@ -3,6 +3,7 @@ #include #include +#include void crypto_cli_print_usage(void) { printf("Usage:\r\n"); @@ -45,14 +46,14 @@ void crypto_cli_encrypt(Cli* cli, FuriString* args) { input = furi_string_alloc(); char c; while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); furi_string_cat(input, "\r\n"); } @@ -120,14 +121,14 @@ void crypto_cli_decrypt(Cli* cli, FuriString* args) { hex_input = furi_string_alloc(); char c; while(cli_read(cli, (uint8_t*)&c, 1) == 1) { - if(c == CliSymbolAsciiETX) { + if(c == CliKeyETX) { printf("\r\n"); break; } else if(c >= 0x20 && c < 0x7F) { putc(c, stdout); fflush(stdout); furi_string_push_back(hex_input, c); - } else if(c == CliSymbolAsciiCR) { + } else if(c == CliKeyCR) { printf("\r\n"); } } diff --git a/applications/services/dialogs/dialogs_module_file_browser.c b/applications/services/dialogs/dialogs_module_file_browser.c index 12a7439e6..603c27cff 100644 --- a/applications/services/dialogs/dialogs_module_file_browser.c +++ b/applications/services/dialogs/dialogs_module_file_browser.c @@ -54,11 +54,14 @@ bool dialogs_app_process_module_file_browser(const DialogsAppMessageDataFileBrow ret = file_browser_context->result; view_holder_set_view(view_holder, NULL); - view_holder_free(view_holder); file_browser_stop(file_browser); + file_browser_free(file_browser); + view_holder_free(view_holder); + api_lock_free(file_browser_context->lock); free(file_browser_context); + furi_record_close(RECORD_GUI); return ret; diff --git a/applications/services/gui/canvas.c b/applications/services/gui/canvas.c index 242f7842e..2aace5ca7 100644 --- a/applications/services/gui/canvas.c +++ b/applications/services/gui/canvas.c @@ -556,12 +556,10 @@ void canvas_draw_xbm( size_t height, const uint8_t* bitmap) { furi_check(canvas); - x += canvas->offset_x; - y += canvas->offset_y; - canvas_draw_u8g2_bitmap(&canvas->fb, x, y, width, height, bitmap, IconRotation0); + canvas_draw_xbm_ex(canvas, x, y, width, height, IconRotation0, bitmap); } -void canvas_draw_xbm_custom( +void canvas_draw_xbm_ex( Canvas* canvas, int32_t x, int32_t y, diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index f22f03875..cd4719b3f 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -298,16 +298,15 @@ 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_custom( +void canvas_draw_xbm_ex( Canvas* canvas, int32_t x, int32_t y, diff --git a/applications/services/gui/modules/text_input.c b/applications/services/gui/modules/text_input.c index c9a3b9301..58c53653a 100644 --- a/applications/services/gui/modules/text_input.c +++ b/applications/services/gui/modules/text_input.c @@ -24,6 +24,7 @@ typedef struct { const char* header; char* text_buffer; size_t text_buffer_size; + size_t minimum_length; bool clear_default_text; TextInputCallback callback; @@ -37,7 +38,6 @@ typedef struct { FuriString* validator_text; bool validator_message_visible; - size_t minimum_length; char extra_symbols[9]; bool cursor_select; size_t cursor_pos; diff --git a/applications/services/gui/modules/text_input.h b/applications/services/gui/modules/text_input.h index c12e8bbc1..7656a21bf 100644 --- a/applications/services/gui/modules/text_input.h +++ b/applications/services/gui/modules/text_input.h @@ -65,13 +65,18 @@ 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, void* callback_context); -void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length); - // Add up to 9 extra characters for symbol keyboard void text_input_add_extra_symbol(TextInput* text_input, char symbol); diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 4bef29350..fb3cdd4ef 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(); @@ -18,7 +24,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)); @@ -70,7 +76,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) { furi_message_queue_free(view_dispatcher->ascii_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); } @@ -98,6 +104,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; } @@ -119,11 +126,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 6b2db9b54..80d4d2f70 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/input/input_cli.c b/applications/services/input/input_cli.c index e6f82552b..10a09cd42 100644 --- a/applications/services/input/input_cli.c +++ b/applications/services/input/input_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include static void input_cli_usage(void) { @@ -73,12 +74,12 @@ static void input_cli_keyboard(Cli* cli, FuriString* args, FuriPubSub* event_pub FuriPubSub* ascii_pubsub = furi_record_open(RECORD_ASCII_EVENTS); while(cli_is_connected(cli)) { char in_chr = cli_getc(cli); - if(in_chr == CliSymbolAsciiETX) break; + if(in_chr == CliKeyETX) break; InputKey send_key = InputKeyMAX; uint8_t send_ascii = AsciiValueNUL; switch(in_chr) { - case CliSymbolAsciiEsc: // Escape code for arrows + case CliKeyEsc: // Escape code for arrows if(!cli_read(cli, (uint8_t*)&in_chr, 1) || in_chr != '[') break; if(!cli_read(cli, (uint8_t*)&in_chr, 1)) break; if(in_chr >= 'A' && in_chr <= 'D') { // Arrows = Dpad @@ -89,8 +90,8 @@ static void input_cli_keyboard(Cli* cli, FuriString* args, FuriPubSub* event_pub } } break; - case CliSymbolAsciiBackspace: // (minicom) Backspace = Back - case CliSymbolAsciiDel: // (putty/picocom) Backspace = Back + case CliKeyBackspace: // (minicom) Backspace = Back + case CliKeyDEL: // (putty/picocom) Backspace = Back if(hold) { send_key = InputKeyBack; } else { @@ -104,14 +105,14 @@ static void input_cli_keyboard(Cli* cli, FuriString* args, FuriPubSub* event_pub send_ascii = AsciiValueESC; } break; - case CliSymbolAsciiCR: // Enter = Ok + case CliKeyCR: // Enter = Ok if(hold) { send_key = InputKeyOk; } else { send_ascii = AsciiValueCR; } break; - case CliSymbolAsciiSpace: // Space = Toggle hold next key + case CliKeySpace: // Space = Toggle hold next key if(hold) { send_ascii = ' '; } else { diff --git a/applications/services/rpc/rpc.c b/applications/services/rpc/rpc.c index 70c8ebfd9..1cd5caa65 100644 --- a/applications/services/rpc/rpc.c +++ b/applications/services/rpc/rpc.c @@ -69,7 +69,7 @@ static RpcSystemCallbacks rpc_systems[] = { struct RpcSession { Rpc* rpc; - FuriThreadId thread_id; + FuriThread* thread; RpcHandlerDict_t handlers; FuriStreamBuffer* stream; @@ -175,7 +175,7 @@ size_t rpc_session_feed( size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout); - furi_thread_flags_set(session->thread_id, RpcEvtNewData); + furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData); return bytes_sent; } @@ -223,7 +223,7 @@ bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { break; } else { /* Save disconnect flag and continue reading buffer */ - furi_thread_flags_set(session->thread_id, RpcEvtDisconnect); + furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); } } else if(flags & RpcEvtNewData) { // Just wake thread up @@ -350,32 +350,37 @@ static int32_t rpc_session_worker(void* context) { return 0; } -static void rpc_session_thread_release_callback( - FuriThread* thread, - FuriThreadState thread_state, - void* context) { - if(thread_state == FuriThreadStateStopped) { - RpcSession* session = (RpcSession*)context; +static void rpc_session_thread_pending_callback(void* context, uint32_t arg) { + UNUSED(arg); + RpcSession* session = (RpcSession*)context; - for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { - if(rpc_systems[i].free) { - (rpc_systems[i].free)(session->system_contexts[i]); - } + for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { + if(rpc_systems[i].free) { + (rpc_systems[i].free)(session->system_contexts[i]); } - free(session->system_contexts); - free(session->decoded_message); - RpcHandlerDict_clear(session->handlers); - furi_stream_buffer_free(session->stream); + } + free(session->system_contexts); + free(session->decoded_message); + RpcHandlerDict_clear(session->handlers); + furi_stream_buffer_free(session->stream); - furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); - if(session->terminated_callback) { - session->terminated_callback(session->context); - } - furi_mutex_release(session->callbacks_mutex); + furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); + if(session->terminated_callback) { + session->terminated_callback(session->context); + } + furi_mutex_release(session->callbacks_mutex); - furi_mutex_free(session->callbacks_mutex); - furi_thread_free(thread); - free(session); + furi_mutex_free(session->callbacks_mutex); + furi_thread_join(session->thread); + furi_thread_free(session->thread); + free(session); +} + +static void + rpc_session_thread_state_callback(FuriThread* thread, FuriThreadState state, void* context) { + UNUSED(thread); + if(state == FuriThreadStateStopped) { + furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0); } } @@ -411,14 +416,12 @@ RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) { }; rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler); - FuriThread* thread = - furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session); - session->thread_id = furi_thread_get_id(thread); + session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session); - furi_thread_set_state_context(thread, session); - furi_thread_set_state_callback(thread, rpc_session_thread_release_callback); + furi_thread_set_state_context(session->thread, session); + furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback); - furi_thread_start(thread); + furi_thread_start(session->thread); rpc->sessions_count++; @@ -434,7 +437,7 @@ void rpc_session_close(RpcSession* session) { rpc_session_set_send_bytes_callback(session, NULL); rpc_session_set_close_callback(session, NULL); rpc_session_set_buffer_is_empty_callback(session, NULL); - furi_thread_flags_set(session->thread_id, RpcEvtDisconnect); + furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); } void rpc_on_system_start(void* p) { diff --git a/applications/services/storage/storage.h b/applications/services/storage/storage.h index 931b39c91..6d707fa9d 100644 --- a/applications/services/storage/storage.h +++ b/applications/services/storage/storage.h @@ -404,7 +404,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); @@ -452,7 +452,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/services/storage/storage_cli.c b/applications/services/storage/storage_cli.c index 67ee3ecb4..88584b39c 100644 --- a/applications/services/storage/storage_cli.c +++ b/applications/services/storage/storage_cli.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -224,7 +225,7 @@ static void storage_cli_write(Cli* cli, FuriString* path, FuriString* args) { while(true) { uint8_t symbol = cli_getc(cli); - if(symbol == CliSymbolAsciiETX) { + if(symbol == CliKeyETX) { size_t write_size = read_index % buffer_size; if(write_size > 0) { diff --git a/applications/system/hid_app/views/hid_mouse_clicker.c b/applications/system/hid_app/views/hid_mouse_clicker.c index 0bb815249..e289b7179 100644 --- a/applications/system/hid_app/views/hid_mouse_clicker.c +++ b/applications/system/hid_app/views/hid_mouse_clicker.c @@ -7,7 +7,7 @@ #define TAG "HidMouseClicker" #define DEFAULT_CLICK_RATE 1 -#define MAXIMUM_CLICK_RATE 60 +#define MAXIMUM_CLICK_RATE 100 struct HidMouseClicker { View* view; @@ -34,7 +34,9 @@ static void hid_mouse_clicker_start_or_restart_timer(void* context) { HidMouseClickerModel * model, { furi_timer_start( - hid_mouse_clicker->timer, furi_kernel_get_tick_frequency() / model->rate); + hid_mouse_clicker->timer, + furi_kernel_get_tick_frequency() / + ((model->rate) ? model->rate : MAXIMUM_CLICK_RATE)); }, true); } @@ -75,7 +77,11 @@ static void hid_mouse_clicker_draw_callback(Canvas* canvas, void* context) { // Clicks/s char label[20]; - snprintf(label, sizeof(label), "%d clicks/s", model->rate); + if(model->rate) { + snprintf(label, sizeof(label), "%d clicks/s", model->rate); + } else { + snprintf(label, sizeof(label), "max clicks/s"); + } elements_multiline_text_aligned(canvas, 28, 37, AlignCenter, AlignBottom, label); canvas_draw_icon(canvas, 25, 20, &I_ButtonUp_7x4); @@ -139,7 +145,7 @@ static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) { consumed = true; break; case InputKeyDown: - if(model->rate > 1) { + if(model->rate > 0) { model->rate--; } rate_changed = true; diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 8c38b6c0d..fe25514e7 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -37,11 +37,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( @@ -68,14 +127,6 @@ App( sources=["modules/js_serial.c"], ) -App( - appid="js_storage", - apptype=FlipperAppType.PLUGIN, - entry_point="js_storage_ep", - requires=["js_app"], - sources=["modules/js_storage.c"], -) - App( appid="js_usbdisk", apptype=FlipperAppType.PLUGIN, @@ -85,11 +136,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( @@ -125,19 +176,11 @@ App( ) App( - appid="js_gpio", + appid="js_storage", apptype=FlipperAppType.PLUGIN, - entry_point="js_gpio_ep", + entry_point="js_storage_ep", requires=["js_app"], - sources=["modules/js_gpio.c"], -) - -App( - appid="js_textbox", - apptype=FlipperAppType.PLUGIN, - entry_point="js_textbox_ep", - requires=["js_app"], - sources=["modules/js_textbox.c"], + sources=["modules/js_storage.c"], ) App( diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/adc.js b/applications/system/js_app/examples/apps/Scripts/Examples/adc.js index 0506d348c..96d4032c4 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/adc.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/adc.js @@ -17,7 +17,7 @@ while (true) { let pa7_value = gpio.readAnalog("PA7"); let pa6_value = gpio.readAnalog("PA6"); let pa4_value = gpio.readAnalog("PA4"); - print("A7: " + to_string(pa7_value) + " A6: " + to_string(pa6_value) + " A4: " + to_string(pa4_value)); + print("A7: " + toString(pa7_value) + " A6: " + toString(pa6_value) + " A4: " + toString(pa4_value)); delay(100); if (pa7_value === pa6_value * 2) { break; diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js b/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js index be94a64d2..472d48c25 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/badusb_demo.js @@ -1,48 +1,73 @@ 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"); + +let views = { + dialog: dialog.makeWith({ + header: "BadUSB demo", + text: "Press OK to start", + center: "Start", + }), +}; badusb.setup({ vid: 0xAAAA, pid: 0xBBBB, - mfr_name: "Flipper", - prod_name: "Zero", - layout_path: "/ext/badusb/assets/layouts/en-US.kl" + mfrName: "Flipper", + prodName: "Zero", + layoutPath: "/ext/badusb/assets/layouts/en-US.kl" }); -dialog.message("BadUSB demo", "Press OK to start"); -if (badusb.isConnected()) { - notify.blink("green", "short"); - print("USB is connected"); +eventLoop.subscribe(views.dialog.input, function (_sub, button, eventLoop, gui) { + if (button !== "center") + return; - badusb.println("Hello, world!"); + gui.viewDispatcher.sendTo("back"); - badusb.press("CTRL", "a"); - badusb.press("CTRL", "c"); - badusb.press("DOWN"); - delay(1000); - badusb.press("CTRL", "v"); - delay(1000); - badusb.press("CTRL", "v"); + if (badusb.isConnected()) { + notify.blink("green", "short"); + print("USB is connected"); - badusb.println("1234", 200); + badusb.println("Hello, world!"); - badusb.println("Flipper Model: " + flipper.getModel()); - badusb.println("Flipper Name: " + flipper.getName()); - badusb.println("Battery level: " + to_string(flipper.getBatteryCharge()) + "%"); + badusb.press("CTRL", "a"); + badusb.press("CTRL", "c"); + badusb.press("DOWN"); + delay(1000); + badusb.press("CTRL", "v"); + delay(1000); + badusb.press("CTRL", "v"); - // Alt+Numpad method works only on Windows!!! - badusb.altPrintln("This was printed with Alt+Numpad method!"); + badusb.println("1234", 200); - // There's also badusb.print() and badusb.altPrint() - // which don't add the return at the end + 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(); -} + // Alt+Numpad method works only on Windows!!! + badusb.altPrintln("This was printed with Alt+Numpad method!"); + + // There's also badusb.print() and badusb.altPrint() + // which don't add the return at the end + + 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(); // Optional, but allows to interchange with usbdisk badusb.quit(); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js b/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js index 53983a745..9a30ad151 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/blebeacon.js @@ -45,7 +45,7 @@ function sendRandomModelAdvertisement() { blebeacon.start(); - print("Sent data for model ID " + to_string(model)); + print("Sent data for model ID " + toString(model)); currentIndex = (currentIndex + 1) % watchValues.length; diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/delay.js b/applications/system/js_app/examples/apps/Scripts/Examples/delay.js index 9f64abee8..5d8fbe422 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/delay.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/dialog.js b/applications/system/js_app/examples/apps/Scripts/Examples/dialog.js deleted file mode 100644 index 4c5b0af20..000000000 --- a/applications/system/js_app/examples/apps/Scripts/Examples/dialog.js +++ /dev/null @@ -1,22 +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: "Files", - button_center: "OK" -}); - -let result2 = dialog.custom(dialog_params); -if (result2 === "") { - print("Back is pressed"); -} else if (result2 === "Files") { - let result3 = dialog.pickFile("/ext", "*"); - print("Selected", result3); -} else { - print(result2, "is pressed"); -} diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/keyboard.js b/applications/system/js_app/examples/apps/Scripts/Examples/keyboard.js index 2b01418de..ecaa1143f 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/keyboard.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/keyboard.js @@ -17,7 +17,7 @@ if (result !== undefined) { result = "0x"; for (let i = 0; i < data.byteLength; i++) { if (data[i] < 0x10) result += "0"; - result += to_hex_string(data[i]); + result += toString(data[i], 16); } } print("Got data:", result); \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/load.js b/applications/system/js_app/examples/apps/Scripts/Examples/load.js index dfb110ca5..813619741 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/load.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/load_api.js b/applications/system/js_app/examples/apps/Scripts/Examples/load_api.js index ad3b26e15..80712c40b 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/load_api.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/math.js b/applications/system/js_app/examples/apps/Scripts/Examples/math.js index c5a0bf18d..63527ea67 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/math.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/notify.js b/applications/system/js_app/examples/apps/Scripts/Examples/notify.js index 20f60c732..dd471650c 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/notify.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/stringutils.js b/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js index 51781328d..76a16653e 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/stringutils.js @@ -1,6 +1,6 @@ let sampleText = "Hello, World!"; -let lengthOfText = "Length of text: " + to_string(sampleText.length); +let lengthOfText = "Length of text: " + toString(sampleText.length); print(lengthOfText); let start = 7; @@ -9,7 +9,7 @@ let substringResult = sampleText.slice(start, end); print(substringResult); let searchStr = "World"; -let result2 = to_string(sampleText.indexOf(searchStr)); +let result2 = toString(sampleText.indexOf(searchStr)); print(result2); let upperCaseText = "Text in upper case: " + to_upper_case(sampleText); diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/submenu.js b/applications/system/js_app/examples/apps/Scripts/Examples/submenu.js deleted file mode 100644 index 245551309..000000000 --- a/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/textbox.js b/applications/system/js_app/examples/apps/Scripts/Examples/textbox.js deleted file mode 100644 index 6caf37234..000000000 --- a/applications/system/js_app/examples/apps/Scripts/Examples/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/Examples/uart_echo.js b/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js index 1cc0d8e62..e0bc1c5df 100644 --- a/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js +++ b/applications/system/js_app/examples/apps/Scripts/Examples/uart_echo.js @@ -6,7 +6,7 @@ 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)); } } 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/js_app.c b/applications/system/js_app/js_app.c index f058d095b..27084b3b5 100644 --- a/applications/system/js_app/js_app.c +++ b/applications/system/js_app/js_app.c @@ -114,7 +114,7 @@ int32_t js_app(void* arg) { FuriString* start_text = furi_string_alloc_printf("Running %s", furi_string_get_cstr(name)); console_view_print(app->console_view, furi_string_get_cstr(start_text)); - console_view_print(app->console_view, "------------"); + console_view_print(app->console_view, "-------------"); furi_string_free(name); furi_string_free(start_text); diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 9ab6cb140..38ff46f75 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -1,7 +1,11 @@ #include #include "js_modules.h" -#include +#include + #include "modules/js_flipper.h" +#ifdef FW_CFG_unit_tests +#include "modules/js_tests.h" +#endif #define TAG "JS modules" @@ -9,54 +13,72 @@ #define MODULES_PATH "/ext/apps_data/js_app/plugins" typedef struct { - JsModeConstructor create; - JsModeDestructor destroy; + FuriString* name; + const JsModuleConstructor create; + const JsModuleDestructor destroy; void* context; } JsModuleData; -DICT_DEF2(JsModuleDict, FuriString*, FURI_STRING_OPLIST, JsModuleData, M_POD_OPLIST); +// not using: +// - a dict because ordering is required +// - a bptree because it forces a sorted ordering +// - an rbtree because i deemed it more tedious to implement, and with the +// amount of modules in use (under 10 in the overwhelming majority of cases) +// i bet it's going to be slower than a plain array +ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST); +#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray) static const JsModuleDescriptor modules_builtin[] = { - {"flipper", js_flipper_create, NULL}, + {"flipper", js_flipper_create, NULL, NULL}, +#ifdef FW_CFG_unit_tests + {"tests", js_tests_create, NULL, NULL}, +#endif }; struct JsModules { struct mjs* mjs; - JsModuleDict_t module_dict; + JsModuleArray_t modules; PluginManager* plugin_manager; + CompositeApiResolver* resolver; }; JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver) { JsModules* modules = malloc(sizeof(JsModules)); modules->mjs = mjs; - JsModuleDict_init(modules->module_dict); + JsModuleArray_init(modules->modules); modules->plugin_manager = plugin_manager_alloc( PLUGIN_APP_ID, PLUGIN_API_VERSION, composite_api_resolver_get(resolver)); + modules->resolver = resolver; + return modules; } -void js_modules_destroy(JsModules* modules) { - JsModuleDict_it_t it; - for(JsModuleDict_it(it, modules->module_dict); !JsModuleDict_end_p(it); - JsModuleDict_next(it)) { - const JsModuleDict_itref_t* module_itref = JsModuleDict_cref(it); - if(module_itref->value.destroy) { - module_itref->value.destroy(module_itref->value.context); +void js_modules_destroy(JsModules* instance) { + for + M_EACH(module, instance->modules, JsModuleArray_t) { + FURI_LOG_T(TAG, "Tearing down %s", furi_string_get_cstr(module->name)); + if(module->destroy) module->destroy(module->context); + furi_string_free(module->name); } - } - plugin_manager_free(modules->plugin_manager); - JsModuleDict_clear(modules->module_dict); - free(modules); + plugin_manager_free(instance->plugin_manager); + JsModuleArray_clear(instance->modules); + free(instance); +} + +JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) { + for + M_EACH(module, instance->modules, JsModuleArray_t) { + if(furi_string_cmp_str(module->name, name) == 0) return module; + } + return NULL; } mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) { - FuriString* module_name = furi_string_alloc_set_str(name); // Check if module is already installed - JsModuleData* module_inst = JsModuleDict_get(modules->module_dict, module_name); + JsModuleData* module_inst = js_find_loaded_module(modules, name); if(module_inst) { //-V547 - furi_string_free(module_name); mjs_prepend_errorf( modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module is already installed", name); return MJS_UNDEFINED; @@ -73,8 +95,11 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le if(strncmp(name, modules_builtin[i].name, name_compare_len) == 0) { JsModuleData module = { - .create = modules_builtin[i].create, .destroy = modules_builtin[i].destroy}; - JsModuleDict_set_at(modules->module_dict, module_name, module); + .create = modules_builtin[i].create, + .destroy = modules_builtin[i].destroy, + .name = furi_string_alloc_set_str(name), + }; + JsModuleArray_push_at(modules->modules, 0, module); module_found = true; FURI_LOG_I(TAG, "Using built-in module %s", name); break; @@ -83,39 +108,57 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le // External module load if(!module_found) { + FuriString* deslashed_name = furi_string_alloc_set_str(name); + furi_string_replace_all_str(deslashed_name, "/", "__"); FuriString* module_path = furi_string_alloc(); - furi_string_printf(module_path, "%s/js_%s.fal", MODULES_PATH, name); - FURI_LOG_I(TAG, "Loading external module %s", furi_string_get_cstr(module_path)); + furi_string_printf( + module_path, "%s/js_%s.fal", MODULES_PATH, furi_string_get_cstr(deslashed_name)); + FURI_LOG_I( + TAG, "Loading external module %s from %s", name, furi_string_get_cstr(module_path)); do { uint32_t plugin_cnt_last = plugin_manager_get_count(modules->plugin_manager); PluginManagerError load_error = plugin_manager_load_single( modules->plugin_manager, furi_string_get_cstr(module_path)); if(load_error != PluginManagerErrorNone) { + FURI_LOG_E( + TAG, + "Module %s load error. It may depend on other modules that are not yet loaded.", + name); break; } const JsModuleDescriptor* plugin = plugin_manager_get_ep(modules->plugin_manager, plugin_cnt_last); furi_assert(plugin); - if(strncmp(name, plugin->name, name_len) != 0) { - FURI_LOG_E(TAG, "Module name missmatch %s", plugin->name); + if(furi_string_cmp_str(deslashed_name, plugin->name) != 0) { + FURI_LOG_E(TAG, "Module name mismatch %s", plugin->name); break; } - JsModuleData module = {.create = plugin->create, .destroy = plugin->destroy}; - JsModuleDict_set_at(modules->module_dict, module_name, module); + JsModuleData module = { + .create = plugin->create, + .destroy = plugin->destroy, + .name = furi_string_alloc_set_str(name), + }; + JsModuleArray_push_at(modules->modules, 0, module); + + if(plugin->api_interface) { + FURI_LOG_I(TAG, "Added module API to composite resolver: %s", plugin->name); + composite_api_resolver_add(modules->resolver, plugin->api_interface); + } module_found = true; } while(0); furi_string_free(module_path); + furi_string_free(deslashed_name); } // Run module constructor mjs_val_t module_object = MJS_UNDEFINED; if(module_found) { - module_inst = JsModuleDict_get(modules->module_dict, module_name); + module_inst = js_find_loaded_module(modules, name); furi_assert(module_inst); if(module_inst->create) { //-V779 - module_inst->context = module_inst->create(modules->mjs, &module_object); + module_inst->context = module_inst->create(modules->mjs, &module_object, modules); } } @@ -123,7 +166,12 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le mjs_prepend_errorf(modules->mjs, MJS_BAD_ARGS_ERROR, "\"%s\" module load fail", name); } - furi_string_free(module_name); - return module_object; } + +void* js_module_get(JsModules* modules, const char* name) { + FuriString* module_name = furi_string_alloc_set_str(name); + JsModuleData* module_inst = js_find_loaded_module(modules, name); + furi_string_free(module_name); + return module_inst ? module_inst->context : NULL; +} diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 77e50786f..788715872 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -1,4 +1,6 @@ #pragma once + +#include #include "js_thread_i.h" #include #include @@ -7,19 +9,269 @@ #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 -typedef void* (*JsModeConstructor)(struct mjs* mjs, mjs_val_t* object); -typedef void (*JsModeDestructor)(void* inst); +/** + * @brief Returns the foreign pointer in `obj["_"]` + */ +#define JS_GET_INST(mjs, obj) mjs_get_ptr(mjs, mjs_get(mjs, obj, INST_PROP_NAME, ~0)) +/** + * @brief Returns the foreign pointer in `this["_"]` + */ +#define JS_GET_CONTEXT(mjs) JS_GET_INST(mjs, mjs_get_this(mjs)) + +/** + * @brief Syntax sugar for constructing an object + * + * @example + * ```c + * mjs_val_t my_obj = mjs_mk_object(mjs); + * JS_ASSIGN_MULTI(mjs, my_obj) { + * JS_FIELD("method1", MJS_MK_FN(js_storage_file_is_open)); + * JS_FIELD("method2", MJS_MK_FN(js_storage_file_is_open)); + * } + * ``` + */ +#define JS_ASSIGN_MULTI(mjs, object) \ + for(struct { \ + struct mjs* mjs; \ + mjs_val_t val; \ + int i; \ + } _ass_multi = {mjs, object, 0}; \ + _ass_multi.i == 0; \ + _ass_multi.i++) +#define JS_FIELD(name, value) mjs_set(_ass_multi.mjs, _ass_multi.val, name, ~0, value) + +/** + * @brief The first word of structures that foreign pointer JS values point to + * + * This is used to detect situations where JS code mistakenly passes an opaque + * foreign pointer of one type as an argument to a native function which expects + * a struct of another type. + * + * It is recommended to use this functionality in conjunction with the following + * convenience verification macros: + * - `JS_ARG_STRUCT()` + * - `JS_ARG_OBJ_WITH_STRUCT()` + * + * @warning In order for the mechanism to work properly, your struct must store + * the magic value in the first word. + */ +typedef enum { + JsForeignMagicStart = 0x15BAD000, + JsForeignMagic_JsEventLoopContract, +} JsForeignMagic; + +// Are you tired of your silly little JS+C glue code functions being 75% +// argument validation code and 25% actual logic? Introducing: ASS (Argument +// Schema for Scripts)! ASS is a set of macros that reduce the typical +// boilerplate code of "check argument count, get arguments, validate arguments, +// extract C values from arguments" down to just one line! + +/** + * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies + * that the function requires exactly as many arguments as were specified. + */ +#define JS_EXACTLY == +/** + * When passed as the second argument to `JS_FETCH_ARGS_OR_RETURN`, signifies + * that the function requires at least as many arguments as were specified. + */ +#define JS_AT_LEAST >= + +#define JS_ENUM_MAP(var_name, ...) \ + static const JsEnumMapping var_name##_mapping[] = { \ + {NULL, sizeof(var_name)}, \ + __VA_ARGS__, \ + {NULL, 0}, \ + }; + +typedef struct { + const char* name; + size_t value; +} JsEnumMapping; + +typedef struct { + void* out; + int (*validator)(mjs_val_t); + void (*converter)(struct mjs*, mjs_val_t*, void* out, const void* extra); + const char* expected_type; + bool (*extended_validator)(struct mjs*, mjs_val_t, const void* extra); + const void* extra_data; +} _js_arg_decl; + +static inline void _js_to_int32(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(int32_t*)out = mjs_get_int32(mjs, *in); +} +#define JS_ARG_INT32(out) ((_js_arg_decl){out, mjs_is_number, _js_to_int32, "number", NULL, NULL}) + +static inline void _js_to_ptr(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(void**)out = mjs_get_ptr(mjs, *in); +} +#define JS_ARG_PTR(out) \ + ((_js_arg_decl){out, mjs_is_foreign, _js_to_ptr, "opaque pointer", NULL, NULL}) + +static inline void _js_to_string(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(const char**)out = mjs_get_string(mjs, in, NULL); +} +#define JS_ARG_STR(out) ((_js_arg_decl){out, mjs_is_string, _js_to_string, "string", NULL, NULL}) + +static inline void _js_to_bool(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + *(bool*)out = !!mjs_get_bool(mjs, *in); +} +#define JS_ARG_BOOL(out) ((_js_arg_decl){out, mjs_is_boolean, _js_to_bool, "boolean", NULL, NULL}) + +static inline void _js_passthrough(struct mjs* mjs, mjs_val_t* in, void* out, const void* extra) { + UNUSED(extra); + UNUSED(mjs); + *(mjs_val_t*)out = *in; +} +#define JS_ARG_ANY(out) ((_js_arg_decl){out, NULL, _js_passthrough, "any", NULL, NULL}) +#define JS_ARG_OBJ(out) ((_js_arg_decl){out, mjs_is_object, _js_passthrough, "any", NULL, NULL}) +#define JS_ARG_FN(out) \ + ((_js_arg_decl){out, mjs_is_function, _js_passthrough, "function", NULL, NULL}) +#define JS_ARG_ARR(out) ((_js_arg_decl){out, mjs_is_array, _js_passthrough, "array", NULL, NULL}) + +static inline bool _js_validate_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { + JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; + JsForeignMagic struct_magic = *(JsForeignMagic*)mjs_get_ptr(mjs, val); + return struct_magic == expected_magic; +} +#define JS_ARG_STRUCT(type, out) \ + ((_js_arg_decl){ \ + out, \ + mjs_is_foreign, \ + _js_to_ptr, \ + #type, \ + _js_validate_struct, \ + (void*)JsForeignMagic##_##type}) + +static inline bool _js_validate_obj_w_struct(struct mjs* mjs, mjs_val_t val, const void* extra) { + JsForeignMagic expected_magic = (JsForeignMagic)(size_t)extra; + JsForeignMagic struct_magic = *(JsForeignMagic*)JS_GET_INST(mjs, val); + return struct_magic == expected_magic; +} +#define JS_ARG_OBJ_WITH_STRUCT(type, out) \ + ((_js_arg_decl){ \ + out, \ + mjs_is_object, \ + _js_passthrough, \ + #type, \ + _js_validate_obj_w_struct, \ + (void*)JsForeignMagic##_##type}) + +static inline bool _js_validate_enum(struct mjs* mjs, mjs_val_t val, const void* extra) { + for(const JsEnumMapping* mapping = (JsEnumMapping*)extra + 1; mapping->name; mapping++) + if(strcmp(mapping->name, mjs_get_string(mjs, &val, NULL)) == 0) return true; + return false; +} +static inline void + _js_convert_enum(struct mjs* mjs, mjs_val_t* val, void* out, const void* extra) { + const JsEnumMapping* mapping = (JsEnumMapping*)extra; + size_t size = mapping->value; // get enum size from first entry + for(mapping++; mapping->name; mapping++) { + if(strcmp(mapping->name, mjs_get_string(mjs, val, NULL)) == 0) { + if(size == 1) + *(uint8_t*)out = mapping->value; + else if(size == 2) + *(uint16_t*)out = mapping->value; + else if(size == 4) + *(uint32_t*)out = mapping->value; + else if(size == 8) + *(uint64_t*)out = mapping->value; + return; + } + } + // unreachable, thanks to _js_validate_enum +} +#define JS_ARG_ENUM(var_name, name) \ + ((_js_arg_decl){ \ + &var_name, \ + mjs_is_string, \ + _js_convert_enum, \ + name " enum", \ + _js_validate_enum, \ + var_name##_mapping}) + +//-V:JS_FETCH_ARGS_OR_RETURN:1008 +/** + * @brief Fetches and validates the arguments passed to a JS function + * + * Example: `int32_t my_arg; JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&my_arg));` + * + * @warning This macro executes `return;` by design in case of an argument count + * mismatch or a validation failure + */ +#define JS_FETCH_ARGS_OR_RETURN(mjs, arg_operator, ...) \ + _js_arg_decl _js_args[] = {__VA_ARGS__}; \ + int _js_arg_cnt = COUNT_OF(_js_args); \ + mjs_val_t _js_arg_vals[_js_arg_cnt]; \ + if(!(mjs_nargs(mjs) arg_operator _js_arg_cnt)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "expected %s%d arguments, got %d", \ + #arg_operator, \ + _js_arg_cnt, \ + mjs_nargs(mjs)); \ + for(int _i = 0; _i < _js_arg_cnt; _i++) { \ + _js_arg_vals[_i] = mjs_arg(mjs, _i); \ + if(_js_args[_i].validator) \ + if(!_js_args[_i].validator(_js_arg_vals[_i])) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "argument %d: expected %s", \ + _i, \ + _js_args[_i].expected_type); \ + if(_js_args[_i].extended_validator) \ + if(!_js_args[_i].extended_validator(mjs, _js_arg_vals[_i], _js_args[_i].extra_data)) \ + JS_ERROR_AND_RETURN( \ + mjs, \ + MJS_BAD_ARGS_ERROR, \ + "argument %d: expected %s", \ + _i, \ + _js_args[_i].expected_type); \ + _js_args[_i].converter( \ + mjs, &_js_arg_vals[_i], _js_args[_i].out, _js_args[_i].extra_data); \ + } + +/** + * @brief Prepends an error, sets the JS return value to `undefined` and returns + * from the C function + * @warning This macro executes `return;` by design + */ +#define JS_ERROR_AND_RETURN(mjs, error_code, ...) \ + do { \ + mjs_prepend_errorf(mjs, error_code, __VA_ARGS__); \ + mjs_return(mjs, MJS_UNDEFINED); \ + return; \ + } while(0) + +typedef struct JsModules JsModules; + +typedef void* (*JsModuleConstructor)(struct mjs* mjs, mjs_val_t* object, JsModules* modules); +typedef void (*JsModuleDestructor)(void* inst); typedef struct { char* name; - JsModeConstructor create; - JsModeDestructor destroy; + JsModuleConstructor create; + JsModuleDestructor destroy; + const ElfApiInterface* api_interface; } JsModuleDescriptor; -typedef struct JsModules JsModules; - JsModules* js_modules_create(struct mjs* mjs, CompositeApiResolver* resolver); void js_modules_destroy(JsModules* modules); mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len); + +/** + * @brief Gets a module instance by its name + * This is useful when a module wants to access a stateful API of another + * module. + * @returns Pointer to module context, NULL if the module is not instantiated + */ +void* js_module_get(JsModules* modules, const char* name); diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index e1c21e9fb..95cad84f6 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -196,17 +196,11 @@ static void js_require(struct mjs* mjs) { } static void js_global_to_string(struct mjs* mjs) { - double num = mjs_get_double(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-2147483648"; - itoa(num, tmp_str, 10); - mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); - mjs_return(mjs, ret); -} - -static void js_global_to_hex_string(struct mjs* mjs) { + int base = 10; + if(mjs_nargs(mjs) > 1) base = mjs_get_int(mjs, mjs_arg(mjs, 1)); double num = mjs_get_int(mjs, mjs_arg(mjs, 0)); - char tmp_str[] = "-FFFFFFFF"; - itoa(num, tmp_str, 16); + char tmp_str[] = "-2147483648"; + itoa(num, tmp_str, base); mjs_val_t ret = mjs_mk_string(mjs, tmp_str, ~0, true); mjs_return(mjs, ret); } @@ -340,8 +334,7 @@ static int32_t js_thread(void* arg) { } mjs_set(mjs, global, "print", ~0, MJS_MK_FN(js_print)); mjs_set(mjs, global, "delay", ~0, MJS_MK_FN(js_delay)); - mjs_set(mjs, global, "to_string", ~0, MJS_MK_FN(js_global_to_string)); - mjs_set(mjs, global, "to_hex_string", ~0, MJS_MK_FN(js_global_to_hex_string)); + mjs_set(mjs, global, "toString", ~0, MJS_MK_FN(js_global_to_string)); mjs_set(mjs, global, "ffi_address", ~0, MJS_MK_FN(js_ffi_address)); mjs_set(mjs, global, "require", ~0, MJS_MK_FN(js_require)); mjs_set(mjs, global, "parse_int", ~0, MJS_MK_FN(js_parse_int)); @@ -400,8 +393,8 @@ static int32_t js_thread(void* arg) { } } - js_modules_destroy(worker->modules); mjs_destroy(mjs); + js_modules_destroy(worker->modules); composite_api_resolver_free(worker->resolver); diff --git a/applications/system/js_app/js_thread.h b/applications/system/js_app/js_thread.h index 969715ec1..581a44919 100644 --- a/applications/system/js_app/js_thread.h +++ b/applications/system/js_app/js_thread.h @@ -1,5 +1,9 @@ #pragma once +#ifdef __cplusplus +extern "C" { +#endif + typedef struct JsThread JsThread; typedef enum { @@ -14,3 +18,7 @@ typedef void (*JsThreadCallback)(JsThreadEvent event, const char* msg, void* con JsThread* js_thread_run(const char* script_path, JsThreadCallback callback, void* context); void js_thread_stop(JsThread* worker); + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/modules/js_badusb.c b/applications/system/js_app/modules/js_badusb.c index 90cd38c08..4abb2331e 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -102,9 +102,9 @@ static bool setup_parse_params( } mjs_val_t vid_obj = mjs_get(mjs, arg, "vid", ~0); mjs_val_t pid_obj = mjs_get(mjs, arg, "pid", ~0); - mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfr_name", ~0); - mjs_val_t prod_obj = mjs_get(mjs, arg, "prod_name", ~0); - mjs_val_t layout_obj = mjs_get(mjs, arg, "layout_path", ~0); + mjs_val_t mfr_obj = mjs_get(mjs, arg, "mfrName", ~0); + mjs_val_t prod_obj = mjs_get(mjs, arg, "prodName", ~0); + mjs_val_t layout_obj = mjs_get(mjs, arg, "layoutPath", ~0); if(mjs_is_number(vid_obj) && mjs_is_number(pid_obj)) { hid_cfg->vid = mjs_get_int32(mjs, vid_obj); @@ -486,7 +486,8 @@ static void js_badusb_alt_println(struct mjs* mjs) { badusb_print(mjs, true, true); } -static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object) { +static void* js_badusb_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); JsBadusbInst* badusb = malloc(sizeof(JsBadusbInst)); mjs_val_t badusb_obj = mjs_mk_object(mjs); mjs_set(mjs, badusb_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, badusb)); @@ -514,6 +515,7 @@ static const JsModuleDescriptor js_badusb_desc = { "badusb", js_badusb_create, js_badusb_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_dialog.c b/applications/system/js_app/modules/js_dialog.c deleted file mode 100644 index 1de1d9069..000000000 --- a/applications/system/js_app/modules/js_dialog.c +++ /dev/null @@ -1,207 +0,0 @@ -#include -#include "../js_modules.h" -#include -#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_pick_file(struct mjs* mjs) { - if(mjs_nargs(mjs) != 2) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Wrong arguments"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - mjs_val_t base_path_obj = mjs_arg(mjs, 0); - if(!mjs_is_string(base_path_obj)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Base path must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - size_t base_path_len = 0; - const char* base_path = mjs_get_string(mjs, &base_path_obj, &base_path_len); - if((base_path_len == 0) || (base_path == NULL)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad base path argument"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - mjs_val_t extension_obj = mjs_arg(mjs, 1); - if(!mjs_is_string(extension_obj)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Extension must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - size_t extension_len = 0; - const char* extension = mjs_get_string(mjs, &extension_obj, &extension_len); - if((extension_len == 0) || (extension == NULL)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Bad extension argument"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); - const DialogsFileBrowserOptions browser_options = { - .extension = extension, - .icon = &I_file_10px, - .base_path = base_path, - }; - FuriString* path = furi_string_alloc_set(base_path); - if(dialog_file_browser_show(dialogs, path, path, &browser_options)) { - mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(path), ~0, true)); - } else { - mjs_return(mjs, MJS_UNDEFINED); - } - furi_string_free(path); - furi_record_close(RECORD_DIALOGS); -} - -static void* js_dialog_create(struct mjs* mjs, mjs_val_t* object) { - mjs_val_t dialog_obj = mjs_mk_object(mjs); - mjs_set(mjs, dialog_obj, "message", ~0, MJS_MK_FN(js_dialog_message)); - mjs_set(mjs, dialog_obj, "custom", ~0, MJS_MK_FN(js_dialog_custom)); - mjs_set(mjs, dialog_obj, "pickFile", ~0, MJS_MK_FN(js_dialog_pick_file)); - *object = dialog_obj; - - return (void*)1; -} - -static const JsModuleDescriptor js_dialog_desc = { - "dialog", - js_dialog_create, - NULL, -}; - -static const FlipperAppPluginDescriptor plugin_descriptor = { - .appid = PLUGIN_APP_ID, - .ep_api_version = PLUGIN_API_VERSION, - .entry_point = &js_dialog_desc, -}; - -const FlipperAppPluginDescriptor* js_dialog_ep(void) { - return &plugin_descriptor; -} diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.c b/applications/system/js_app/modules/js_event_loop/js_event_loop.c new file mode 100644 index 000000000..c4f0d1bee --- /dev/null +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.c @@ -0,0 +1,451 @@ +#include "js_event_loop.h" +#include "../../js_modules.h" // IWYU pragma: keep +#include +#include + +/** + * @brief Number of arguments that callbacks receive from this module that they can't modify + */ +#define SYSTEM_ARGS 2 + +/** + * @brief Context passed to the generic event callback + */ +typedef struct { + JsEventLoopObjectType object_type; + + struct mjs* mjs; + mjs_val_t callback; + // NOTE: not using an mlib array because resizing is not needed. + mjs_val_t* arguments; + size_t arity; + + JsEventLoopTransformer transformer; + void* transformer_context; +} JsEventLoopCallbackContext; + +/** + * @brief Contains data needed to cancel a subscription + */ +typedef struct { + FuriEventLoop* loop; + JsEventLoopObjectType object_type; + FuriEventLoopObject* object; + JsEventLoopCallbackContext* context; + JsEventLoopContract* contract; + void* subscriptions; // SubscriptionArray_t, which we can't reference in this definition +} JsEventLoopSubscription; + +typedef struct { + FuriEventLoop* loop; + struct mjs* mjs; +} JsEventLoopTickContext; + +ARRAY_DEF(SubscriptionArray, JsEventLoopSubscription*, M_PTR_OPLIST); //-V575 +ARRAY_DEF(ContractArray, JsEventLoopContract*, M_PTR_OPLIST); //-V575 + +/** + * @brief Per-module instance control structure + */ +struct JsEventLoop { + FuriEventLoop* loop; + SubscriptionArray_t subscriptions; + ContractArray_t owned_contracts; //mjs, + &result, + context->callback, + MJS_UNDEFINED, + context->arity, + context->arguments); + + // save returned args for next call + if(mjs_array_length(context->mjs, result) != context->arity - SYSTEM_ARGS) return; + for(size_t i = 0; i < context->arity - SYSTEM_ARGS; i++) { + mjs_disown(context->mjs, &context->arguments[i + SYSTEM_ARGS]); + context->arguments[i + SYSTEM_ARGS] = mjs_array_get(context->mjs, result, i); + mjs_own(context->mjs, &context->arguments[i + SYSTEM_ARGS]); + } +} + +/** + * @brief Handles non-timer events + */ +static bool js_event_loop_callback(void* object, void* param) { + JsEventLoopCallbackContext* context = param; + + if(context->transformer) { + mjs_disown(context->mjs, &context->arguments[1]); + context->arguments[1] = + context->transformer(context->mjs, object, context->transformer_context); + mjs_own(context->mjs, &context->arguments[1]); + } else { + // default behavior: take semaphores and mutexes + switch(context->object_type) { + case JsEventLoopObjectTypeSemaphore: { + FuriSemaphore* semaphore = object; + furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk); + } break; + default: + // the corresponding check has been performed when we were given the contract + furi_crash(); + } + } + + js_event_loop_callback_generic(param); + + return true; +} + +/** + * @brief Cancels an event subscription + */ +static void js_event_loop_subscription_cancel(struct mjs* mjs) { + JsEventLoopSubscription* subscription = JS_GET_CONTEXT(mjs); + + if(subscription->object_type == JsEventLoopObjectTypeTimer) { + furi_event_loop_timer_stop(subscription->object); + } else { + furi_event_loop_unsubscribe(subscription->loop, subscription->object); + } + + free(subscription->context->arguments); + free(subscription->context); + + // find and remove ourselves from the array + SubscriptionArray_it_t iterator; + for(SubscriptionArray_it(iterator, subscription->subscriptions); + !SubscriptionArray_end_p(iterator); + SubscriptionArray_next(iterator)) { + JsEventLoopSubscription* item = *SubscriptionArray_cref(iterator); + if(item == subscription) break; + } + SubscriptionArray_remove(subscription->subscriptions, iterator); + free(subscription); + + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief Subscribes a JavaScript function to an event + */ +static void js_event_loop_subscribe(struct mjs* mjs) { + JsEventLoop* module = JS_GET_CONTEXT(mjs); + + // get arguments + JsEventLoopContract* contract; + mjs_val_t callback; + JS_FETCH_ARGS_OR_RETURN( + mjs, JS_AT_LEAST, JS_ARG_STRUCT(JsEventLoopContract, &contract), JS_ARG_FN(&callback)); + + // create subscription object + JsEventLoopSubscription* subscription = malloc(sizeof(JsEventLoopSubscription)); + JsEventLoopCallbackContext* context = malloc(sizeof(JsEventLoopCallbackContext)); + subscription->loop = module->loop; + subscription->object_type = contract->object_type; + subscription->context = context; + subscription->subscriptions = module->subscriptions; + if(contract->object_type == JsEventLoopObjectTypeTimer) subscription->contract = contract; + mjs_val_t subscription_obj = mjs_mk_object(mjs); + mjs_set(mjs, subscription_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, subscription)); + mjs_set(mjs, subscription_obj, "cancel", ~0, MJS_MK_FN(js_event_loop_subscription_cancel)); + + // create callback context + context->object_type = contract->object_type; + context->arity = mjs_nargs(mjs) - SYSTEM_ARGS + 2; + context->arguments = calloc(context->arity, sizeof(mjs_val_t)); + context->arguments[0] = subscription_obj; + context->arguments[1] = MJS_UNDEFINED; + for(size_t i = SYSTEM_ARGS; i < context->arity; i++) { + mjs_val_t arg = mjs_arg(mjs, i - SYSTEM_ARGS + 2); + context->arguments[i] = arg; + mjs_own(mjs, &context->arguments[i]); + } + context->mjs = mjs; + context->callback = callback; + mjs_own(mjs, &context->callback); + mjs_own(mjs, &context->arguments[0]); + mjs_own(mjs, &context->arguments[1]); + + // queue and stream contracts must have a transform callback, others are allowed to delegate + // the obvious default behavior to this module + if(contract->object_type == JsEventLoopObjectTypeQueue || + contract->object_type == JsEventLoopObjectTypeStream) { + furi_check(contract->non_timer.transformer); + } + context->transformer = contract->non_timer.transformer; + context->transformer_context = contract->non_timer.transformer_context; + + // subscribe + switch(contract->object_type) { + case JsEventLoopObjectTypeTimer: { + FuriEventLoopTimer* timer = furi_event_loop_timer_alloc( + module->loop, js_event_loop_callback_generic, contract->timer.type, context); + furi_event_loop_timer_start(timer, contract->timer.interval_ticks); + contract->object = timer; + } break; + case JsEventLoopObjectTypeSemaphore: + furi_event_loop_subscribe_semaphore( + module->loop, + contract->object, + contract->non_timer.event, + js_event_loop_callback, + context); + break; + case JsEventLoopObjectTypeQueue: + furi_event_loop_subscribe_message_queue( + module->loop, + contract->object, + contract->non_timer.event, + js_event_loop_callback, + context); + break; + default: + furi_crash("unimplemented"); + } + + subscription->object = contract->object; + SubscriptionArray_push_back(module->subscriptions, subscription); + mjs_return(mjs, subscription_obj); +} + +/** + * @brief Runs the event loop until it is stopped + */ +static void js_event_loop_run(struct mjs* mjs) { + JsEventLoop* module = JS_GET_CONTEXT(mjs); + furi_event_loop_run(module->loop); +} + +/** + * @brief Stops a running event loop + */ +static void js_event_loop_stop(struct mjs* mjs) { + JsEventLoop* module = JS_GET_CONTEXT(mjs); + furi_event_loop_stop(module->loop); +} + +/** + * @brief Creates a timer event that can be subscribed to just like any other + * event + */ +static void js_event_loop_timer(struct mjs* mjs) { + // get arguments + const char* mode_str; + int32_t interval; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&mode_str), JS_ARG_INT32(&interval)); + JsEventLoop* module = JS_GET_CONTEXT(mjs); + + FuriEventLoopTimerType mode; + if(strcasecmp(mode_str, "periodic") == 0) { + mode = FuriEventLoopTimerTypePeriodic; + } else if(strcasecmp(mode_str, "oneshot") == 0) { + mode = FuriEventLoopTimerTypeOnce; + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: unknown mode"); + } + + // make timer contract + JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract)); + *contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeTimer, + .object = NULL, + .timer = + { + .interval_ticks = furi_ms_to_ticks((uint32_t)interval), + .type = mode, + }, + }; + ContractArray_push_back(module->owned_contracts, contract); + mjs_return(mjs, mjs_mk_foreign(mjs, contract)); +} + +/** + * @brief Queue transformer. Takes `mjs_val_t` pointers out of a queue and + * returns their dereferenced value + */ +static mjs_val_t + js_event_loop_queue_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) { + UNUSED(context); + mjs_val_t* message_ptr; + furi_check(furi_message_queue_get(object, &message_ptr, 0) == FuriStatusOk); + mjs_val_t message = *message_ptr; + mjs_disown(mjs, message_ptr); + free(message_ptr); + return message; +} + +/** + * @brief Sends a message to a queue + */ +static void js_event_loop_queue_send(struct mjs* mjs) { + // get arguments + mjs_val_t message; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&message)); + JsEventLoopContract* contract = JS_GET_CONTEXT(mjs); + + // send message + mjs_val_t* message_ptr = malloc(sizeof(mjs_val_t)); + *message_ptr = message; + mjs_own(mjs, message_ptr); + furi_message_queue_put(contract->object, &message_ptr, 0); + + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief Creates a queue + */ +static void js_event_loop_queue(struct mjs* mjs) { + // get arguments + int32_t length; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&length)); + JsEventLoop* module = JS_GET_CONTEXT(mjs); + + // make queue contract + JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract)); + *contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + // we could store `mjs_val_t`s in the queue directly if not for mJS' requirement to have consistent pointers to owned values + .object = furi_message_queue_alloc((size_t)length, sizeof(mjs_val_t*)), + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = js_event_loop_queue_transformer, + }, + }; + ContractArray_push_back(module->owned_contracts, contract); + + // return object with control methods + mjs_val_t queue = mjs_mk_object(mjs); + mjs_set(mjs, queue, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, contract)); + mjs_set(mjs, queue, "input", ~0, mjs_mk_foreign(mjs, contract)); + mjs_set(mjs, queue, "send", ~0, MJS_MK_FN(js_event_loop_queue_send)); + mjs_return(mjs, queue); +} + +static void js_event_loop_tick(void* param) { + JsEventLoopTickContext* context = param; + uint32_t flags = furi_thread_flags_wait(ThreadEventStop, FuriFlagWaitAny | FuriFlagNoClear, 0); + if(flags & FuriFlagError) { + return; + } + if(flags & ThreadEventStop) { + furi_event_loop_stop(context->loop); + mjs_exit(context->mjs); + } +} + +static void* js_event_loop_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + mjs_val_t event_loop_obj = mjs_mk_object(mjs); + JsEventLoop* module = malloc(sizeof(JsEventLoop)); + JsEventLoopTickContext* tick_ctx = malloc(sizeof(JsEventLoopTickContext)); + module->loop = furi_event_loop_alloc(); + tick_ctx->loop = module->loop; + tick_ctx->mjs = mjs; + module->tick_context = tick_ctx; + furi_event_loop_tick_set(module->loop, 10, js_event_loop_tick, tick_ctx); + SubscriptionArray_init(module->subscriptions); + ContractArray_init(module->owned_contracts); + + mjs_set(mjs, event_loop_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module)); + mjs_set(mjs, event_loop_obj, "subscribe", ~0, MJS_MK_FN(js_event_loop_subscribe)); + mjs_set(mjs, event_loop_obj, "run", ~0, MJS_MK_FN(js_event_loop_run)); + mjs_set(mjs, event_loop_obj, "stop", ~0, MJS_MK_FN(js_event_loop_stop)); + mjs_set(mjs, event_loop_obj, "timer", ~0, MJS_MK_FN(js_event_loop_timer)); + mjs_set(mjs, event_loop_obj, "queue", ~0, MJS_MK_FN(js_event_loop_queue)); + + *object = event_loop_obj; + return module; +} + +static void js_event_loop_destroy(void* inst) { + if(inst) { + JsEventLoop* module = inst; + furi_event_loop_stop(module->loop); + + // free subscriptions + SubscriptionArray_it_t sub_iterator; + for(SubscriptionArray_it(sub_iterator, module->subscriptions); + !SubscriptionArray_end_p(sub_iterator); + SubscriptionArray_next(sub_iterator)) { + JsEventLoopSubscription* const* sub = SubscriptionArray_cref(sub_iterator); + free((*sub)->context->arguments); + free((*sub)->context); + free(*sub); + } + SubscriptionArray_clear(module->subscriptions); + + // free owned contracts + ContractArray_it_t iterator; + for(ContractArray_it(iterator, module->owned_contracts); !ContractArray_end_p(iterator); + ContractArray_next(iterator)) { + // unsubscribe object + JsEventLoopContract* contract = *ContractArray_cref(iterator); + if(contract->object_type == JsEventLoopObjectTypeTimer) { + furi_event_loop_timer_stop(contract->object); + } else { + furi_event_loop_unsubscribe(module->loop, contract->object); + } + + // free object + switch(contract->object_type) { + case JsEventLoopObjectTypeTimer: + furi_event_loop_timer_free(contract->object); + break; + case JsEventLoopObjectTypeSemaphore: + furi_semaphore_free(contract->object); + break; + case JsEventLoopObjectTypeQueue: + furi_message_queue_free(contract->object); + break; + default: + furi_crash("unimplemented"); + } + + free(contract); + } + ContractArray_clear(module->owned_contracts); + + furi_event_loop_free(module->loop); + free(module->tick_context); + free(module); + } +} + +extern const ElfApiInterface js_event_loop_hashtable_api_interface; + +static const JsModuleDescriptor js_event_loop_desc = { + "event_loop", + js_event_loop_create, + js_event_loop_destroy, + &js_event_loop_hashtable_api_interface, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_event_loop_desc, +}; + +const FlipperAppPluginDescriptor* js_event_loop_ep(void) { + return &plugin_descriptor; +} + +FuriEventLoop* js_event_loop_get_loop(JsEventLoop* loop) { + // porta: not the proudest function that i ever wrote + furi_check(loop); + return loop->loop; +} diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop.h b/applications/system/js_app/modules/js_event_loop/js_event_loop.h new file mode 100644 index 000000000..7ae608e34 --- /dev/null +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop.h @@ -0,0 +1,104 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include +#include + +/** + * @file js_event_loop.h + * + * In JS interpreter code, `js_event_loop` always creates and maintains the + * event loop. There are two ways in which other modules can integrate with this + * loop: + * - Via contracts: The user of your module would have to acquire an opaque + * JS value from you and pass it to `js_event_loop`. This is useful for + * events that they user may be interested in. For more info, look at + * `JsEventLoopContract`. Also look at `js_event_loop_get_loop`, which + * you will need to unsubscribe the event loop from your object. + * - Directly: When your module is created, you can acquire an instance of + * `JsEventLoop` which you can use to acquire an instance of + * `FuriEventLoop` that you can manipulate directly, without the JS + * programmer having to pass contracts around. This is useful for + * "behind-the-scenes" events that the user does not need to know about. For + * more info, look at `js_event_loop_get_loop`. + * + * In both cases, your module is responsible for both instantiating, + * unsubscribing and freeing the object that the event loop subscribes to. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct JsEventLoop JsEventLoop; + +typedef enum { + JsEventLoopObjectTypeTimer, + JsEventLoopObjectTypeQueue, + JsEventLoopObjectTypeMutex, + JsEventLoopObjectTypeSemaphore, + JsEventLoopObjectTypeStream, +} JsEventLoopObjectType; + +typedef mjs_val_t ( + *JsEventLoopTransformer)(struct mjs* mjs, FuriEventLoopObject* object, void* context); + +typedef struct { + FuriEventLoopEvent event; + JsEventLoopTransformer transformer; + void* transformer_context; +} JsEventLoopNonTimerContract; + +typedef struct { + FuriEventLoopTimerType type; + uint32_t interval_ticks; +} JsEventLoopTimerContract; + +/** + * @brief Adapter for other JS modules that wish to integrate with the event + * loop JS module + * + * If another module wishes to integrate with `js_event_loop`, it needs to + * implement a function callable from JS that returns an mJS foreign pointer to + * an instance of this structure. This value is then read by `event_loop`'s + * `subscribe` function. + * + * There are two fundamental variants of this structure: + * - `object_type` is `JsEventLoopObjectTypeTimer`: the `timer` field is + * valid, and the `non_timer` field is invalid. + * - `object_type` is something else: the `timer` field is invalid, and the + * `non_timer` field is valid. `non_timer.event` will be passed to + * `furi_event_loop_subscribe`. `non_timer.transformer` will be called to + * transform an object into a JS value (called an item) that's passed to the + * JS callback. This is useful for example to take an item out of a message + * queue and pass it to JS code in a convenient format. If + * `non_timer.transformer` is NULL, the event loop will take semaphores and + * mutexes on its own. + * + * The producer of the contract is responsible for freeing both the contract and + * the object that it points to when the interpreter is torn down. + */ +typedef struct { + JsForeignMagic magic; // +#include + +#include "js_event_loop_api_table_i.h" + +static_assert(!has_hash_collisions(js_event_loop_api_table), "Detected API method hash collision!"); + +extern "C" constexpr HashtableApiInterface js_event_loop_hashtable_api_interface{ + { + .api_version_major = 0, + .api_version_minor = 0, + .resolver_callback = &elf_resolve_from_hashtable, + }, + js_event_loop_api_table.cbegin(), + js_event_loop_api_table.cend(), +}; diff --git a/applications/system/js_app/modules/js_event_loop/js_event_loop_api_table_i.h b/applications/system/js_app/modules/js_event_loop/js_event_loop_api_table_i.h new file mode 100644 index 000000000..49090caeb --- /dev/null +++ b/applications/system/js_app/modules/js_event_loop/js_event_loop_api_table_i.h @@ -0,0 +1,4 @@ +#include "js_event_loop.h" + +static constexpr auto js_event_loop_api_table = sort( + create_array_t(API_METHOD(js_event_loop_get_loop, FuriEventLoop*, (JsEventLoop*)))); diff --git a/applications/system/js_app/modules/js_flipper.c b/applications/system/js_app/modules/js_flipper.c index 4619a1593..43c675e10 100644 --- a/applications/system/js_app/modules/js_flipper.c +++ b/applications/system/js_app/modules/js_flipper.c @@ -25,7 +25,8 @@ static void js_flipper_get_battery(struct mjs* mjs) { mjs_return(mjs, mjs_mk_number(mjs, info.charge)); } -void* js_flipper_create(struct mjs* mjs, mjs_val_t* object) { +void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); mjs_val_t flipper_obj = mjs_mk_object(mjs); mjs_set(mjs, flipper_obj, "getModel", ~0, MJS_MK_FN(js_flipper_get_model)); mjs_set(mjs, flipper_obj, "getName", ~0, MJS_MK_FN(js_flipper_get_name)); diff --git a/applications/system/js_app/modules/js_flipper.h b/applications/system/js_app/modules/js_flipper.h index 3b05389cc..98979ce58 100644 --- a/applications/system/js_app/modules/js_flipper.h +++ b/applications/system/js_app/modules/js_flipper.h @@ -1,4 +1,4 @@ #pragma once #include "../js_thread_i.h" -void* js_flipper_create(struct mjs* mjs, mjs_val_t* object); +void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules); diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index fb42bea2b..70021968f 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -1,387 +1,337 @@ -#include "../js_modules.h" +#include "../js_modules.h" // IWYU pragma: keep +#include "./js_event_loop/js_event_loop.h" #include #include #include +#include +#include -typedef struct { - FuriHalAdcHandle* handle; -} JsGpioInst; +#define INTERRUPT_QUEUE_LEN 16 +/** + * Per-pin control structure + */ typedef struct { const GpioPin* pin; - const char* name; - const FuriHalAdcChannel channel; -} GpioPinCtx; + bool had_interrupt; + FuriSemaphore* interrupt_semaphore; + JsEventLoopContract* interrupt_contract; + FuriHalAdcChannel adc_channel; + FuriHalAdcHandle* adc_handle; +} JsGpioPinInst; -static const GpioPinCtx js_gpio_pins[] = { - {.pin = &gpio_ext_pa7, .name = "PA7", .channel = FuriHalAdcChannel12}, // 2 - {.pin = &gpio_ext_pa6, .name = "PA6", .channel = FuriHalAdcChannel11}, // 3 - {.pin = &gpio_ext_pa4, .name = "PA4", .channel = FuriHalAdcChannel9}, // 4 - {.pin = &gpio_ext_pb3, .name = "PB3", .channel = FuriHalAdcChannelNone}, // 5 - {.pin = &gpio_ext_pb2, .name = "PB2", .channel = FuriHalAdcChannelNone}, // 6 - {.pin = &gpio_ext_pc3, .name = "PC3", .channel = FuriHalAdcChannel4}, // 7 - {.pin = &gpio_swclk, .name = "PA14", .channel = FuriHalAdcChannelNone}, // 10 - {.pin = &gpio_swdio, .name = "PA13", .channel = FuriHalAdcChannelNone}, // 12 - {.pin = &gpio_usart_tx, .name = "PB6", .channel = FuriHalAdcChannelNone}, // 13 - {.pin = &gpio_usart_rx, .name = "PB7", .channel = FuriHalAdcChannelNone}, // 14 - {.pin = &gpio_ext_pc1, .name = "PC1", .channel = FuriHalAdcChannel2}, // 15 - {.pin = &gpio_ext_pc0, .name = "PC0", .channel = FuriHalAdcChannel1}, // 16 - {.pin = &gpio_ibutton, .name = "PB14", .channel = FuriHalAdcChannelNone}, // 17 -}; +ARRAY_DEF(ManagedPinsArray, JsGpioPinInst*, M_PTR_OPLIST); //-V575 -bool js_gpio_get_gpio_pull(const char* pull, GpioPull* value) { - if(strcmp(pull, "no") == 0) { - *value = GpioPullNo; - return true; - } else if(strcmp(pull, "up") == 0) { - *value = GpioPullUp; - return true; - } else if(strcmp(pull, "down") == 0) { - *value = GpioPullDown; - return true; - } else { - *value = GpioPullNo; - return true; - } - return false; -} - -bool js_gpio_get_gpio_mode(const char* mode, GpioMode* value) { - if(strcmp(mode, "input") == 0) { - *value = GpioModeInput; - return true; - } else if(strcmp(mode, "outputPushPull") == 0) { - *value = GpioModeOutputPushPull; - return true; - } else if(strcmp(mode, "outputOpenDrain") == 0) { - *value = GpioModeOutputOpenDrain; - return true; - } else if(strcmp(mode, "altFunctionPushPull") == 0) { - *value = GpioModeAltFunctionPushPull; - return true; - } else if(strcmp(mode, "altFunctionOpenDrain") == 0) { - *value = GpioModeAltFunctionOpenDrain; - return true; - } else if(strcmp(mode, "analog") == 0) { - *value = GpioModeAnalog; - return true; - } else if(strcmp(mode, "interruptRise") == 0) { - *value = GpioModeInterruptRise; - return true; - } else if(strcmp(mode, "interruptFall") == 0) { - *value = GpioModeInterruptFall; - return true; - } else if(strcmp(mode, "interruptRiseFall") == 0) { - *value = GpioModeInterruptRiseFall; - return true; - } else if(strcmp(mode, "eventRise") == 0) { - *value = GpioModeEventRise; - return true; - } else if(strcmp(mode, "eventFall") == 0) { - *value = GpioModeEventFall; - return true; - } else if(strcmp(mode, "eventRiseFall") == 0) { - *value = GpioModeEventRiseFall; - return true; - } else { - return false; - } -} - -const GpioPin* js_gpio_get_gpio_pin(const char* name) { - for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { - if(strcmp(js_gpio_pins[i].name, name) == 0) { - return js_gpio_pins[i].pin; - } - } - return NULL; -} - -FuriHalAdcChannel js_gpio_get_gpio_channel(const char* name) { - for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { - if(strcmp(js_gpio_pins[i].name, name) == 0) { - return js_gpio_pins[i].channel; - } - } - return FuriHalAdcChannelNone; +/** + * Per-module instance control structure + */ +typedef struct { + FuriEventLoop* loop; + ManagedPinsArray_t managed_pins; + FuriHalAdcHandle* adc_handle; +} JsGpioInst; + +/** + * @brief Interrupt callback + */ +static void js_gpio_int_cb(void* arg) { + furi_assert(arg); + FuriSemaphore* semaphore = arg; + furi_semaphore_release(semaphore); } +/** + * @brief Initializes a GPIO pin according to the provided mode object + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let led = gpio.get("pc3"); + * led.init({ direction: "out", outMode: "push_pull" }); + * ``` + */ static void js_gpio_init(struct mjs* mjs) { - mjs_val_t pin_arg = mjs_arg(mjs, 0); - mjs_val_t mode_arg = mjs_arg(mjs, 1); - mjs_val_t pull_arg = mjs_arg(mjs, 2); + // deconstruct mode object + mjs_val_t mode_arg; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&mode_arg)); + mjs_val_t direction_arg = mjs_get(mjs, mode_arg, "direction", ~0); + mjs_val_t out_mode_arg = mjs_get(mjs, mode_arg, "outMode", ~0); + mjs_val_t in_mode_arg = mjs_get(mjs, mode_arg, "inMode", ~0); + mjs_val_t edge_arg = mjs_get(mjs, mode_arg, "edge", ~0); + mjs_val_t pull_arg = mjs_get(mjs, mode_arg, "pull", ~0); - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; + // get strings + const char* direction = mjs_get_string(mjs, &direction_arg, NULL); + const char* out_mode = mjs_get_string(mjs, &out_mode_arg, NULL); + const char* in_mode = mjs_get_string(mjs, &in_mode_arg, NULL); + const char* edge = mjs_get_string(mjs, &edge_arg, NULL); + const char* pull = mjs_get_string(mjs, &pull_arg, NULL); + if(!direction) + JS_ERROR_AND_RETURN( + mjs, MJS_BAD_ARGS_ERROR, "Expected string in \"direction\" field of mode object"); + if(!out_mode) out_mode = "open_drain"; + if(!in_mode) in_mode = "plain_digital"; + if(!edge) edge = "rising"; + + // convert strings to mode + GpioMode mode; + if(strcmp(direction, "out") == 0) { + if(strcmp(out_mode, "push_pull") == 0) + mode = GpioModeOutputPushPull; + else if(strcmp(out_mode, "open_drain") == 0) + mode = GpioModeOutputOpenDrain; + else + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid outMode"); + } else if(strcmp(direction, "in") == 0) { + if(strcmp(in_mode, "analog") == 0) { + mode = GpioModeAnalog; + } else if(strcmp(in_mode, "plain_digital") == 0) { + mode = GpioModeInput; + } else if(strcmp(in_mode, "interrupt") == 0) { + if(strcmp(edge, "rising") == 0) + mode = GpioModeInterruptRise; + else if(strcmp(edge, "falling") == 0) + mode = GpioModeInterruptFall; + else if(strcmp(edge, "both") == 0) + mode = GpioModeInterruptRiseFall; + else + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); + } else if(strcmp(in_mode, "event") == 0) { + if(strcmp(edge, "rising") == 0) + mode = GpioModeEventRise; + else if(strcmp(edge, "falling") == 0) + mode = GpioModeEventFall; + else if(strcmp(edge, "both") == 0) + mode = GpioModeEventRiseFall; + else + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid edge"); + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid inMode"); + } + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid direction"); } - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; + // convert pull + GpioPull pull_mode; + if(!pull) { + pull_mode = GpioPullNo; + } else if(strcmp(pull, "up") == 0) { + pull_mode = GpioPullUp; + } else if(strcmp(pull, "down") == 0) { + pull_mode = GpioPullDown; + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "invalid pull"); } - if(!mjs_is_string(mode_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* mode_name = mjs_get_string(mjs, &mode_arg, NULL); - if(!mode_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get mode name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - if(!mjs_is_string(pull_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pull_name = mjs_get_string(mjs, &pull_arg, NULL); - if(!pull_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pull name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); - if(gpio_pin == NULL) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - GpioMode gpio_mode; - if(!js_gpio_get_gpio_mode(mode_name, &gpio_mode)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid mode name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - GpioPull gpio_pull; - if(!js_gpio_get_gpio_pull(pull_name, &gpio_pull)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pull name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - expansion_disable(furi_record_open(RECORD_EXPANSION)); - furi_record_close(RECORD_EXPANSION); - - furi_hal_gpio_init(gpio_pin, gpio_mode, gpio_pull, GpioSpeedVeryHigh); - + // init GPIO + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + furi_hal_gpio_init(manager_data->pin, mode, pull_mode, GpioSpeedVeryHigh); mjs_return(mjs, MJS_UNDEFINED); } +/** + * @brief Writes a logic value to a GPIO pin + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let led = gpio.get("pc3"); + * led.init({ direction: "out", outMode: "push_pull" }); + * led.write(true); + * ``` + */ static void js_gpio_write(struct mjs* mjs) { - mjs_val_t pin_arg = mjs_arg(mjs, 0); - mjs_val_t value_arg = mjs_arg(mjs, 1); - - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - if(!mjs_is_boolean(value_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a boolean"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - bool value = mjs_get_bool(mjs, value_arg); - - const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); - - if(gpio_pin == NULL) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - furi_hal_gpio_write(gpio_pin, value); - + bool level; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_BOOL(&level)); + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + furi_hal_gpio_write(manager_data->pin, level); mjs_return(mjs, MJS_UNDEFINED); } +/** + * @brief Reads a logic value from a GPIO pin + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let button = gpio.get("pc1"); + * button.init({ direction: "in" }); + * if(button.read()) + * print("hi button!!!!!"); + * ``` + */ static void js_gpio_read(struct mjs* mjs) { - mjs_val_t pin_arg = mjs_arg(mjs, 0); - - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const GpioPin* gpio_pin = js_gpio_get_gpio_pin(pin_name); - - if(gpio_pin == NULL) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - bool value = furi_hal_gpio_read(gpio_pin); - + // get level + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + bool value = furi_hal_gpio_read(manager_data->pin); mjs_return(mjs, mjs_mk_boolean(mjs, value)); } +/** + * @brief Returns a event loop contract that can be used to listen to interrupts + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let button = gpio.get("pc1"); + * let event_loop = require("event_loop"); + * button.init({ direction: "in", pull: "up", inMode: "interrupt", edge: "falling" }); + * event_loop.subscribe(button.interrupt(), function (_) { print("Hi!"); }); + * event_loop.run(); + * ``` + */ +static void js_gpio_interrupt(struct mjs* mjs) { + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + + // interrupt handling + if(!manager_data->had_interrupt) { + furi_hal_gpio_add_int_callback( + manager_data->pin, js_gpio_int_cb, manager_data->interrupt_semaphore); + furi_hal_gpio_enable_int_callback(manager_data->pin); + manager_data->had_interrupt = true; + } + + // make contract + JsEventLoopContract* contract = malloc(sizeof(JsEventLoopContract)); + *contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeSemaphore, + .object = manager_data->interrupt_semaphore, + .non_timer = + { + .event = FuriEventLoopEventIn, + }, + }; + manager_data->interrupt_contract = contract; + mjs_return(mjs, mjs_mk_foreign(mjs, contract)); +} + +/** + * @brief Reads a voltage from a GPIO pin in analog mode + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let pot = gpio.get("pc0"); + * pot.init({ direction: "in", inMode: "analog" }); + * print("voltage:" pot.read_analog(), "mV"); + * ``` + */ static void js_gpio_read_analog(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst); - furi_assert(gpio); - - if(gpio->handle == NULL) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - mjs_val_t pin_arg = mjs_arg(mjs, 0); - - if(!mjs_is_string(pin_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a string"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - const char* pin_name = mjs_get_string(mjs, &pin_arg, NULL); - if(!pin_name) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Failed to get pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - FuriHalAdcChannel channel = js_gpio_get_gpio_channel(pin_name); - if(channel == FuriHalAdcChannelNone) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid pin name"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - uint16_t adc_value = furi_hal_adc_read(gpio->handle, channel); - float adc_mv = furi_hal_adc_convert_to_voltage(gpio->handle, adc_value); - - mjs_return(mjs, mjs_mk_number(mjs, adc_mv)); + // get mV (ADC is configured for 12 bits and 2048 mV max) + JsGpioPinInst* manager_data = JS_GET_CONTEXT(mjs); + uint16_t millivolts = + furi_hal_adc_read(manager_data->adc_handle, manager_data->adc_channel) / 2; + mjs_return(mjs, mjs_mk_number(mjs, (double)millivolts)); } -static void js_gpio_start_analog(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst); - furi_assert(gpio); +/** + * @brief Returns an object that manages a specified pin. + * + * Example usage: + * + * ```js + * let gpio = require("gpio"); + * let led = gpio.get("pc3"); + * ``` + */ +static void js_gpio_get(struct mjs* mjs) { + mjs_val_t name_arg; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&name_arg)); + const char* name_string = mjs_get_string(mjs, &name_arg, NULL); + const GpioPinRecord* pin_record = NULL; - FuriHalAdcScale scale = FuriHalAdcScale2048; - if(mjs_nargs(mjs) > 0) { - mjs_val_t scale_arg = mjs_arg(mjs, 0); - - if(!mjs_is_number(scale_arg)) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Argument must be a number"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - int32_t scale_num = mjs_get_int32(mjs, scale_arg); - if(scale_num == 2048 || scale_num == 2000) { // 2 volt reference - scale = FuriHalAdcScale2048; - } else if(scale_num == 2500) { // 2.5 volt reference - scale = FuriHalAdcScale2500; - } else { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "Invalid scale"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + // parse input argument to a pin pointer + if(name_string) { + pin_record = furi_hal_resources_pin_by_name(name_string); + } else if(mjs_is_number(name_arg)) { + int name_int = mjs_get_int(mjs, name_arg); + pin_record = furi_hal_resources_pin_by_number(name_int); + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Must be either a string or a number"); } - if(gpio->handle != NULL) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode already started"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + if(!pin_record) JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin not found on device"); + if(pin_record->debug) + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "Pin is used for debugging"); - gpio->handle = furi_hal_adc_acquire(); - furi_hal_adc_configure_ex( - gpio->handle, - scale, - FuriHalAdcClockSync64, - FuriHalAdcOversample64, - FuriHalAdcSamplingtime247_5); + // return pin manager object + JsGpioInst* module = JS_GET_CONTEXT(mjs); + mjs_val_t manager = mjs_mk_object(mjs); + JsGpioPinInst* manager_data = malloc(sizeof(JsGpioPinInst)); + manager_data->pin = pin_record->pin; + manager_data->interrupt_semaphore = furi_semaphore_alloc(UINT32_MAX, 0); + manager_data->adc_handle = module->adc_handle; + manager_data->adc_channel = pin_record->channel; + mjs_own(mjs, &manager); + mjs_set(mjs, manager, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, manager_data)); + mjs_set(mjs, manager, "init", ~0, MJS_MK_FN(js_gpio_init)); + mjs_set(mjs, manager, "write", ~0, MJS_MK_FN(js_gpio_write)); + mjs_set(mjs, manager, "read", ~0, MJS_MK_FN(js_gpio_read)); + mjs_set(mjs, manager, "read_analog", ~0, MJS_MK_FN(js_gpio_read_analog)); + mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); + mjs_return(mjs, manager); + + // remember pin + ManagedPinsArray_push_back(module->managed_pins, manager_data); } -static void js_gpio_stop_analog(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsGpioInst* gpio = mjs_get_ptr(mjs, obj_inst); - furi_assert(gpio); +static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + JsEventLoop* js_loop = js_module_get(modules, "event_loop"); + if(M_UNLIKELY(!js_loop)) return NULL; + FuriEventLoop* loop = js_event_loop_get_loop(js_loop); - if(gpio->handle == NULL) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Analog mode not started"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } + JsGpioInst* module = malloc(sizeof(JsGpioInst)); + ManagedPinsArray_init(module->managed_pins); + module->adc_handle = furi_hal_adc_acquire(); + module->loop = loop; + furi_hal_adc_configure(module->adc_handle); - furi_hal_adc_release(gpio->handle); - gpio->handle = NULL; -} - -static void* js_gpio_create(struct mjs* mjs, mjs_val_t* object) { - JsGpioInst* gpio = malloc(sizeof(JsGpioInst)); - gpio->handle = NULL; mjs_val_t gpio_obj = mjs_mk_object(mjs); - mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, gpio)); - mjs_set(mjs, gpio_obj, "init", ~0, MJS_MK_FN(js_gpio_init)); - mjs_set(mjs, gpio_obj, "write", ~0, MJS_MK_FN(js_gpio_write)); - mjs_set(mjs, gpio_obj, "read", ~0, MJS_MK_FN(js_gpio_read)); - mjs_set(mjs, gpio_obj, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); - mjs_set(mjs, gpio_obj, "startAnalog", ~0, MJS_MK_FN(js_gpio_start_analog)); - mjs_set(mjs, gpio_obj, "stopAnalog", ~0, MJS_MK_FN(js_gpio_stop_analog)); + mjs_set(mjs, gpio_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, module)); + mjs_set(mjs, gpio_obj, "get", ~0, MJS_MK_FN(js_gpio_get)); *object = gpio_obj; - return (void*)gpio; + return (void*)module; } static void js_gpio_destroy(void* inst) { - if(inst != NULL) { - JsGpioInst* gpio = (JsGpioInst*)inst; - if(gpio->handle != NULL) { - furi_hal_adc_release(gpio->handle); - gpio->handle = NULL; + furi_assert(inst); + JsGpioInst* module = (JsGpioInst*)inst; + + // reset pins + ManagedPinsArray_it_t iterator; + for(ManagedPinsArray_it(iterator, module->managed_pins); !ManagedPinsArray_end_p(iterator); + ManagedPinsArray_next(iterator)) { + JsGpioPinInst* manager_data = *ManagedPinsArray_cref(iterator); + if(manager_data->had_interrupt) { + furi_hal_gpio_disable_int_callback(manager_data->pin); + furi_hal_gpio_remove_int_callback(manager_data->pin); } - free(gpio); + furi_hal_gpio_init(manager_data->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + furi_event_loop_maybe_unsubscribe(module->loop, manager_data->interrupt_semaphore); + furi_semaphore_free(manager_data->interrupt_semaphore); + free(manager_data->interrupt_contract); + free(manager_data); } - // loop through all pins and reset them to analog mode - for(size_t i = 0; i < COUNT_OF(js_gpio_pins); i++) { - furi_hal_gpio_write(js_gpio_pins[i].pin, false); - furi_hal_gpio_init(js_gpio_pins[i].pin, GpioModeAnalog, GpioPullNo, GpioSpeedVeryHigh); - } - - expansion_enable(furi_record_open(RECORD_EXPANSION)); - furi_record_close(RECORD_EXPANSION); + // free buffers + furi_hal_adc_release(module->adc_handle); + ManagedPinsArray_clear(module->managed_pins); + free(module); } static const JsModuleDescriptor js_gpio_desc = { "gpio", js_gpio_create, js_gpio_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_gui/dialog.c b/applications/system/js_app/modules/js_gui/dialog.c new file mode 100644 index 000000000..31eee237f --- /dev/null +++ b/applications/system/js_app/modules/js_gui/dialog.c @@ -0,0 +1,129 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#define QUEUE_LEN 2 + +typedef struct { + FuriMessageQueue* queue; + JsEventLoopContract contract; +} JsDialogCtx; + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) { + UNUSED(context); + DialogExResult result; + furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk); + const char* string; + if(result == DialogExResultLeft) { + string = "left"; + } else if(result == DialogExResultCenter) { + string = "center"; + } else if(result == DialogExResultRight) { + string = "right"; + } else { + furi_crash(); + } + return mjs_mk_string(mjs, string, ~0, false); +} + +static void input_callback(DialogExResult result, JsDialogCtx* context) { + furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk); +} + +static bool + header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) { + UNUSED(mjs); + UNUSED(context); + dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop); + return true; +} + +static bool + text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) { + UNUSED(mjs); + UNUSED(context); + dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter); + return true; +} + +static bool + left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) { + UNUSED(mjs); + UNUSED(context); + dialog_ex_set_left_button_text(dialog, value.string); + return true; +} +static bool + center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) { + UNUSED(mjs); + UNUSED(context); + dialog_ex_set_center_button_text(dialog, value.string); + return true; +} +static bool + right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) { + UNUSED(mjs); + UNUSED(context); + dialog_ex_set_right_button_text(dialog, value.string); + return true; +} + +static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) { + JsDialogCtx* context = malloc(sizeof(JsDialogCtx)); + context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult)); + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + }, + }; + mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); + dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback); + dialog_ex_set_context(dialog, context); + return context; +} + +static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->queue); + furi_message_queue_free(context->queue); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)dialog_ex_alloc, + .free = (JsViewFree)dialog_ex_free, + .get_view = (JsViewGetView)dialog_ex_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 5, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)header_assign}, + (JsViewPropDescriptor){ + .name = "text", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)text_assign}, + (JsViewPropDescriptor){ + .name = "left", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)left_assign}, + (JsViewPropDescriptor){ + .name = "center", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)center_assign}, + (JsViewPropDescriptor){ + .name = "right", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)right_assign}, + }}; + +JS_GUI_VIEW_DEF(dialog, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/empty_screen.c b/applications/system/js_app/modules/js_gui/empty_screen.c new file mode 100644 index 000000000..9684eabdc --- /dev/null +++ b/applications/system/js_app/modules/js_gui/empty_screen.c @@ -0,0 +1,12 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)empty_screen_alloc, + .free = (JsViewFree)empty_screen_free, + .get_view = (JsViewGetView)empty_screen_get_view, + .prop_cnt = 0, + .props = {}, +}; +JS_GUI_VIEW_DEF(empty_screen, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/js_gui.c b/applications/system/js_app/modules/js_gui/js_gui.c new file mode 100644 index 000000000..8ac3055d5 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui.c @@ -0,0 +1,348 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "./js_gui.h" +#include +#include +#include +#include "../js_event_loop/js_event_loop.h" +#include + +#define EVENT_QUEUE_SIZE 16 + +typedef struct { + uint32_t next_view_id; + FuriEventLoop* loop; + Gui* gui; + ViewDispatcher* dispatcher; + // event stuff + JsEventLoopContract custom_contract; + FuriMessageQueue* custom; + JsEventLoopContract navigation_contract; + FuriSemaphore* + navigation; // FIXME: (-nofl) convert into callback once FuriEventLoop starts supporting this +} JsGui; + +// Useful for factories +static JsGui* js_gui; + +typedef struct { + uint32_t id; + const JsViewDescriptor* descriptor; + void* specific_view; + void* custom_data; +} JsGuiViewData; + +/** + * @brief Transformer for custom events + */ +static mjs_val_t + js_gui_vd_custom_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) { + UNUSED(context); + furi_check(object); + FuriMessageQueue* queue = object; + uint32_t event; + furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk); + return mjs_mk_number(mjs, (double)event); +} + +/** + * @brief ViewDispatcher custom event callback + */ +static bool js_gui_vd_custom_callback(void* context, uint32_t event) { + furi_check(context); + JsGui* module = context; + furi_check(furi_message_queue_put(module->custom, &event, 0) == FuriStatusOk); + return true; +} + +/** + * @brief ViewDispatcher navigation event callback + */ +static bool js_gui_vd_nav_callback(void* context) { + furi_check(context); + JsGui* module = context; + furi_semaphore_release(module->navigation); + return true; +} + +/** + * @brief `viewDispatcher.sendCustom` + */ +static void js_gui_vd_send_custom(struct mjs* mjs) { + int32_t event; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event)); + + JsGui* module = JS_GET_CONTEXT(mjs); + view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event); +} + +/** + * @brief `viewDispatcher.sendTo` + */ +static void js_gui_vd_send_to(struct mjs* mjs) { + enum { + SendDirToFront, + SendDirToBack, + } send_direction; + JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack}); + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection")); + + JsGui* module = JS_GET_CONTEXT(mjs); + if(send_direction == SendDirToBack) { + view_dispatcher_send_to_back(module->dispatcher); + } else { + view_dispatcher_send_to_front(module->dispatcher); + } +} + +/** + * @brief `viewDispatcher.switchTo` + */ +static void js_gui_vd_switch_to(struct mjs* mjs) { + mjs_val_t view; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view)); + JsGuiViewData* view_data = JS_GET_INST(mjs, view); + JsGui* module = JS_GET_CONTEXT(mjs); + view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id); +} + +static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + // get event loop + JsEventLoop* js_loop = js_module_get(modules, "event_loop"); + if(M_UNLIKELY(!js_loop)) return NULL; + FuriEventLoop* loop = js_event_loop_get_loop(js_loop); + + // create C object + JsGui* module = malloc(sizeof(JsGui)); + module->loop = loop; + module->gui = furi_record_open(RECORD_GUI); + module->dispatcher = view_dispatcher_alloc_ex(loop); + module->custom = furi_message_queue_alloc(EVENT_QUEUE_SIZE, sizeof(uint32_t)); + module->navigation = furi_semaphore_alloc(EVENT_QUEUE_SIZE, 0); + view_dispatcher_attach_to_gui(module->dispatcher, module->gui, ViewDispatcherTypeFullscreen); + view_dispatcher_send_to_front(module->dispatcher); + + // subscribe to events and create contracts + view_dispatcher_set_event_callback_context(module->dispatcher, module); + view_dispatcher_set_custom_event_callback(module->dispatcher, js_gui_vd_custom_callback); + view_dispatcher_set_navigation_event_callback(module->dispatcher, js_gui_vd_nav_callback); + module->custom_contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object = module->custom, + .object_type = JsEventLoopObjectTypeQueue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = js_gui_vd_custom_transformer, + }, + }; + module->navigation_contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object = module->navigation, + .object_type = JsEventLoopObjectTypeSemaphore, + .non_timer = + { + .event = FuriEventLoopEventIn, + }, + }; + + // create viewDispatcher object + mjs_val_t view_dispatcher = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, view_dispatcher) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module)); + JS_FIELD("sendCustom", MJS_MK_FN(js_gui_vd_send_custom)); + JS_FIELD("sendTo", MJS_MK_FN(js_gui_vd_send_to)); + JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to)); + JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract)); + JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract)); + } + + // create API object + mjs_val_t api = mjs_mk_object(mjs); + mjs_set(mjs, api, "viewDispatcher", ~0, view_dispatcher); + + *object = api; + js_gui = module; + return module; +} + +static void js_gui_destroy(void* inst) { + furi_assert(inst); + JsGui* module = inst; + + view_dispatcher_free(module->dispatcher); + furi_event_loop_maybe_unsubscribe(module->loop, module->custom); + furi_event_loop_maybe_unsubscribe(module->loop, module->navigation); + furi_message_queue_free(module->custom); + furi_semaphore_free(module->navigation); + + furi_record_close(RECORD_GUI); + free(module); + js_gui = NULL; +} + +/** + * @brief Assigns a `View` property. Not available from JS. + */ +static bool + js_gui_view_assign(struct mjs* mjs, const char* name, mjs_val_t value, JsGuiViewData* data) { + const JsViewDescriptor* descriptor = data->descriptor; + for(size_t i = 0; i < descriptor->prop_cnt; i++) { + JsViewPropDescriptor prop = descriptor->props[i]; + if(strcmp(prop.name, name) != 0) continue; + + // convert JS value to C + JsViewPropValue c_value; + const char* expected_type = NULL; + switch(prop.type) { + case JsViewPropTypeNumber: { + if(!mjs_is_number(value)) { + expected_type = "number"; + break; + } + c_value = (JsViewPropValue){.number = mjs_get_int32(mjs, value)}; + } break; + case JsViewPropTypeString: { + if(!mjs_is_string(value)) { + expected_type = "string"; + break; + } + c_value = (JsViewPropValue){.string = mjs_get_string(mjs, &value, NULL)}; + } break; + case JsViewPropTypeArr: { + if(!mjs_is_array(value)) { + expected_type = "array"; + break; + } + c_value = (JsViewPropValue){.array = value}; + } break; + } + + if(expected_type) { + mjs_prepend_errorf( + mjs, MJS_BAD_ARGS_ERROR, "view prop \"%s\" requires %s value", name, expected_type); + return false; + } else { + return prop.assign(mjs, data->specific_view, c_value, data->custom_data); + } + } + + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "view has no prop named \"%s\"", name); + return false; +} + +/** + * @brief `View.set` + */ +static void js_gui_view_set(struct mjs* mjs) { + const char* name; + mjs_val_t value; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value)); + JsGuiViewData* data = JS_GET_CONTEXT(mjs); + bool success = js_gui_view_assign(mjs, name, value, data); + UNUSED(success); + mjs_return(mjs, MJS_UNDEFINED); +} + +/** + * @brief `View` destructor + */ +static void js_gui_view_destructor(struct mjs* mjs, mjs_val_t obj) { + JsGuiViewData* data = JS_GET_INST(mjs, obj); + view_dispatcher_remove_view(js_gui->dispatcher, data->id); + if(data->descriptor->custom_destroy) + data->descriptor->custom_destroy(data->specific_view, data->custom_data, js_gui->loop); + data->descriptor->free(data->specific_view); + free(data); +} + +/** + * @brief Creates a `View` object from a descriptor. Not available from JS. + */ +static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descriptor) { + void* specific_view = descriptor->alloc(); + View* view = descriptor->get_view(specific_view); + uint32_t view_id = js_gui->next_view_id++; + view_dispatcher_add_view(js_gui->dispatcher, view_id, view); + + // generic view API + mjs_val_t view_obj = mjs_mk_object(mjs); + mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set)); + + // object data + JsGuiViewData* data = malloc(sizeof(JsGuiViewData)); + *data = (JsGuiViewData){ + .descriptor = descriptor, + .id = view_id, + .specific_view = specific_view, + .custom_data = + descriptor->custom_make ? descriptor->custom_make(mjs, specific_view, view_obj) : NULL, + }; + mjs_set(mjs, view_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, data)); + mjs_set(mjs, view_obj, MJS_DESTRUCTOR_PROP_NAME, ~0, MJS_MK_FN(js_gui_view_destructor)); + + return view_obj; +} + +/** + * @brief `ViewFactory.make` + */ +static void js_gui_vf_make(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); + mjs_return(mjs, js_gui_make_view(mjs, descriptor)); +} + +/** + * @brief `ViewFactory.makeWith` + */ +static void js_gui_vf_make_with(struct mjs* mjs) { + mjs_val_t props; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props)); + const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs); + + // make the object like normal + mjs_val_t view_obj = js_gui_make_view(mjs, descriptor); + JsGuiViewData* data = JS_GET_INST(mjs, view_obj); + + // assign properties one by one + mjs_val_t key, iter = MJS_UNDEFINED; + while((key = mjs_next(mjs, props, &iter)) != MJS_UNDEFINED) { + furi_check(mjs_is_string(key)); + const char* name = mjs_get_string(mjs, &key, NULL); + mjs_val_t value = mjs_get(mjs, props, name, ~0); + + if(!js_gui_view_assign(mjs, name, value, data)) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + } + + mjs_return(mjs, view_obj); +} + +mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor) { + mjs_val_t factory = mjs_mk_object(mjs); + mjs_set(mjs, factory, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, (void*)view_descriptor)); + mjs_set(mjs, factory, "make", ~0, MJS_MK_FN(js_gui_vf_make)); + mjs_set(mjs, factory, "makeWith", ~0, MJS_MK_FN(js_gui_vf_make_with)); + return factory; +} + +extern const ElfApiInterface js_gui_hashtable_api_interface; + +static const JsModuleDescriptor js_gui_desc = { + "gui", + js_gui_create, + js_gui_destroy, + &js_gui_hashtable_api_interface, +}; + +static const FlipperAppPluginDescriptor plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_gui_desc, +}; + +const FlipperAppPluginDescriptor* js_gui_ep(void) { + return &plugin_descriptor; +} diff --git a/applications/system/js_app/modules/js_gui/js_gui.h b/applications/system/js_app/modules/js_gui/js_gui.h new file mode 100644 index 000000000..02198ca4f --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui.h @@ -0,0 +1,116 @@ +#include "../../js_modules.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + JsViewPropTypeString, + JsViewPropTypeNumber, + JsViewPropTypeArr, +} JsViewPropType; + +typedef union { + const char* string; + int32_t number; + mjs_val_t array; +} JsViewPropValue; + +/** + * @brief Assigns a value to a view property + * + * The name and the type are implicit and defined in the property descriptor + */ +typedef bool ( + *JsViewPropAssign)(struct mjs* mjs, void* specific_view, JsViewPropValue value, void* context); + +/** @brief Property descriptor */ +typedef struct { + const char* name; // get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free +// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/ + +/** + * @brief Creates a JS `ViewFactory` object + * + * This function is intended to be used by individual view adapter modules that + * wish to create a unified JS API interface in a declarative way. Usually this + * is done via the `JS_GUI_VIEW_DEF` macro which hides all the boilerplate. + * + * The `ViewFactory` object exposes two methods, `make` and `makeWith`, each + * returning a `View` object. These objects fully comply with the expectations + * of the `ViewDispatcher`, TS type definitions and the proposed Flipper JS + * coding style. + */ +mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor); + +/** + * @brief Defines a module implementing `View` glue code + */ +#define JS_GUI_VIEW_DEF(name, descriptor) \ + static void* view_mod_ctor(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { \ + UNUSED(modules); \ + *object = js_gui_make_view_factory(mjs, descriptor); \ + return NULL; \ + } \ + static const JsModuleDescriptor js_mod_desc = { \ + "gui__" #name, \ + view_mod_ctor, \ + NULL, \ + NULL, \ + }; \ + static const FlipperAppPluginDescriptor plugin_descriptor = { \ + .appid = PLUGIN_APP_ID, \ + .ep_api_version = PLUGIN_API_VERSION, \ + .entry_point = &js_mod_desc, \ + }; \ + const FlipperAppPluginDescriptor* js_view_##name##_ep(void) { \ + return &plugin_descriptor; \ + } + +#ifdef __cplusplus +} +#endif diff --git a/applications/system/js_app/modules/js_gui/js_gui_api_table.cpp b/applications/system/js_app/modules/js_gui/js_gui_api_table.cpp new file mode 100644 index 000000000..2be9cb3b2 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui_api_table.cpp @@ -0,0 +1,16 @@ +#include +#include + +#include "js_gui_api_table_i.h" + +static_assert(!has_hash_collisions(js_gui_api_table), "Detected API method hash collision!"); + +extern "C" constexpr HashtableApiInterface js_gui_hashtable_api_interface{ + { + .api_version_major = 0, + .api_version_minor = 0, + .resolver_callback = &elf_resolve_from_hashtable, + }, + js_gui_api_table.cbegin(), + js_gui_api_table.cend(), +}; diff --git a/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h b/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h new file mode 100644 index 000000000..852b3d107 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/js_gui_api_table_i.h @@ -0,0 +1,4 @@ +#include "js_gui.h" + +static constexpr auto js_gui_api_table = sort(create_array_t( + API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)))); diff --git a/applications/system/js_app/modules/js_gui/loading.c b/applications/system/js_app/modules/js_gui/loading.c new file mode 100644 index 000000000..e291824a0 --- /dev/null +++ b/applications/system/js_app/modules/js_gui/loading.c @@ -0,0 +1,12 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)loading_alloc, + .free = (JsViewFree)loading_free, + .get_view = (JsViewGetView)loading_get_view, + .prop_cnt = 0, + .props = {}, +}; +JS_GUI_VIEW_DEF(loading, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/submenu.c b/applications/system/js_app/modules/js_gui/submenu.c new file mode 100644 index 000000000..aecd413be --- /dev/null +++ b/applications/system/js_app/modules/js_gui/submenu.c @@ -0,0 +1,87 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#define QUEUE_LEN 2 + +typedef struct { + FuriMessageQueue* queue; + JsEventLoopContract contract; +} JsSubmenuCtx; + +static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) { + UNUSED(context); + uint32_t index; + furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk); + return mjs_mk_number(mjs, (double)index); +} + +void choose_callback(void* context, uint32_t index) { + JsSubmenuCtx* ctx = context; + furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk); +} + +static bool + header_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { + UNUSED(mjs); + UNUSED(context); + submenu_set_header(submenu, value.string); + return true; +} + +static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) { + UNUSED(mjs); + submenu_reset(submenu); + size_t len = mjs_array_length(mjs, value.array); + for(size_t i = 0; i < len; i++) { + mjs_val_t item = mjs_array_get(mjs, value.array, i); + if(!mjs_is_string(item)) return false; + submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context); + } + return true; +} + +static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) { + UNUSED(input); + JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx)); + context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(uint32_t)); + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeQueue, + .object = context->queue, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)choose_transformer, + }, + }; + mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(Submenu* input, JsSubmenuCtx* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->queue); + furi_message_queue_free(context->queue); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)submenu_alloc, + .free = (JsViewFree)submenu_free, + .get_view = (JsViewGetView)submenu_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 2, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)header_assign}, + (JsViewPropDescriptor){ + .name = "items", + .type = JsViewPropTypeArr, + .assign = (JsViewPropAssign)items_assign}, + }}; +JS_GUI_VIEW_DEF(submenu, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/text_box.c b/applications/system/js_app/modules/js_gui/text_box.c new file mode 100644 index 000000000..4e6c8247c --- /dev/null +++ b/applications/system/js_app/modules/js_gui/text_box.c @@ -0,0 +1,78 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include + +static bool + text_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, FuriString* context) { + UNUSED(mjs); + furi_string_set(context, value.string); + text_box_set_text(text_box, furi_string_get_cstr(context)); + return true; +} + +static bool font_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) { + UNUSED(context); + TextBoxFont font; + if(strcasecmp(value.string, "hex") == 0) { + font = TextBoxFontHex; + } else if(strcasecmp(value.string, "text") == 0) { + font = TextBoxFontText; + } else { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"text\", \"hex\""); + return false; + } + text_box_set_font(text_box, font); + return true; +} + +static bool + focus_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) { + UNUSED(context); + TextBoxFocus focus; + if(strcasecmp(value.string, "start") == 0) { + focus = TextBoxFocusStart; + } else if(strcasecmp(value.string, "end") == 0) { + focus = TextBoxFocusEnd; + } else { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"start\", \"end\""); + return false; + } + text_box_set_focus(text_box, focus); + return true; +} + +FuriString* ctx_make(struct mjs* mjs, TextBox* specific_view, mjs_val_t view_obj) { + UNUSED(mjs); + UNUSED(specific_view); + UNUSED(view_obj); + return furi_string_alloc(); +} + +void ctx_destroy(TextBox* specific_view, FuriString* context, FuriEventLoop* loop) { + UNUSED(specific_view); + UNUSED(loop); + furi_string_free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)text_box_alloc, + .free = (JsViewFree)text_box_free, + .get_view = (JsViewGetView)text_box_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 3, + .props = { + (JsViewPropDescriptor){ + .name = "text", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)text_assign}, + (JsViewPropDescriptor){ + .name = "font", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)font_assign}, + (JsViewPropDescriptor){ + .name = "focus", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)focus_assign}, + }}; +JS_GUI_VIEW_DEF(text_box, &view_descriptor); diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c new file mode 100644 index 000000000..575029f8e --- /dev/null +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -0,0 +1,120 @@ +#include "../../js_modules.h" // IWYU pragma: keep +#include "js_gui.h" +#include "../js_event_loop/js_event_loop.h" +#include + +#define DEFAULT_BUF_SZ 33 + +typedef struct { + char* buffer; + size_t buffer_size; + FuriString* header; + FuriSemaphore* input_semaphore; + JsEventLoopContract contract; +} JsKbdContext; + +static mjs_val_t + input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsKbdContext* context) { + furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk); + return mjs_mk_string(mjs, context->buffer, ~0, true); +} + +static void input_callback(JsKbdContext* context) { + furi_semaphore_release(context->input_semaphore); +} + +static bool + header_assign(struct mjs* mjs, TextInput* input, JsViewPropValue value, JsKbdContext* context) { + UNUSED(mjs); + furi_string_set(context->header, value.string); + text_input_set_header_text(input, furi_string_get_cstr(context->header)); + return true; +} + +static bool min_len_assign( + struct mjs* mjs, + TextInput* input, + JsViewPropValue value, + JsKbdContext* context) { + UNUSED(mjs); + UNUSED(context); + text_input_set_minimum_length(input, (size_t)value.number); + return true; +} + +static bool max_len_assign( + struct mjs* mjs, + TextInput* input, + JsViewPropValue value, + JsKbdContext* context) { + UNUSED(mjs); + context->buffer_size = (size_t)(value.number + 1); + context->buffer = realloc(context->buffer, context->buffer_size); //-V701 + text_input_set_result_callback( + input, + (TextInputCallback)input_callback, + context, + context->buffer, + context->buffer_size, + true); + return true; +} + +static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) { + UNUSED(input); + JsKbdContext* context = malloc(sizeof(JsKbdContext)); + *context = (JsKbdContext){ + .buffer_size = DEFAULT_BUF_SZ, + .buffer = malloc(DEFAULT_BUF_SZ), + .header = furi_string_alloc(), + .input_semaphore = furi_semaphore_alloc(1, 0), + }; + context->contract = (JsEventLoopContract){ + .magic = JsForeignMagic_JsEventLoopContract, + .object_type = JsEventLoopObjectTypeSemaphore, + .object = context->input_semaphore, + .non_timer = + { + .event = FuriEventLoopEventIn, + .transformer = (JsEventLoopTransformer)input_transformer, + .transformer_context = context, + }, + }; + UNUSED(mjs); + UNUSED(view_obj); + mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract)); + return context; +} + +static void ctx_destroy(TextInput* input, JsKbdContext* context, FuriEventLoop* loop) { + UNUSED(input); + furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore); + furi_semaphore_free(context->input_semaphore); + furi_string_free(context->header); + free(context->buffer); + free(context); +} + +static const JsViewDescriptor view_descriptor = { + .alloc = (JsViewAlloc)text_input_alloc, + .free = (JsViewFree)text_input_free, + .get_view = (JsViewGetView)text_input_get_view, + .custom_make = (JsViewCustomMake)ctx_make, + .custom_destroy = (JsViewCustomDestroy)ctx_destroy, + .prop_cnt = 3, + .props = { + (JsViewPropDescriptor){ + .name = "header", + .type = JsViewPropTypeString, + .assign = (JsViewPropAssign)header_assign}, + (JsViewPropDescriptor){ + .name = "minLength", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)min_len_assign}, + (JsViewPropDescriptor){ + .name = "maxLength", + .type = JsViewPropTypeNumber, + .assign = (JsViewPropAssign)max_len_assign}, + }}; + +JS_GUI_VIEW_DEF(text_input, &view_descriptor); diff --git a/applications/system/js_app/modules/js_math.c b/applications/system/js_app/modules/js_math.c index d8812e61b..7d54cf9b9 100644 --- a/applications/system/js_app/modules/js_math.c +++ b/applications/system/js_app/modules/js_math.c @@ -305,7 +305,8 @@ void js_math_trunc(struct mjs* mjs) { mjs_return(mjs, mjs_mk_number(mjs, x < (double)0. ? ceil(x) : floor(x))); } -static void* js_math_create(struct mjs* mjs, mjs_val_t* object) { +static void* js_math_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); mjs_val_t math_obj = mjs_mk_object(mjs); mjs_set(mjs, math_obj, "is_equal", ~0, MJS_MK_FN(js_math_is_equal)); mjs_set(mjs, math_obj, "abs", ~0, MJS_MK_FN(js_math_abs)); @@ -342,6 +343,7 @@ static const JsModuleDescriptor js_math_desc = { "math", js_math_create, NULL, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_notification.c b/applications/system/js_app/modules/js_notification.c index 2f57c45d1..994283a09 100644 --- a/applications/system/js_app/modules/js_notification.c +++ b/applications/system/js_app/modules/js_notification.c @@ -75,7 +75,8 @@ static void js_notify_blink(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } -static void* js_notification_create(struct mjs* mjs, mjs_val_t* object) { +static void* js_notification_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); mjs_val_t notify_obj = mjs_mk_object(mjs); mjs_set(mjs, notify_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, notification)); @@ -96,6 +97,7 @@ static const JsModuleDescriptor js_notification_desc = { "notification", js_notification_create, js_notification_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_serial.c b/applications/system/js_app/modules/js_serial.c index 293798a12..b1e578fbc 100644 --- a/applications/system/js_app/modules/js_serial.c +++ b/applications/system/js_app/modules/js_serial.c @@ -658,7 +658,8 @@ static void js_serial_expect(struct mjs* mjs) { } } -static void* js_serial_create(struct mjs* mjs, mjs_val_t* object) { +static void* js_serial_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); JsSerialInst* js_serial = malloc(sizeof(JsSerialInst)); js_serial->mjs = mjs; mjs_val_t serial_obj = mjs_mk_object(mjs); @@ -686,6 +687,7 @@ static const JsModuleDescriptor js_serial_desc = { "serial", js_serial_create, js_serial_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_storage.c b/applications/system/js_app/modules/js_storage.c index 67c17c486..1d4053a5f 100644 --- a/applications/system/js_app/modules/js_storage.c +++ b/applications/system/js_app/modules/js_storage.c @@ -1,335 +1,375 @@ -#include "../js_modules.h" -#include +#include "../js_modules.h" // IWYU pragma: keep +#include -typedef struct { - Storage* api; - File* virtual; -} JsStorageInst; +// ---=== file ops ===--- -static JsStorageInst* get_this_ctx(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsStorageInst* storage = mjs_get_ptr(mjs, obj_inst); - furi_assert(storage); - return storage; +static void js_storage_file_close(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_close(file))); } -static void ret_bad_args(struct mjs* mjs, const char* error) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); - mjs_return(mjs, MJS_UNDEFINED); +static void js_storage_file_is_open(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_is_open(file))); } -static void ret_int_err(struct mjs* mjs, const char* error) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", error); - mjs_return(mjs, MJS_UNDEFINED); -} - -static bool check_arg_count(struct mjs* mjs, size_t count) { - size_t num_args = mjs_nargs(mjs); - if(num_args != count) { - ret_bad_args(mjs, "Wrong argument count"); - return false; +static void js_storage_file_read(struct mjs* mjs) { + enum { + ReadModeAscii, + ReadModeBinary, + } read_mode; + JS_ENUM_MAP(read_mode, {"ascii", ReadModeAscii}, {"binary", ReadModeBinary}); + int32_t length; + JS_FETCH_ARGS_OR_RETURN( + mjs, JS_EXACTLY, JS_ARG_ENUM(read_mode, "ReadMode"), JS_ARG_INT32(&length)); + File* file = JS_GET_CONTEXT(mjs); + char buffer[length]; + size_t actually_read = storage_file_read(file, buffer, length); + if(read_mode == ReadModeAscii) { + mjs_return(mjs, mjs_mk_string(mjs, buffer, actually_read, true)); + } else if(read_mode == ReadModeBinary) { + mjs_return(mjs, mjs_mk_array_buf(mjs, buffer, actually_read)); } - return true; } -static bool get_path_arg(struct mjs* mjs, const char** path, size_t index) { - mjs_val_t path_obj = mjs_arg(mjs, index); - if(!mjs_is_string(path_obj)) { - ret_bad_args(mjs, "Path must be a string"); - return false; +static void js_storage_file_write(struct mjs* mjs) { + mjs_val_t data; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&data)); + const void* buf; + size_t len; + if(mjs_is_string(data)) { + buf = mjs_get_string(mjs, &data, &len); + } else if(mjs_is_array_buf(data)) { + buf = mjs_array_buf_get_ptr(mjs, data, &len); + } else { + JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 0: expected string or ArrayBuffer"); } - size_t path_len = 0; - *path = mjs_get_string(mjs, &path_obj, &path_len); - if((path_len == 0) || (*path == NULL)) { - ret_bad_args(mjs, "Bad path argument"); - return false; - } - return true; + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_write(file, buf, len))); } -static void js_storage_read(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); +static void js_storage_file_seek_relative(struct mjs* mjs) { + int32_t offset; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, false))); +} - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; +static void js_storage_file_seek_absolute(struct mjs* mjs) { + int32_t offset; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&offset)); + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_seek(file, offset, true))); +} - File* file = storage_file_alloc(storage->api); - do { - if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { - ret_int_err(mjs, storage_file_get_error_desc(file)); - break; - } +static void js_storage_file_tell(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_tell(file))); +} - uint64_t size = storage_file_size(file); - mjs_val_t size_arg = mjs_arg(mjs, 1); - if(mjs_is_number(size_arg)) { - size = mjs_get_int32(mjs, size_arg); - } +static void js_storage_file_truncate(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_truncate(file))); +} - mjs_val_t seek_arg = mjs_arg(mjs, 2); - if(mjs_is_number(seek_arg)) { - storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true); - size = MIN(size, storage_file_size(file) - storage_file_tell(file)); - } +static void js_storage_file_size(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_number(mjs, storage_file_size(file))); +} - if(size > memmgr_heap_get_max_free_block()) { - ret_int_err(mjs, "Read size too large"); - break; - } +static void js_storage_file_eof(struct mjs* mjs) { + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args + File* file = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_eof(file))); +} - uint8_t* data = malloc(size); - size_t read = storage_file_read(file, data, size); - if(read == size) { - mjs_return(mjs, mjs_mk_array_buf(mjs, (char*)data, size)); - } else { - ret_int_err(mjs, "File read failed"); - } - free(data); - } while(0); +static void js_storage_file_copy_to(struct mjs* mjs) { + File* source = JS_GET_CONTEXT(mjs); + mjs_val_t dest_obj; + int32_t bytes; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&dest_obj), JS_ARG_INT32(&bytes)); + File* destination = JS_GET_INST(mjs, dest_obj); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_copy_to_file(source, destination, bytes))); +} + +// ---=== top-level file ops ===--- + +// common destructor for file and dir objects +static void js_storage_file_destructor(struct mjs* mjs, mjs_val_t obj) { + File* file = JS_GET_INST(mjs, obj); storage_file_free(file); } -static void js_storage_write(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - +static void js_storage_open_file(struct mjs* mjs) { const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + FS_AccessMode access_mode; + FS_OpenMode open_mode; + JS_ENUM_MAP(access_mode, {"r", FSAM_READ}, {"w", FSAM_WRITE}, {"rw", FSAM_READ_WRITE}); + JS_ENUM_MAP( + open_mode, + {"open_existing", FSOM_OPEN_EXISTING}, + {"open_always", FSOM_OPEN_ALWAYS}, + {"open_append", FSOM_OPEN_APPEND}, + {"create_new", FSOM_CREATE_NEW}, + {"create_always", FSOM_CREATE_ALWAYS}); + JS_FETCH_ARGS_OR_RETURN( + mjs, + JS_EXACTLY, + JS_ARG_STR(&path), + JS_ARG_ENUM(access_mode, "AccessMode"), + JS_ARG_ENUM(open_mode, "OpenMode")); - mjs_val_t data_arg = mjs_arg(mjs, 1); - if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) { - ret_bad_args(mjs, "Data must be string, arraybuf or dataview"); + Storage* storage = JS_GET_CONTEXT(mjs); + File* file = storage_file_alloc(storage); + if(!storage_file_open(file, path, access_mode, open_mode)) { + mjs_return(mjs, MJS_UNDEFINED); return; } - if(mjs_is_data_view(data_arg)) { - data_arg = mjs_dataview_get_buf(mjs, data_arg); + + mjs_val_t file_obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, file_obj) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, file)); + JS_FIELD(MJS_DESTRUCTOR_PROP_NAME, MJS_MK_FN(js_storage_file_destructor)); + JS_FIELD("close", MJS_MK_FN(js_storage_file_close)); + JS_FIELD("isOpen", MJS_MK_FN(js_storage_file_is_open)); + JS_FIELD("read", MJS_MK_FN(js_storage_file_read)); + JS_FIELD("write", MJS_MK_FN(js_storage_file_write)); + JS_FIELD("seekRelative", MJS_MK_FN(js_storage_file_seek_relative)); + JS_FIELD("seekAbsolute", MJS_MK_FN(js_storage_file_seek_absolute)); + JS_FIELD("tell", MJS_MK_FN(js_storage_file_tell)); + JS_FIELD("truncate", MJS_MK_FN(js_storage_file_truncate)); + JS_FIELD("size", MJS_MK_FN(js_storage_file_size)); + JS_FIELD("eof", MJS_MK_FN(js_storage_file_eof)); + JS_FIELD("copyTo", MJS_MK_FN(js_storage_file_copy_to)); } - size_t data_len = 0; - const char* data = NULL; - if(mjs_is_string(data_arg)) { - data = mjs_get_string(mjs, &data_arg, &data_len); - } else if(mjs_is_typed_array(data_arg)) { - data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); - } - - mjs_val_t seek_arg = mjs_arg(mjs, 2); - - File* file = storage_file_alloc(storage->api); - if(!storage_file_open( - file, - path, - FSAM_WRITE, - mjs_is_number(seek_arg) ? FSOM_OPEN_ALWAYS : FSOM_CREATE_ALWAYS)) { - ret_int_err(mjs, storage_file_get_error_desc(file)); - - } else { - if(mjs_is_number(seek_arg)) { - storage_file_seek(file, mjs_get_int32(mjs, seek_arg), true); - } - - size_t write = storage_file_write(file, data, data_len); - mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len)); - } - storage_file_free(file); + mjs_return(mjs, file_obj); } -static void js_storage_append(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; - +static void js_storage_file_exists(struct mjs* mjs) { const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_file_exists(storage, path))); +} - mjs_val_t data_arg = mjs_arg(mjs, 1); - if(!mjs_is_typed_array(data_arg) && !mjs_is_string(data_arg)) { - ret_bad_args(mjs, "Data must be string, arraybuf or dataview"); +// ---=== dir ops ===--- + +static void js_storage_read_directory(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + + Storage* storage = JS_GET_CONTEXT(mjs); + File* dir = storage_file_alloc(storage); + if(!storage_dir_open(dir, path)) { + mjs_return(mjs, MJS_UNDEFINED); return; } - if(mjs_is_data_view(data_arg)) { - data_arg = mjs_dataview_get_buf(mjs, data_arg); - } - size_t data_len = 0; - const char* data = NULL; - if(mjs_is_string(data_arg)) { - data = mjs_get_string(mjs, &data_arg, &data_len); - } else if(mjs_is_typed_array(data_arg)) { - data = mjs_array_buf_get_ptr(mjs, data_arg, &data_len); + + FileInfo file_info; + char name[128]; + FuriString* file_path = furi_string_alloc_set_str(path); + size_t path_size = furi_string_size(file_path); + uint32_t timestamp; + + mjs_val_t ret = mjs_mk_array(mjs); + while(storage_dir_read(dir, &file_info, name, sizeof(name))) { + furi_string_left(file_path, path_size); + path_append(file_path, name); + furi_check( + storage_common_timestamp(storage, furi_string_get_cstr(file_path), ×tamp) == + FSE_OK); + mjs_val_t obj = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, obj) { + JS_FIELD("path", mjs_mk_string(mjs, name, ~0, true)); + JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info))); + JS_FIELD("size", mjs_mk_number(mjs, file_info.size)); + JS_FIELD("timestamp", mjs_mk_number(mjs, timestamp)); + } + mjs_array_push(mjs, ret, obj); } - File* file = storage_file_alloc(storage->api); - if(!storage_file_open(file, path, FSAM_WRITE, FSOM_OPEN_APPEND)) { - ret_int_err(mjs, storage_file_get_error_desc(file)); - } else { - size_t write = storage_file_write(file, data, data_len); - mjs_return(mjs, mjs_mk_boolean(mjs, write == data_len)); - } - storage_file_free(file); + storage_file_free(dir); + furi_string_free(file_path); + mjs_return(mjs, ret); } -static void js_storage_exists(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - +static void js_storage_directory_exists(struct mjs* mjs) { const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_dir_exists(storage, path))); +} - mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage->api, path))); +static void js_storage_make_directory(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage, path))); +} + +// ---=== common ops ===--- + +static void js_storage_file_or_dir_exists(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_common_exists(storage, path))); +} + +static void js_storage_stat(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + FileInfo file_info; + uint32_t timestamp; + if((storage_common_stat(storage, path, &file_info) | + storage_common_timestamp(storage, path, ×tamp)) != FSE_OK) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + mjs_val_t ret = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, ret) { + JS_FIELD("path", mjs_mk_string(mjs, path, ~0, 1)); + JS_FIELD("isDirectory", mjs_mk_boolean(mjs, file_info_is_dir(&file_info))); + JS_FIELD("size", mjs_mk_number(mjs, file_info.size)); + JS_FIELD("accessTime", mjs_mk_number(mjs, timestamp)); + } + mjs_return(mjs, ret); } static void js_storage_remove(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage, path))); +} - mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove(storage->api, path))); +static void js_storage_rmrf(struct mjs* mjs) { + const char* path; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&path)); + Storage* storage = JS_GET_CONTEXT(mjs); + mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_remove_recursive(storage, path))); +} + +static void js_storage_rename(struct mjs* mjs) { + const char *old, *new; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&old), JS_ARG_STR(&new)); + Storage* storage = JS_GET_CONTEXT(mjs); + FS_Error status = storage_common_rename(storage, old, new); + mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK)); } static void js_storage_copy(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; + const char *source, *dest; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&source), JS_ARG_STR(&dest)); + Storage* storage = JS_GET_CONTEXT(mjs); + FS_Error status = storage_common_copy(storage, source, dest); + mjs_return(mjs, mjs_mk_boolean(mjs, status == FSE_OK || status == FSE_EXIST)); +} - const char* old_path; - if(!get_path_arg(mjs, &old_path, 0)) return; - - const char* new_path; - if(!get_path_arg(mjs, &new_path, 1)) return; - - FS_Error error = storage_common_copy(storage->api, old_path, new_path); - if(error == FSE_OK) { +static void js_storage_fs_info(struct mjs* mjs) { + const char* fs; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&fs)); + Storage* storage = JS_GET_CONTEXT(mjs); + uint64_t total_space, free_space; + if(storage_common_fs_info(storage, fs, &total_space, &free_space) != FSE_OK) { mjs_return(mjs, MJS_UNDEFINED); - } else { - ret_int_err(mjs, storage_error_get_desc(error)); - } -} - -static void js_storage_move(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; - - const char* old_path; - if(!get_path_arg(mjs, &old_path, 0)) return; - - const char* new_path; - if(!get_path_arg(mjs, &new_path, 1)) return; - - FS_Error error = storage_common_rename(storage->api, old_path, new_path); - if(error == FSE_OK) { - mjs_return(mjs, MJS_UNDEFINED); - } else { - ret_int_err(mjs, storage_error_get_desc(error)); - } -} - -static void js_storage_mkdir(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; - - mjs_return(mjs, mjs_mk_boolean(mjs, storage_simply_mkdir(storage->api, path))); -} - -static void js_storage_virtual_init(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - - const char* path; - if(!get_path_arg(mjs, &path, 0)) return; - - if(storage->virtual) { - ret_int_err(mjs, "Virtual already setup"); return; } - - storage->virtual = storage_file_alloc(storage->api); - if(!storage_file_open(storage->virtual, path, FSAM_READ | FSAM_WRITE, FSOM_OPEN_EXISTING)) { - storage_file_free(storage->virtual); - storage->virtual = NULL; - ret_int_err(mjs, "Open file failed"); - 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)); } - - bool success = storage_virtual_init(storage->api, storage->virtual) == FSE_OK; - if(!success) { - if(storage_virtual_quit(storage->api) == FSE_OK) { - success = storage_virtual_init(storage->api, storage->virtual) == FSE_OK; - } - } - if(!success) { - storage_file_free(storage->virtual); - storage->virtual = NULL; - ret_int_err(mjs, "Virtual init failed"); - return; - } - - mjs_return(mjs, MJS_UNDEFINED); + mjs_return(mjs, ret); } -static void js_storage_virtual_mount(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; - - if(storage_virtual_mount(storage->api) != FSE_OK) { - ret_int_err(mjs, "Virtual mount failed"); - return; - } - - mjs_return(mjs, MJS_UNDEFINED); +static void js_storage_next_available_filename(struct mjs* mjs) { + const char *dir_path, *file_name, *file_ext; + int32_t max_len; + JS_FETCH_ARGS_OR_RETURN( + mjs, + JS_EXACTLY, + JS_ARG_STR(&dir_path), + JS_ARG_STR(&file_name), + JS_ARG_STR(&file_ext), + JS_ARG_INT32(&max_len)); + Storage* storage = JS_GET_CONTEXT(mjs); + FuriString* next_name = furi_string_alloc(); + storage_get_next_filename(storage, dir_path, file_name, file_ext, next_name, max_len); + mjs_return(mjs, mjs_mk_string(mjs, furi_string_get_cstr(next_name), ~0, true)); + furi_string_free(next_name); } -static void js_storage_virtual_quit(struct mjs* mjs) { - JsStorageInst* storage = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; +// ---=== path ops ===--- - if(storage_virtual_quit(storage->api) != FSE_OK) { - ret_int_err(mjs, "Virtual quit failed"); - return; - } - - if(storage->virtual) { - storage_file_free(storage->virtual); - storage->virtual = NULL; - } - - mjs_return(mjs, MJS_UNDEFINED); +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_create(struct mjs* mjs, mjs_val_t* object) { - JsStorageInst* storage = malloc(sizeof(JsStorageInst)); - mjs_val_t storage_obj = mjs_mk_object(mjs); - mjs_set(mjs, storage_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, storage)); - mjs_set(mjs, storage_obj, "read", ~0, MJS_MK_FN(js_storage_read)); - mjs_set(mjs, storage_obj, "write", ~0, MJS_MK_FN(js_storage_write)); - mjs_set(mjs, storage_obj, "append", ~0, MJS_MK_FN(js_storage_append)); - mjs_set(mjs, storage_obj, "exists", ~0, MJS_MK_FN(js_storage_exists)); - mjs_set(mjs, storage_obj, "remove", ~0, MJS_MK_FN(js_storage_remove)); - mjs_set(mjs, storage_obj, "copy", ~0, MJS_MK_FN(js_storage_copy)); - mjs_set(mjs, storage_obj, "move", ~0, MJS_MK_FN(js_storage_move)); - mjs_set(mjs, storage_obj, "mkdir", ~0, MJS_MK_FN(js_storage_mkdir)); - mjs_set(mjs, storage_obj, "virtualInit", ~0, MJS_MK_FN(js_storage_virtual_init)); - mjs_set(mjs, storage_obj, "virtualMount", ~0, MJS_MK_FN(js_storage_virtual_mount)); - mjs_set(mjs, storage_obj, "virtualQuit", ~0, MJS_MK_FN(js_storage_virtual_quit)); - storage->api = furi_record_open(RECORD_STORAGE); - *object = storage_obj; - return storage; +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))); } -static void js_storage_destroy(void* inst) { - JsStorageInst* storage = inst; - if(storage->virtual) { - storage_virtual_quit(storage->api); - storage_file_free(storage->virtual); +// ---=== module ctor & dtor ===--- + +static void* js_storage_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + Storage* storage = furi_record_open(RECORD_STORAGE); + UNUSED(storage); + *object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, *object) { + JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, storage)); + + // top-level file ops + JS_FIELD("openFile", MJS_MK_FN(js_storage_open_file)); + JS_FIELD("fileExists", MJS_MK_FN(js_storage_file_exists)); + + // dir ops + JS_FIELD("readDirectory", MJS_MK_FN(js_storage_read_directory)); + JS_FIELD("directoryExists", MJS_MK_FN(js_storage_directory_exists)); + JS_FIELD("makeDirectory", MJS_MK_FN(js_storage_make_directory)); + + // common ops + JS_FIELD("fileOrDirExists", MJS_MK_FN(js_storage_file_or_dir_exists)); + JS_FIELD("stat", MJS_MK_FN(js_storage_stat)); + JS_FIELD("remove", MJS_MK_FN(js_storage_remove)); + JS_FIELD("rmrf", MJS_MK_FN(js_storage_rmrf)); + JS_FIELD("rename", MJS_MK_FN(js_storage_rename)); + JS_FIELD("copy", MJS_MK_FN(js_storage_copy)); + JS_FIELD("fsInfo", MJS_MK_FN(js_storage_fs_info)); + JS_FIELD("nextAvailableFilename", MJS_MK_FN(js_storage_next_available_filename)); + + // path ops + JS_FIELD("arePathsEqual", MJS_MK_FN(js_storage_are_paths_equal)); + JS_FIELD("isSubpathOf", MJS_MK_FN(js_storage_is_subpath_of)); } + return NULL; +} + +static void js_storage_destroy(void* data) { + UNUSED(data); furi_record_close(RECORD_STORAGE); - free(storage); } +// ---=== boilerplate ===--- + static const JsModuleDescriptor js_storage_desc = { "storage", js_storage_create, js_storage_destroy, + NULL, }; static const FlipperAppPluginDescriptor plugin_descriptor = { diff --git a/applications/system/js_app/modules/js_submenu.c b/applications/system/js_app/modules/js_submenu.c deleted file mode 100644 index 5ab9bef77..000000000 --- a/applications/system/js_app/modules/js_submenu.c +++ /dev/null @@ -1,147 +0,0 @@ -#include -#include -#include -#include -#include "../js_modules.h" - -typedef struct { - Submenu* submenu; - ViewHolder* view_holder; - FuriApiLock lock; - uint32_t result; - bool accepted; -} JsSubmenuInst; - -static JsSubmenuInst* get_this_ctx(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSubmenuInst* submenu = mjs_get_ptr(mjs, obj_inst); - furi_assert(submenu); - return submenu; -} - -static void ret_bad_args(struct mjs* mjs, const char* error) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); - mjs_return(mjs, MJS_UNDEFINED); -} - -static bool check_arg_count(struct mjs* mjs, size_t count) { - size_t num_args = mjs_nargs(mjs); - if(num_args != count) { - ret_bad_args(mjs, "Wrong argument count"); - return false; - } - return true; -} - -static void submenu_callback(void* context, uint32_t id) { - JsSubmenuInst* submenu = context; - submenu->result = id; - submenu->accepted = true; - api_lock_unlock(submenu->lock); -} - -static void submenu_exit(void* context) { - JsSubmenuInst* submenu = context; - submenu->result = 0; - submenu->accepted = false; - api_lock_unlock(submenu->lock); -} - -static void js_submenu_add_item(struct mjs* mjs) { - JsSubmenuInst* submenu = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; - - mjs_val_t label_arg = mjs_arg(mjs, 0); - const char* label = mjs_get_string(mjs, &label_arg, NULL); - if(!label) { - ret_bad_args(mjs, "Label must be a string"); - return; - } - - mjs_val_t id_arg = mjs_arg(mjs, 1); - if(!mjs_is_number(id_arg)) { - ret_bad_args(mjs, "Id must be a number"); - return; - } - int32_t id = mjs_get_int32(mjs, id_arg); - - submenu_add_item(submenu->submenu, label, id, submenu_callback, submenu); - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_submenu_set_header(struct mjs* mjs) { - JsSubmenuInst* submenu = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - - mjs_val_t header_arg = mjs_arg(mjs, 0); - const char* header = mjs_get_string(mjs, &header_arg, NULL); - if(!header) { - ret_bad_args(mjs, "Header must be a string"); - return; - } - - submenu_set_header(submenu->submenu, header); - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_submenu_show(struct mjs* mjs) { - JsSubmenuInst* submenu = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; - - submenu->lock = api_lock_alloc_locked(); - Gui* gui = furi_record_open(RECORD_GUI); - submenu->view_holder = view_holder_alloc(); - view_holder_attach_to_gui(submenu->view_holder, gui); - view_holder_set_back_callback(submenu->view_holder, submenu_exit, submenu); - - view_holder_set_view(submenu->view_holder, submenu_get_view(submenu->submenu)); - api_lock_wait_unlock(submenu->lock); - - view_holder_set_view(submenu->view_holder, NULL); - view_holder_free(submenu->view_holder); - furi_record_close(RECORD_GUI); - api_lock_free(submenu->lock); - - submenu_reset(submenu->submenu); - if(submenu->accepted) { - mjs_return(mjs, mjs_mk_number(mjs, submenu->result)); - } else { - mjs_return(mjs, MJS_UNDEFINED); - } -} - -static void* js_submenu_create(struct mjs* mjs, mjs_val_t* object) { - JsSubmenuInst* submenu = malloc(sizeof(JsSubmenuInst)); - mjs_val_t submenu_obj = mjs_mk_object(mjs); - mjs_set(mjs, submenu_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, submenu)); - mjs_set(mjs, submenu_obj, "addItem", ~0, MJS_MK_FN(js_submenu_add_item)); - mjs_set(mjs, submenu_obj, "setHeader", ~0, MJS_MK_FN(js_submenu_set_header)); - mjs_set(mjs, submenu_obj, "show", ~0, MJS_MK_FN(js_submenu_show)); - submenu->submenu = submenu_alloc(); - *object = submenu_obj; - return submenu; -} - -static void js_submenu_destroy(void* inst) { - JsSubmenuInst* submenu = inst; - submenu_free(submenu->submenu); - free(submenu); -} - -static const JsModuleDescriptor js_submenu_desc = { - "submenu", - js_submenu_create, - js_submenu_destroy, -}; - -static const FlipperAppPluginDescriptor submenu_plugin_descriptor = { - .appid = PLUGIN_APP_ID, - .ep_api_version = PLUGIN_API_VERSION, - .entry_point = &js_submenu_desc, -}; - -const FlipperAppPluginDescriptor* js_submenu_ep(void) { - return &submenu_plugin_descriptor; -} diff --git a/applications/system/js_app/modules/js_tests.c b/applications/system/js_app/modules/js_tests.c new file mode 100644 index 000000000..f27564000 --- /dev/null +++ b/applications/system/js_app/modules/js_tests.c @@ -0,0 +1,104 @@ +#include "../js_modules.h" // IWYU pragma: keep +#include +#include +#include + +#define TAG "JsTests" + +static void js_tests_fail(struct mjs* mjs) { + furi_check(mjs_nargs(mjs) == 1); + mjs_val_t message_arg = mjs_arg(mjs, 0); + const char* message = mjs_get_string(mjs, &message_arg, NULL); + furi_check(message); + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "%s", message); + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_tests_assert_eq(struct mjs* mjs) { + furi_check(mjs_nargs(mjs) == 2); + + mjs_val_t expected_arg = mjs_arg(mjs, 0); + mjs_val_t result_arg = mjs_arg(mjs, 1); + + if(mjs_is_number(expected_arg) && mjs_is_number(result_arg)) { + int32_t expected = mjs_get_int32(mjs, expected_arg); + int32_t result = mjs_get_int32(mjs, result_arg); + if(expected == result) { + FURI_LOG_T(TAG, "eq passed (exp=%ld res=%ld)", expected, result); + } else { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "expected %d, found %d", expected, result); + } + } else if(mjs_is_string(expected_arg) && mjs_is_string(result_arg)) { + const char* expected = mjs_get_string(mjs, &expected_arg, NULL); + const char* result = mjs_get_string(mjs, &result_arg, NULL); + if(strcmp(expected, result) == 0) { + FURI_LOG_T(TAG, "eq passed (exp=\"%s\" res=\"%s\")", expected, result); + } else { + mjs_prepend_errorf( + mjs, MJS_INTERNAL_ERROR, "expected \"%s\", found \"%s\"", expected, result); + } + } else if(mjs_is_boolean(expected_arg) && mjs_is_boolean(result_arg)) { + bool expected = mjs_get_bool(mjs, expected_arg); + bool result = mjs_get_bool(mjs, result_arg); + if(expected == result) { + FURI_LOG_T( + TAG, + "eq passed (exp=%s res=%s)", + expected ? "true" : "false", + result ? "true" : "false"); + } else { + mjs_prepend_errorf( + mjs, + MJS_INTERNAL_ERROR, + "expected %s, found %s", + expected ? "true" : "false", + result ? "true" : "false"); + } + } else { + JS_ERROR_AND_RETURN( + mjs, + MJS_INTERNAL_ERROR, + "type mismatch (expected %s, result %s)", + mjs_typeof(expected_arg), + mjs_typeof(result_arg)); + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_tests_assert_float_close(struct mjs* mjs) { + furi_check(mjs_nargs(mjs) == 3); + + mjs_val_t expected_arg = mjs_arg(mjs, 0); + mjs_val_t result_arg = mjs_arg(mjs, 1); + mjs_val_t epsilon_arg = mjs_arg(mjs, 2); + furi_check(mjs_is_number(expected_arg)); + furi_check(mjs_is_number(result_arg)); + furi_check(mjs_is_number(epsilon_arg)); + double expected = mjs_get_double(mjs, expected_arg); + double result = mjs_get_double(mjs, result_arg); + double epsilon = mjs_get_double(mjs, epsilon_arg); + + if(ABS(expected - result) > epsilon) { + mjs_prepend_errorf( + mjs, + MJS_INTERNAL_ERROR, + "expected %f found %f (tolerance=%f)", + expected, + result, + epsilon); + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + mjs_val_t tests_obj = mjs_mk_object(mjs); + mjs_set(mjs, tests_obj, "fail", ~0, MJS_MK_FN(js_tests_fail)); + mjs_set(mjs, tests_obj, "assert_eq", ~0, MJS_MK_FN(js_tests_assert_eq)); + mjs_set(mjs, tests_obj, "assert_float_close", ~0, MJS_MK_FN(js_tests_assert_float_close)); + *object = tests_obj; + + return (void*)1; +} diff --git a/applications/system/js_app/modules/js_tests.h b/applications/system/js_app/modules/js_tests.h new file mode 100644 index 000000000..49f752c2b --- /dev/null +++ b/applications/system/js_app/modules/js_tests.h @@ -0,0 +1,5 @@ +#pragma once +#include "../js_thread_i.h" +#include "../js_modules.h" + +void* js_tests_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules); diff --git a/applications/system/js_app/modules/js_textbox.c b/applications/system/js_app/modules/js_textbox.c deleted file mode 100644 index b90dbc153..000000000 --- a/applications/system/js_app/modules/js_textbox.c +++ /dev/null @@ -1,219 +0,0 @@ -#include -#include -#include "../js_modules.h" - -typedef struct { - TextBox* text_box; - ViewHolder* view_holder; - FuriString* text; - bool is_shown; -} JsTextboxInst; - -static JsTextboxInst* get_this_ctx(struct mjs* mjs) { - mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsTextboxInst* textbox = mjs_get_ptr(mjs, obj_inst); - furi_assert(textbox); - return textbox; -} - -static void ret_bad_args(struct mjs* mjs, const char* error) { - mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); - mjs_return(mjs, MJS_UNDEFINED); -} - -static bool check_arg_count(struct mjs* mjs, size_t count) { - size_t num_args = mjs_nargs(mjs); - if(num_args != count) { - ret_bad_args(mjs, "Wrong argument count"); - return false; - } - return true; -} - -static void js_textbox_set_config(struct mjs* mjs) { - JsTextboxInst* textbox = get_this_ctx(mjs); - if(!check_arg_count(mjs, 2)) return; - - TextBoxFocus set_focus = TextBoxFocusStart; - mjs_val_t focus_arg = mjs_arg(mjs, 0); - const char* focus = mjs_get_string(mjs, &focus_arg, NULL); - if(!focus) { - ret_bad_args(mjs, "Focus must be a string"); - return; - } else { - if(!strncmp(focus, "start", strlen("start"))) { - set_focus = TextBoxFocusStart; - } else if(!strncmp(focus, "end", strlen("end"))) { - set_focus = TextBoxFocusEnd; - } else { - ret_bad_args(mjs, "Bad focus value"); - return; - } - } - - TextBoxFont set_font = TextBoxFontText; - mjs_val_t font_arg = mjs_arg(mjs, 1); - const char* font = mjs_get_string(mjs, &font_arg, NULL); - if(!font) { - ret_bad_args(mjs, "Font must be a string"); - return; - } else { - if(!strncmp(font, "text", strlen("text"))) { - set_font = TextBoxFontText; - } else if(!strncmp(font, "hex", strlen("hex"))) { - set_font = TextBoxFontHex; - } else { - ret_bad_args(mjs, "Bad font value"); - return; - } - } - - text_box_set_focus(textbox->text_box, set_focus); - text_box_set_font(textbox->text_box, set_font); - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_textbox_add_text(struct mjs* mjs) { - JsTextboxInst* textbox = get_this_ctx(mjs); - if(!check_arg_count(mjs, 1)) return; - - mjs_val_t text_arg = mjs_arg(mjs, 0); - size_t text_len = 0; - const char* text = mjs_get_string(mjs, &text_arg, &text_len); - if(!text) { - ret_bad_args(mjs, "Text must be a string"); - return; - } - - // Avoid condition race between GUI and JS thread - text_box_set_text(textbox->text_box, ""); - - size_t new_len = furi_string_size(textbox->text) + text_len; - if(new_len >= 4096) { - furi_string_right(textbox->text, new_len / 2); - } - - furi_string_cat(textbox->text, text); - - text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text)); - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_textbox_clear_text(struct mjs* mjs) { - JsTextboxInst* textbox = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; - - // Avoid condition race between GUI and JS thread - text_box_set_text(textbox->text_box, ""); - - furi_string_reset(textbox->text); - - text_box_set_text(textbox->text_box, furi_string_get_cstr(textbox->text)); - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_textbox_is_open(struct mjs* mjs) { - JsTextboxInst* textbox = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; - - mjs_return(mjs, mjs_mk_boolean(mjs, textbox->is_shown)); -} - -static void textbox_callback(void* context, uint32_t arg) { - UNUSED(arg); - JsTextboxInst* textbox = context; - view_holder_set_view(textbox->view_holder, NULL); - textbox->is_shown = false; -} - -static void textbox_exit(void* context) { - JsTextboxInst* textbox = context; - // Using timer to schedule view_holder stop, will not work under high CPU load - furi_timer_pending_callback(textbox_callback, textbox, 0); -} - -static void js_textbox_show(struct mjs* mjs) { - JsTextboxInst* textbox = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; - - if(textbox->is_shown) { - mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "Textbox is already shown"); - mjs_return(mjs, MJS_UNDEFINED); - return; - } - - view_holder_set_view(textbox->view_holder, text_box_get_view(textbox->text_box)); - textbox->is_shown = true; - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void js_textbox_close(struct mjs* mjs) { - JsTextboxInst* textbox = get_this_ctx(mjs); - if(!check_arg_count(mjs, 0)) return; - - view_holder_set_view(textbox->view_holder, NULL); - textbox->is_shown = false; - - mjs_return(mjs, MJS_UNDEFINED); -} - -static void* js_textbox_create(struct mjs* mjs, mjs_val_t* object) { - JsTextboxInst* textbox = malloc(sizeof(JsTextboxInst)); - - mjs_val_t textbox_obj = mjs_mk_object(mjs); - mjs_set(mjs, textbox_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, textbox)); - mjs_set(mjs, textbox_obj, "setConfig", ~0, MJS_MK_FN(js_textbox_set_config)); - mjs_set(mjs, textbox_obj, "addText", ~0, MJS_MK_FN(js_textbox_add_text)); - mjs_set(mjs, textbox_obj, "clearText", ~0, MJS_MK_FN(js_textbox_clear_text)); - mjs_set(mjs, textbox_obj, "isOpen", ~0, MJS_MK_FN(js_textbox_is_open)); - mjs_set(mjs, textbox_obj, "show", ~0, MJS_MK_FN(js_textbox_show)); - mjs_set(mjs, textbox_obj, "close", ~0, MJS_MK_FN(js_textbox_close)); - - textbox->text = furi_string_alloc(); - textbox->text_box = text_box_alloc(); - - Gui* gui = furi_record_open(RECORD_GUI); - textbox->view_holder = view_holder_alloc(); - view_holder_attach_to_gui(textbox->view_holder, gui); - view_holder_set_back_callback(textbox->view_holder, textbox_exit, textbox); - - *object = textbox_obj; - return textbox; -} - -static void js_textbox_destroy(void* inst) { - JsTextboxInst* textbox = inst; - - view_holder_set_view(textbox->view_holder, NULL); - view_holder_free(textbox->view_holder); - textbox->view_holder = NULL; - - furi_record_close(RECORD_GUI); - - text_box_reset(textbox->text_box); - furi_string_reset(textbox->text); - - text_box_free(textbox->text_box); - furi_string_free(textbox->text); - free(textbox); -} - -static const JsModuleDescriptor js_textbox_desc = { - "textbox", - js_textbox_create, - js_textbox_destroy, -}; - -static const FlipperAppPluginDescriptor textbox_plugin_descriptor = { - .appid = PLUGIN_APP_ID, - .ep_api_version = PLUGIN_API_VERSION, - .entry_point = &js_textbox_desc, -}; - -const FlipperAppPluginDescriptor* js_textbox_ep(void) { - return &textbox_plugin_descriptor; -} diff --git a/applications/system/js_app/plugin_api/app_api_table_i.h b/applications/system/js_app/plugin_api/app_api_table_i.h index b48221343..b2debbde8 100644 --- a/applications/system/js_app/plugin_api/app_api_table_i.h +++ b/applications/system/js_app/plugin_api/app_api_table_i.h @@ -7,4 +7,5 @@ static constexpr auto app_api_table = sort(create_array_t( API_METHOD(js_delay_with_flags, bool, (struct mjs*, uint32_t)), API_METHOD(js_flags_set, void, (struct mjs*, uint32_t)), - API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)))); + API_METHOD(js_flags_wait, uint32_t, (struct mjs*, uint32_t, uint32_t)), + API_METHOD(js_module_get, void*, (JsModules*, const char*)))); diff --git a/applications/system/js_app/plugin_api/js_plugin_api.h b/applications/system/js_app/plugin_api/js_plugin_api.h index a817d34a9..421b68576 100644 --- a/applications/system/js_app/plugin_api/js_plugin_api.h +++ b/applications/system/js_app/plugin_api/js_plugin_api.h @@ -7,12 +7,16 @@ extern "C" { #endif +typedef void JsModules; + bool js_delay_with_flags(struct mjs* mjs, uint32_t time); void js_flags_set(struct mjs* mjs, uint32_t flags); uint32_t js_flags_wait(struct mjs* mjs, uint32_t flags, uint32_t timeout); +void* js_module_get(JsModules* modules, const char* name); + #ifdef __cplusplus } #endif diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/types/badusb/index.d.ts new file mode 100644 index 000000000..210790967 --- /dev/null +++ b/applications/system/js_app/types/badusb/index.d.ts @@ -0,0 +1,81 @@ +/** + * @brief Special key codes that this module recognizes + */ +export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI"; + +export type MainKey = + "DOWN" | "LEFT" | "RIGHT" | "UP" | + + "ENTER" | "PAUSE" | "CAPSLOCK" | "DELETE" | "BACKSPACE" | "END" | "ESC" | + "HOME" | "INSERT" | "NUMLOCK" | "PAGEUP" | "PAGEDOWN" | "PRINTSCREEN" | + "SCROLLLOCK" | "SPACE" | "TAB" | "MENU" | + + "F1" | "F2" | "F3" | "F4" | "F5" | "F6" | "F7" | "F8" | "F9" | "F10" | + "F11" | "F12" | "F13" | "F14" | "F15" | "F16" | "F17" | "F18" | "F19" | + "F20" | "F21" | "F22" | "F23" | "F24" | + + "\n" | " " | "!" | "\"" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | + "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | ">" | "=" | "?" | "@" | "[" | + "]" | "\\" | "^" | "_" | "`" | "{" | "}" | "|" | "~" | + + "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | + + "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | + "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | + "Y" | "Z" | + + "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | + "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | + "y" | "z"; + +export type KeyCode = MainKey | ModifierKey | number; + +/** + * @brief Initializes the module + * @param settings USB device settings. Omit to select default parameters + */ +export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string }): void; + +/** + * @brief Tells whether the virtual USB HID device has successfully connected + */ +export declare function isConnected(): boolean; + +/** + * @brief Presses one or multiple keys at once, then releases them + * @param keys The arguments represent a set of keys to. Out of that set, only + * one of the keys may represent a "main key" (see `MainKey`), with + * the rest being modifier keys (see `ModifierKey`). + */ +export declare function press(...keys: KeyCode[]): void; + +/** + * @brief Presses one or multiple keys at once without releasing them + * @param keys The arguments represent a set of keys to. Out of that set, only + * one of the keys may represent a "main key" (see `MainKey`), with + * the rest being modifier keys (see `ModifierKey`). + */ +export declare function hold(...keys: KeyCode[]): void; + +/** + * @brief Releases one or multiple keys at once + * @param keys The arguments represent a set of keys to. Out of that set, only + * one of the keys may represent a "main key" (see `MainKey`), with + * the rest being modifier keys (see `ModifierKey`). + */ +export declare function release(...keys: KeyCode[]): void; + +/** + * @brief Prints a string by repeatedly pressing and releasing keys + * @param string The string to print + * @param delay How many milliseconds to wait between key presses + */ +export declare function print(string: string, delay?: number): void; + +/** + * @brief Prints a string by repeatedly pressing and releasing keys. Presses + * "Enter" after printing the string + * @param string The string to print + * @param delay How many milliseconds to wait between key presses + */ +export declare function println(): void; diff --git a/applications/system/js_app/types/event_loop/index.d.ts b/applications/system/js_app/types/event_loop/index.d.ts new file mode 100644 index 000000000..49237782c --- /dev/null +++ b/applications/system/js_app/types/event_loop/index.d.ts @@ -0,0 +1,70 @@ +type Lit = undefined | null | {}; + +/** + * Subscription control interface + */ +export interface Subscription { + /** + * Cancels the subscription, preventing any future events managed by the + * subscription from firing + */ + cancel(): void; +} + +/** + * Opaque event source identifier + */ +export type Contract = symbol; + +/** + * A callback can be assigned to an event loop to listen to an event. It may + * return an array with values that will be passed to it as arguments the next + * time that it is called. The first argument is always the subscription + * manager, and the second argument is always the item that trigged the event. + * The type of the item is defined by the event source. + */ +export type Callback = (subscription: Subscription, item: Item, ...args: Args) => Args | undefined | void; + +/** + * Subscribes a callback to an event + * @param contract Event identifier + * @param callback Function to call when the event is triggered + * @param args Initial arguments passed to the callback + */ +export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; +/** + * Runs the event loop until it is stopped (potentially never) + */ +export function run(): void | never; +/** + * Stops the event loop + */ +export function stop(): void; + +/** + * Creates a timer event that can be subscribed to just like any other event + * @param mode Either `"oneshot"` or `"periodic"` + * @param interval Timer interval in milliseconds + */ +export function timer(mode: "oneshot" | "periodic", interval: number): Contract; + +/** + * Message queue + */ +export interface Queue { + /** + * Message event + */ + input: Contract; + /** + * Sends a message to the queue + * @param message message to send + */ + send(message: T): void; +} + +/** + * Creates a message queue + * @param length maximum queue capacity + */ +export function queue(length: number): Queue; diff --git a/applications/system/js_app/types/flipper/index.d.ts b/applications/system/js_app/types/flipper/index.d.ts new file mode 100644 index 000000000..b1b1d474b --- /dev/null +++ b/applications/system/js_app/types/flipper/index.d.ts @@ -0,0 +1,14 @@ +/** + * @brief Returns the device model + */ +export declare function getModel(): string; + +/** + * @brief Returns the name of the virtual dolphin + */ +export declare function getName(): string; + +/** + * @brief Returns the battery charge percentage + */ +export declare function getBatteryCharge(): number; diff --git a/applications/system/js_app/types/global.d.ts b/applications/system/js_app/types/global.d.ts new file mode 100644 index 000000000..ab1660cf6 --- /dev/null +++ b/applications/system/js_app/types/global.d.ts @@ -0,0 +1,178 @@ +/** + * @brief Pauses JavaScript execution for a while + * @param ms How many milliseconds to pause the execution for + */ +declare function delay(ms: number): void; + +/** + * @brief Prints to the GUI console view + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the console view + */ +declare function print(...args: any[]): void; + +/** + * @brief Converts a number to a string + * @param value The number to convert to a string + * @param base Integer base (`2`...`16`), default: 16 + */ +declare function toString(value: number, base?: number): string; + +/** + * @brief Reads a JS value from a file + * + * Reads a file at the specified path, interprets it as a JS value and returns + * said value. + * + * @param path The path to the file + */ +declare function load(path: string): any; + +/** + * @brief mJS Foreign Pointer type + * + * JavaScript code cannot do anything with values of `RawPointer` type except + * acquire them from native code and pass them right back to other parts of + * native code. These values cannot be turned into something meaningful, nor can + * be they modified. + */ +declare type RawPointer = symbol & { "__tag__": "raw_ptr" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * @brief Holds raw bytes + */ +declare class ArrayBuffer { + /** + * @brief The pointer to the byte buffer + * @note Like other `RawPointer` values, this value is essentially useless + * to JS code. + */ + getPtr: RawPointer; + /** + * @brief The length of the buffer in bytes + */ + byteLength: number; + /** + * @brief Creates an `ArrayBuffer` that contains a sub-part of the buffer + * @param start The index of the byte in the source buffer to be used as the + * start for the new buffer + * @param end The index of the byte in the source buffer that follows the + * byte to be used as the last byte for the new buffer + */ + slice(start: number, end?: number): ArrayBuffer; +} + +declare function ArrayBuffer(): ArrayBuffer; + +declare type ElementType = "u8" | "i8" | "u16" | "i16" | "u32" | "i32"; + +declare class TypedArray { + /** + * @brief The length of the buffer in bytes + */ + byteLength: number; + /** + * @brief The length of the buffer in typed elements + */ + length: number; + /** + * @brief The underlying `ArrayBuffer` + */ + buffer: ArrayBuffer; +} + +declare class Uint8Array extends TypedArray<"u8"> { } +declare class Int8Array extends TypedArray<"i8"> { } +declare class Uint16Array extends TypedArray<"u16"> { } +declare class Int16Array extends TypedArray<"i16"> { } +declare class Uint32Array extends TypedArray<"u32"> { } +declare class Int32Array extends TypedArray<"i32"> { } + +declare function Uint8Array(data: ArrayBuffer | number | number[]): Uint8Array; +declare function Int8Array(data: ArrayBuffer | number | number[]): Int8Array; +declare function Uint16Array(data: ArrayBuffer | number | number[]): Uint16Array; +declare function Int16Array(data: ArrayBuffer | number | number[]): Int16Array; +declare function Uint32Array(data: ArrayBuffer | number | number[]): Uint32Array; +declare function Int32Array(data: ArrayBuffer | number | number[]): Int32Array; + +declare const console: { + /** + * @brief Prints to the UART logs at the `[I]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + log(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[D]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + debug(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[W]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + warn(...args: any[]): void; + /** + * @brief Prints to the UART logs at the `[E]` level + * @param args The arguments are converted to strings, concatenated without any + * spaces in between and printed to the logs + */ + error(...args: any[]): void; +}; + +declare class Array { + /** + * @brief Takes items out of the array + * + * Removes elements from the array and returns them in a new array + * + * @param start The index to start taking elements from + * @param deleteCount How many elements to take + * @returns The elements that were taken out of the original array as a new + * array + */ + splice(start: number, deleteCount: number): T[]; + /** + * @brief Adds a value to the end of the array + * @param value The value to add + * @returns New length of the array + */ + push(value: T): number; + /** + * @brief How many elements there are in the array + */ + length: number; +} + +declare class String { + /** + * @brief How many characters there are in the string + */ + length: number; + /** + * @brief Returns the character code at an index in the string + * @param index The index to consult + */ + charCodeAt(index: number): number; + /** + * See `charCodeAt` + */ + at(index: number): number; +} + +declare class Boolean { } + +declare class Function { } + +declare class Number { } + +declare class Object { } + +declare class RegExp { } + +declare interface IArguments { } + +declare type Partial = { [K in keyof O]?: O[K] }; diff --git a/applications/system/js_app/types/gpio/index.d.ts b/applications/system/js_app/types/gpio/index.d.ts new file mode 100644 index 000000000..18705f898 --- /dev/null +++ b/applications/system/js_app/types/gpio/index.d.ts @@ -0,0 +1,45 @@ +import type { Contract } from "../event_loop"; + +export interface Mode { + direction: "in" | "out"; + outMode?: "push_pull" | "open_drain"; + inMode?: "analog" | "plain_digital" | "interrupt" | "event"; + edge?: "rising" | "falling" | "both"; + pull?: "up" | "down"; +} + +export interface Pin { + /** + * Configures a pin. This may be done several times. + * @param mode Pin configuration object + */ + init(mode: Mode): void; + /** + * Sets the output value of a pin if it's been configured with + * `direction: "out"`. + * @param value Logic value to output + */ + write(value: boolean): void; + /** + * Gets the input value of a pin if it's been configured with + * `direction: "in"`, but not `inMode: "analog"`. + */ + read(): boolean; + /** + * Gets the input voltage of a pin in millivolts if it's been configured + * with `direction: "in"` and `inMode: "analog"` + */ + read_analog(): number; + /** + * Returns an `event_loop` event that can be used to listen to interrupts, + * as configured by `init` + */ + interrupt(): Contract; +} + +/** + * Returns an object that can be used to manage a GPIO pin. For the list of + * available pins, see https://docs.flipper.net/gpio-and-modules#miFsS + * @param pin Pin name (e.g. `"PC3"`) or number (e.g. `7`) + */ +export function get(pin: string | number): Pin; diff --git a/applications/system/js_app/types/gui/dialog.d.ts b/applications/system/js_app/types/gui/dialog.d.ts new file mode 100644 index 000000000..6d9c8d43b --- /dev/null +++ b/applications/system/js_app/types/gui/dialog.d.ts @@ -0,0 +1,16 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + text: string, + left: string, + center: string, + right: string, +} +declare class Dialog extends View { + input: Contract<"left" | "center" | "right">; +} +declare class DialogFactory extends ViewFactory { } +declare const factory: DialogFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/empty_screen.d.ts b/applications/system/js_app/types/gui/empty_screen.d.ts new file mode 100644 index 000000000..c71e93b32 --- /dev/null +++ b/applications/system/js_app/types/gui/empty_screen.d.ts @@ -0,0 +1,7 @@ +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class EmptyScreen extends View { } +declare class EmptyScreenFactory extends ViewFactory { } +declare const factory: EmptyScreenFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/index.d.ts b/applications/system/js_app/types/gui/index.d.ts new file mode 100644 index 000000000..3f95ab780 --- /dev/null +++ b/applications/system/js_app/types/gui/index.d.ts @@ -0,0 +1,41 @@ +import type { Contract } from "../event_loop"; + +type Properties = { [K: string]: any }; + +export declare class View { + set

(property: P, value: Props[P]): void; +} + +export declare class ViewFactory> { + make(): V; + makeWith(initial: Partial): V; +} + +declare class ViewDispatcher { + /** + * Event source for `sendCustom` events + */ + custom: Contract; + /** + * Event source for navigation events (back key presses) + */ + navigation: Contract; + /** + * Sends a number to the custom event handler + * @param event number to send + */ + sendCustom(event: number): void; + /** + * Switches to a view + * @param assoc View-ViewDispatcher association as returned by `add` + */ + switchTo(assoc: View): void; + /** + * Sends this ViewDispatcher to the front or back, above or below all other + * GUI viewports + * @param direction Either `"front"` or `"back"` + */ + sendTo(direction: "front" | "back"): void; +} + +export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/types/gui/loading.d.ts b/applications/system/js_app/types/gui/loading.d.ts new file mode 100644 index 000000000..73a963349 --- /dev/null +++ b/applications/system/js_app/types/gui/loading.d.ts @@ -0,0 +1,7 @@ +import type { View, ViewFactory } from "."; + +type Props = {}; +declare class Loading extends View { } +declare class LoadingFactory extends ViewFactory { } +declare const factory: LoadingFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/submenu.d.ts b/applications/system/js_app/types/gui/submenu.d.ts new file mode 100644 index 000000000..59d535864 --- /dev/null +++ b/applications/system/js_app/types/gui/submenu.d.ts @@ -0,0 +1,13 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + items: string[], +}; +declare class Submenu extends View { + chosen: Contract; +} +declare class SubmenuFactory extends ViewFactory { } +declare const factory: SubmenuFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/text_box.d.ts b/applications/system/js_app/types/gui/text_box.d.ts new file mode 100644 index 000000000..3dbbac571 --- /dev/null +++ b/applications/system/js_app/types/gui/text_box.d.ts @@ -0,0 +1,14 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + text: string, + font: "text" | "hex", + focus: "start" | "end", +} +declare class TextBox extends View { + chosen: Contract; +} +declare class TextBoxFactory extends ViewFactory { } +declare const factory: TextBoxFactory; +export = factory; diff --git a/applications/system/js_app/types/gui/text_input.d.ts b/applications/system/js_app/types/gui/text_input.d.ts new file mode 100644 index 000000000..96652b1d4 --- /dev/null +++ b/applications/system/js_app/types/gui/text_input.d.ts @@ -0,0 +1,14 @@ +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + minLength: number, + maxLength: number, +} +declare class TextInput extends View { + input: Contract; +} +declare class TextInputFactory extends ViewFactory { } +declare const factory: TextInputFactory; +export = factory; diff --git a/applications/system/js_app/types/math/index.d.ts b/applications/system/js_app/types/math/index.d.ts new file mode 100644 index 000000000..25abca4af --- /dev/null +++ b/applications/system/js_app/types/math/index.d.ts @@ -0,0 +1,24 @@ +export function abs(n: number): number; +export function acos(n: number): number; +export function acosh(n: number): number; +export function asin(n: number): number; +export function asinh(n: number): number; +export function atan(n: number): number; +export function atan2(a: number, b: number): number; +export function atanh(n: number): number; +export function cbrt(n: number): number; +export function ceil(n: number): number; +export function clz32(n: number): number; +export function cos(n: number): number; +export function exp(n: number): number; +export function floor(n: number): number; +export function max(n: number, m: number): number; +export function min(n: number, m: number): number; +export function pow(n: number, m: number): number; +export function random(): number; +export function sign(n: number): number; +export function sin(n: number): number; +export function sqrt(n: number): number; +export function trunc(n: number): number; +declare const PI: number; +declare const EPSILON: number; diff --git a/applications/system/js_app/types/notification/index.d.ts b/applications/system/js_app/types/notification/index.d.ts new file mode 100644 index 000000000..947daba21 --- /dev/null +++ b/applications/system/js_app/types/notification/index.d.ts @@ -0,0 +1,20 @@ +/** + * @brief Signals success to the user via the color LED, speaker and vibration + * motor + */ +export declare function success(): void; + +/** + * @brief Signals failure to the user via the color LED, speaker and vibration + * motor + */ +export declare function error(): void; + +export type Color = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta"; + +/** + * @brief Displays a basic color on the color LED + * @param color The color to display, see `Color` + * @param duration The duration, either `"short"` (10ms) or `"long"` (100ms) + */ +export declare function blink(color: Color, duration: "short" | "long"): void; diff --git a/applications/system/js_app/types/serial/index.d.ts b/applications/system/js_app/types/serial/index.d.ts new file mode 100644 index 000000000..1a7ed6397 --- /dev/null +++ b/applications/system/js_app/types/serial/index.d.ts @@ -0,0 +1,77 @@ +/** + * @brief Initializes the serial port + * @param port The port to initialize (`"lpuart"` or `"start"`) + * @param baudRate + */ +export declare function setup(port: "lpuart" | "usart", baudRate: number): void; + +/** + * @brief Writes data to the serial port + * @param value The data to write: + * - Strings will get sent as ASCII. + * - Numbers will get sent as a single byte. + * - Arrays of numbers will get sent as a sequence of bytes. + * - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence + * of bytes. + */ +export declare function write(value: string | number | number[] | ArrayBuffer | TypedArray): void; + +/** + * @brief Reads data from the serial port + * @param length The number of bytes to read + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. + * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes + * were read. + */ +export declare function read(length: number, timeout?: number): string | undefined; + +/** + * @brief Reads data from the serial port + * + * Data is read one character after another until either a `\r` or `\n` + * character is received, neither of which is included in the result. + * + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. The timeout only + * applies to characters, not entire strings. + * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes + * were read. + */ +export declare function readln(timeout?: number): string; + +/** + * @brief Reads data from the serial port + * @param length The number of bytes to read + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. + * @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were + * read. + */ +export declare function readBytes(length: number, timeout?: number): ArrayBuffer; + +/** + * @brief Reads data from the serial port, trying to match it to a pattern + * @param patterns A single pattern or an array of patterns: + * - If the argument is a single `string`, this function will + * match against the given string. + * - If the argument is an array of `number`s, this function + * will match against the given sequence of bytes, + * - If the argument is an array of `string`s, this function + * will match against any string out of the ones that were + * provided. + * - If the argument is an array of arrays of `number`s, this + * function will match against any sequence of bytes out of + * the ones that were provided. + * @param timeout The number of time, in milliseconds, after which this function + * will give up and return what it read up to that point. If + * unset, the function will wait forever. The timeout only + * applies to characters, not entire strings. + * @returns The index of the matched pattern if multiple were provided, or 0 if + * only one was provided and it matched, or `undefined` if none of the + * patterns matched. + */ +export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined; diff --git a/applications/system/js_app/types/storage/index.d.ts b/applications/system/js_app/types/storage/index.d.ts new file mode 100644 index 000000000..0dd29e121 --- /dev/null +++ b/applications/system/js_app/types/storage/index.d.ts @@ -0,0 +1,237 @@ +/** + * File readability mode: + * - `"r"`: read-only + * - `"w"`: write-only + * - `"rw"`: read-write + */ +export type AccessMode = "r" | "w" | "rw"; + +/** + * File creation mode: + * - `"open_existing"`: open file or fail if it doesn't exist + * - `"open_always"`: open file or create a new empty one if it doesn't exist + * - `"open_append"`: open file and set r/w pointer to EOF, or create a new one if it doesn't exist + * - `"create_new"`: create new file or fail if it exists + * - `"create_always"`: truncate and open file, or create a new empty one if it doesn't exist + */ +export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always"; + +/** Standard UNIX timestamp */ +export type Timestamp = number; + +/** File information structure */ +export declare class FileInfo { + /** + * Full path (e.g. "/ext/test", returned by `stat`) or file name + * (e.g. "test", returned by `readDirectory`) + */ + path: string; + /** + * Is the file a directory? + */ + isDirectory: boolean; + /** + * File size in bytes, or 0 in the case of directories + */ + size: number; + /** + * Time of last access as a UNIX timestamp + */ + accessTime: Timestamp; +} + +/** Filesystem information structure */ +export declare class FsInfo { + /** Total size of the filesystem, in bytes */ + totalSpace: number; + /** Free space in the filesystem, in bytes */ + freeSpace: number; +} + +// file operations + +/** File class */ +export declare class File { + /** + * Closes the file. After this method is called, all other operations + * related to this file become unavailable. + * @returns `true` on success, `false` on failure + */ + close(): boolean; + /** + * Is the file currently open? + */ + isOpen(): boolean; + /** + * Reads bytes from a file opened in read-only or read-write mode + * @param mode The data type to interpret the bytes as: a `string` decoded + * from ASCII data (`"ascii"`), or an `ArrayBuf` (`"binary"`) + * @param bytes How many bytes to read from the file + * @returns an `ArrayBuf` if the mode is `"binary"`, a `string` if the mode + * is `ascii`. The number of bytes that was actually read may be + * fewer than requested. + */ + read(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T; + /** + * Writes bytes to a file opened in write-only or read-write mode + * @param data The data to write: a string that will be ASCII-encoded, or an + * ArrayBuf + * @returns the amount of bytes that was actually written + */ + write(data: ArrayBuffer | string): number; + /** + * Moves the R/W pointer forward + * @param bytes How many bytes to move the pointer forward by + * @returns `true` on success, `false` on failure + */ + seekRelative(bytes: number): boolean; + /** + * Moves the R/W pointer to an absolute position inside the file + * @param bytes The position inside the file + * @returns `true` on success, `false` on failure + */ + seekAbsolute(bytes: number): boolean; + /** + * Gets the absolute position of the R/W pointer in bytes + */ + tell(): number; + /** + * Discards the data after the current position of the R/W pointer in a file + * opened in either write-only or read-write mode. + * @returns `true` on success, `false` on failure + */ + truncate(): boolean; + /** + * Reads the total size of the file in bytes + */ + size(): number; + /** + * Detects whether the R/W pointer has reached the end of the file + */ + eof(): boolean; + /** + * Copies bytes from the R/W pointer in the current file to the R/W pointer + * in another file + * @param dest The file to copy the bytes into + * @param bytes The number of bytes to copy + * @returns `true` on success, `false` on failure + */ + copyTo(dest: File, bytes: number): boolean; +} + +/** + * Opens a file + * @param path The path to the file + * @param accessMode `"r"`, `"w"` or `"rw"`; see `AccessMode` + * @param openMode `"open_existing"`, `"open_always"`, `"open_append"`, + * `"create_new"` or `"create_always"`; see `OpenMode` + * @returns a `File` on success, or `undefined` on failure + */ +export declare function openFile(path: string, accessMode: AccessMode, openMode: OpenMode): File | undefined; +/** + * Detects whether a file exists + * @param path The path to the file + * @returns `true` on success, `false` on failure + */ +export declare function fileExists(path: string): boolean; + +// directory operations + +/** + * Reads the list of files in a directory + * @param path The path to the directory + * @returns Array of `FileInfo` structures with directory entries, + * or `undefined` on failure + */ +export declare function readDirectory(path: string): FileInfo[] | undefined; +/** + * Detects whether a directory exists + * @param path The path to the directory + */ +export declare function directoryExists(path: string): boolean; +/** + * Creates an empty directory + * @param path The path to the new directory + * @returns `true` on success, `false` on failure + */ +export declare function makeDirectory(path: string): boolean; + +// common (file/dir) operations + +/** + * Detects whether a file or a directory exists + * @param path The path to the file or directory + */ +export declare function fileOrDirExists(path: string): boolean; +/** + * Acquires metadata about a file or directory + * @param path The path to the file or directory + * @returns A `FileInfo` structure or `undefined` on failure + */ +export declare function stat(path: string): FileInfo | undefined; +/** + * Removes a file or an empty directory + * @param path The path to the file or directory + * @returns `true` on success, `false` on failure + */ +export declare function remove(path: string): boolean; +/** + * Removes a file or recursively removes a possibly non-empty directory + * @param path The path to the file or directory + * @returns `true` on success, `false` on failure + */ +export declare function rmrf(path: string): boolean; +/** + * Renames or moves a file or directory + * @param oldPath The old path to the file or directory + * @param newPath The new path that the file or directory will become accessible + * under + * @returns `true` on success, `false` on failure + */ +export declare function rename(oldPath: string, newPath: string): boolean; +/** + * Copies a file or recursively copies a possibly non-empty directory + * @param oldPath The original path to the file or directory + * @param newPath The new path that the copy of the file or directory will be + * accessible under + */ +export declare function copy(oldPath: string, newPath: string): boolean; +/** + * Fetches generic information about a filesystem + * @param filesystem The path to the filesystem (e.g. `"/ext"` or `"/int"`) + */ +export declare function fsInfo(filesystem: string): FsInfo | undefined; +/** + * Chooses the next available filename with a numeric suffix in a directory + * + * ``` + * "/ext/example_dir/example_file123.txt" + * \______________/ \__________/\_/\__/ + * dirPath fileName | | + * | +---- fileExt + * +------- selected by this function + * ``` + * + * @param dirPath The directory to look in + * @param fileName The base of the filename (the part before the numeric suffix) + * @param fileExt The extension of the filename (the part after the numeric suffix) + * @param maxLen The maximum length of the filename with the numeric suffix + * @returns The base of the filename with the next available numeric suffix, + * without the extension or the base directory. + */ +export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string; + +// path operations that do not access the filesystem + +/** + * Determines whether the two paths are equivalent. Respects filesystem-defined + * path equivalence rules. + */ +export declare function arePathsEqual(path1: string, path2: string): boolean; +/** + * Determines whether a path is a subpath of another path. Respects + * filesystem-defined path equivalence rules. + * @param parentPath The parent path + * @param childPath The child path + */ +export declare function isSubpathOf(parentPath: string, childPath: string): boolean; diff --git a/applications/system/js_app/types/tests/index.d.ts b/applications/system/js_app/types/tests/index.d.ts new file mode 100644 index 000000000..8aaeec5e5 --- /dev/null +++ b/applications/system/js_app/types/tests/index.d.ts @@ -0,0 +1,8 @@ +/** + * Unit test module. Only available if the firmware has been configured with + * `FIRMWARE_APP_SET=unit_tests`. + */ + +export function fail(message: string): never; +export function assert_eq(expected: T, result: T): void | never; +export function assert_float_close(expected: number, result: number, epsilon: number): void | never; diff --git a/documentation/FuriHalBus.md b/documentation/FuriHalBus.md index f534e5bd1..5e7bb5f40 100644 --- a/documentation/FuriHalBus.md +++ b/documentation/FuriHalBus.md @@ -3,18 +3,15 @@ ## Basic info On system startup, most of the peripheral devices are under reset and not clocked by default. This is done to reduce power consumption and to guarantee that the device will always be in the same state before use. - Some crucial peripherals are enabled right away by the system, others must be explicitly enabled by the user code. **NOTE:** Here and afterwards, the word *"system"* refers to any code belonging to the operating system, hardware drivers or built-in apps. -To **ENABLE** a peripheral, call `furi_hal_bus_enable()`. At the time of the call, the peripheral in question **MUST** be disabled; -otherwise a crash will occur to indicate improper use. This means that any given peripheral cannot be enabled twice or more without disabling it first. +To **ENABLE** a peripheral, call `furi_hal_bus_enable()`. At the time of the call, the peripheral in question MUST be disabled, otherwise a crash will occur to indicate improper use. This means that any given peripheral cannot be enabled twice or more without disabling it first. -To **DISABLE** a peripheral, call `furi_hal_bus_disable()`. Likewise, the peripheral in question **MUST** be enabled, otherwise a crash will occur. +To **DISABLE** a peripheral, call `furi_hal_bus_disable()`. Likewise, the peripheral in question MUST be enabled, otherwise a crash will occur. -To **RESET** a peripheral, call `furi_hal_bus_reset()`. The peripheral in question MUST be enabled, otherwise a crash will occur. -This method is used whenever it is necessary to reset all the peripheral's registers to their initial states without disabling it. +To **RESET** a peripheral, call `furi_hal_bus_reset()`. The peripheral in question MUST be enabled, otherwise a crash will occur. This method is used whenever it is necessary to reset all the peripheral's registers to their initial states without disabling it. ## Peripherals @@ -25,26 +22,26 @@ Built-in peripherals are divided into three categories: ### Always-on peripherals -Below is the list of peripherals that are enabled by the system. The user code must **NEVER** attempt to disable them. +Below is the list of peripherals that are enabled by the system. The user code must NEVER attempt to disable them. If a corresponding API is provided, the user code must employ it in order to access the peripheral. *Table 1* — Peripherals enabled by the system -| Peripheral | Enabled at | -|:-------------:|:---------------------------:| -| DMA1 | `furi_hal_dma.c` | -| DMA2 | -- | -| DMAMUX | -- | -| GPIOA | `furi_hal_resources.c` | -| GPIOB | -- | -| GPIOC | -- | -| GPIOD | -- | -| GPIOE | -- | -| GPIOH | -- | -| PKA | `furi_hal_bt.c` | -| AES2 | -- | -| HSEM | -- | -| IPCC | -- | -| FLASH | enabled by hardware | +| Peripheral | Enabled at | +| :-----------: | :-----------------------: | +| DMA1 | `furi_hal_dma.c` | +| DMA2 | -- | +| DMAMUX | -- | +| GPIOA | `furi_hal_resources.c` | +| GPIOB | -- | +| GPIOC | -- | +| GPIOD | -- | +| GPIOE | -- | +| GPIOH | -- | +| PKA | `furi_hal_bt.c` | +| AES2 | -- | +| HSEM | -- | +| IPCC | -- | +| FLASH | enabled by hardware | ### On-demand system peripherals @@ -54,64 +51,63 @@ When not using the API, these peripherals MUST be enabled by the user code and t *Table 2* — Peripherals enabled and disabled by the system -| Peripheral | API header file | -|:--------------:|:------------------------:| -| RNG | `furi_hal_random.h` | -| SPI1 | `furi_hal_spi.h` | -| SPI2 | -- | -| I2C1 | `furi_hal_i2c.h` | -| I2C3 | -- | -| USART1 | `furi_hal_serial.h` | -| LPUART1 | -- | -| USB | `furi_hal_usb.h` | +| Peripheral | API header file | +| :-----------: | :-------------------: | +| RNG | `furi_hal_random.h` | +| SPI1 | `furi_hal_spi.h` | +| SPI2 | -- | +| I2C1 | `furi_hal_i2c.h` | +| I2C3 | -- | +| USART1 | `furi_hal_serial.h` | +| LPUART1 | -- | +| USB | `furi_hal_usb.h` | ### On-demand shared peripherals -Below is the list of peripherals that are not enabled by default and **MUST** be enabled by the user code each time it accesses them. +Below is the list of peripherals that are not enabled by default and MUST be enabled by the user code each time it accesses them. Note that some of these peripherals may also be used by the system to implement its certain features. - The system will take over any given peripheral only when the respective feature is in use. *Table 3* — Peripherals enabled and disabled by user -| Peripheral | System | Purpose | -|:----------:|:------:|:----------------------------------------| -| CRC | | | -| TSC | | | -| ADC | | | -| QUADSPI | | | -| TIM1 | yes | subghz, lfrfid, nfc, infrared, etc... | -| TIM2 | yes | subghz, infrared, etc... | -| TIM16 | yes | speaker | -| TIM17 | yes | cc1101_ext | -| LPTIM1 | yes | tickless idle timer | -| LPTIM2 | yes | pwm | -| SAI1 | | | -| LCD | | | +| Peripheral | System | Purpose | +| :-----------: | :-------: | ------------------------------------- | +| CRC | | | +| TSC | | | +| ADC | | | +| QUADSPI | | | +| TIM1 | yes | subghz, lfrfid, nfc, infrared, etc... | +| TIM2 | yes | subghz, infrared, etc... | +| TIM16 | yes | speaker | +| TIM17 | yes | cc1101_ext | +| LPTIM1 | yes | tickless idle timer | +| LPTIM2 | yes | pwm | +| SAI1 | | | +| LCD | | | + ## DMA -The `DMA1`, `DMA2` peripherals are a special case in that they have multiple independent channels. -Some channels may be in use by the system. +The DMA1,2 peripherals are a special case in that they have multiple independent channels. Some of the channels may be in use by the system. Below is the list of DMA channels and their usage by the system. *Table 4* — DMA channels -| DMA | Channel | System | Purpose | -|:------:|:-------:|:------:|:-----------------------------| -| DMA1 | 1 | yes | digital signal | -| -- | 2 | yes | -- | -| -- | 3 | | | -| -- | 4 | yes | pulse reader | -| -- | 5 | | | -| -- | 6 | yes | USART_Rx | -| -- | 7 | yes | LPUART_Rx | -| DMA2 | 1 | yes | infrared, lfrfid, subghz, | -| -- | 2 | yes | -- | -| -- | 3 | yes | cc1101_ext | -| -- | 4 | yes | cc1101_ext | -| -- | 5 | yes | cc1101_ext | -| -- | 6 | yes | SPI | -| -- | 7 | yes | SPI | +| DMA | Channel | System | Purpose | +| :---: | :-------: | :-------: | ------------------------- | +| DMA1 | 1 | yes | digital signal | +| -- | 2 | yes | -- | +| -- | 3 | | | +| -- | 4 | yes | pulse reader | +| -- | 5 | | | +| -- | 6 | yes | USART_Rx | +| -- | 7 | yes | LPUART_Rx | +| DMA2 | 1 | yes | infrared, lfrfid, subghz, | +| -- | 2 | yes | -- | +| -- | 3 | yes | cc1101_ext | +| -- | 4 | yes | cc1101_ext | +| -- | 5 | yes | cc1101_ext | +| -- | 6 | yes | SPI | +| -- | 7 | yes | SPI | diff --git a/documentation/devboard/Debugging via the Devboard.md b/documentation/devboard/Debugging via the Devboard.md new file mode 100644 index 000000000..49888cdbc --- /dev/null +++ b/documentation/devboard/Debugging via the Devboard.md @@ -0,0 +1,88 @@ +# Debugging via the Devboard {#dev_board_debugging_guide} + +On this page, you'll learn about how debugging via the Wi-Fi Developer Board works. To illustrate this process, we'll start a debug session for Flipper Zero's firmware in VS Code using the native Flipper Build Tool. + +*** + +## Overview + +The Developer Board acts as the debug probe, which provides a bridge between the IDE (integrated development environment) with a debugger running on a host computer and the target microcontroller (in your Flipper Zero). The user controls the debugging process on the computer connected to the Developer Board via [Wi-Fi](#dev_board_wifi_connection) or [USB cable](#dev_board_usb_connection). + +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_hardware_CDN.jpg width=700 + +Data exchange between the Wi-Fi Developer Board and your Flipper Zero is conducted via the Serial Wire Debug interface. The following GPIO pins serve this purpose: + +- **Pin 10:** Serial Wire Clock (SWCLK) + +- **Pin 12:** Serial Wire Debug Data I/O (SWDIO) + +To learn more about Flipper Zero pinout, visit [GPIO & modules in Flipper Docs](https://docs.flipper.net/gpio-and-modules). + +*** + +## Prerequisites + +### Step 1. Installing Git + +You'll need Git installed on your computer to clone the firmware repository. If you don't have Git, install it following the [official installation guide](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). + +### Step 2. Building the firmware + +Before starting debugging, you need to clone and build Flipper Zero firmware: + +1. Open the **Terminal** (on Linux & macOS) or **PowerShell** (on Windows) in the directory where you want to store the firmware source code. + +2. Clone the firmware repository: + + ``` + git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git + cd flipperzero-firmware + ``` + +3. Run the **Flipper Build Tool (FBT)** to build the firmware: + + ``` + ./fbt + ``` + +*** + +## Debugging the firmware + +From the **flipperzero-firmware** directory that you cloned earlier, run the following command: + +``` +./fbt flash +``` + +This will upload the firmware you've just built to your Flipper Zero via the Developer Board. After that, you can start debugging the firmware. We recommend using **VS Code** with the recommended extensions (as described below), and we have pre-made configurations for it. + +To debug in **VS Code**, do the following: + +1. In VS Code, open the **flipperzero-firmware** directory. + +2. You should see a notification about recommended extensions. Install them. + + If there were no notifications, open the **Extensions** tab, enter `@recommended` in the search bar, and install the workspace recommendations. + +3. Run the `./fbt vscode_dist` command. This will generate the VS Code configuration files needed for debugging. + +4. In VS Code, open the **Run and Debug** tab and select a debugger from the dropdown menu: + + - **Attach FW (blackmagic):** Can be used via **Wi-Fi** or **USB** + - **Attach FW (DAP):** Can be used via **USB** only + + Note that when debugging via USB, you need to make sure the selected debugger matches the debug mode on your Devboard. To check the debug mode on your Devboard, access the Devboard's web interface as described [here](#dev_board_wifi_connection) and check the **USB mode** field. If you want to use a different debug mode, enable this mode by following the steps in [Devboard debug modes](#dev_board_debug_modes). + +5. If needed, flash your Flipper Zero with the `./fbt flash` command, then click the ▷ **Start Debugging** button in the debug sidebar to start the debugging session. + +6. Note that starting a debug session halts the execution of the firmware, so you'll need to click the I▷ **Continue** button on the toolbar at the top of your VS Code window to continue execution. + +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_VS_Code.jpg width=900 + +> [!note] +> If you want to use a different debug mode on your Developer Board, visit [Devboard debug modes](#dev_board_debug_modes). +> +> If you want to read logs via the Developer Board, see [Reading logs via the Devboard](#dev_board_reading_logs). +> +> To learn about debugging in VS Code, see [VS Code official guide](https://code.visualstudio.com/docs/editor/debugging). diff --git a/documentation/devboard/Devboard debug modes.md b/documentation/devboard/Devboard debug modes.md new file mode 100644 index 000000000..a6550cbbb --- /dev/null +++ b/documentation/devboard/Devboard debug modes.md @@ -0,0 +1,33 @@ +# Devboard debug modes {#dev_board_debug_modes} + +The Wi-Fi Devboard for Flipper Zero supports **Black Magic** and **DAPLink** debug modes, and you can switch between them depending on your needs. Note that available modes depend on connection: + +- **Wi-Fi:** Only **Black Magic** mode is available. +- **USB:** Switch between **Black Magic** (default) and **DAPLink**. Learn more about switching debug modes for USB connection below. + +> [!note] +> Black Magic mode doesn't support RTOS threads, but you can still perform other debugging operations. + +*** + +## Switching debug modes for USB connection + +Switching debug modes for working via USB has to be done wirelessly (yes, you read that correctly). Additionally, depending on how the Devboard wireless connection is configured, you may need to follow different steps for **Wi-Fi access point mode** or **Wi-Fi client mode**: + +1. If the Devboard isn't connected to your Flipper Zero, turn off your Flipper Zero and connect the Developer Board, then turn the device back on. + +2. Access the Devboard's web interface: + + - [Wi-Fi access point mode](#wifi-access-point) + + - [Wi-Fi client mode](#wifi-client-mode) + +3. In the **WiFi** tab, click the **USB mode** option and select **BlackMagicProbe** or **DapLink**. + +4. Click **SAVE**, then click **REBOOT** to apply the changes. + +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_switching_modes_CDN.jpg width=700 + +> [!note] +> After switching debug modes on your Devboard, remember to select the same debugger in **VS Code** in the **Run and Debug** tab, and click the ▷ **Start Debugging** button. + diff --git a/documentation/devboard/Firmware update on Developer Board.md b/documentation/devboard/Firmware update on Developer Board.md index 0df5c1f7d..0b88e52be 100644 --- a/documentation/devboard/Firmware update on Developer Board.md +++ b/documentation/devboard/Firmware update on Developer Board.md @@ -1,104 +1,112 @@ # Firmware update on Developer Board {#dev_board_fw_update} -> [!IMPORTANT] -> -> It's important to regularly update your Developer Board to ensure that you have access to the latest features and bug fixes. -> This tutorial will guide you through the necessary steps to update the firmware of your Developer Board. +It's important to regularly update your Developer Board to ensure that you have access to the latest features and bug fixes. This page will guide you through the necessary steps to update the firmware of your Developer Board. -This tutorial assumes that you're familiar with the basics of the command line. +> [!note] +> This guide assumes that you're familiar with the basics of the command line. If you're new to it, we recommend checking out these [Windows](https://learn.microsoft.com/en-us/powershell/scripting/learn/ps101/01-getting-started?view=powershell-7.4) or [macOS/Linux](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) command line tutorials. -If you’re not, please refer to the [Windows](https://www.digitalcitizen.life/command-prompt-how-use-basic-commands/) or [MacOS / Linux](https://ubuntu.com/tutorials/command-line-for-beginners#1-overview) command line tutorials. +*** -## Installing the micro Flipper Build Tool +## Step 1. Install the micro Flipper Build Tool -Micro Flipper Build Tool (uFBT) is a cross-platform tool that enables basic development tasks for Flipper Zero, such as building and debugging applications, flashing firmware, and creating VS Code development configurations. + is a cross-platform tool developed and supported by our team that enables basic development tasks for Flipper Zero, such as building and debugging applications, flashing firmware, creating VS Code development configurations, and flashing firmware to the Wi-Fi Developer Board. -Install uFBT on your computer by running the following command in the Terminal: +**On Linux & macOS:** -**For Linux & macOS:** +Run the following command in the Terminal: -```bash +``` python3 -m pip install --upgrade ufbt ``` -**For Windows:** +**On Windows:** -```powershell -python -m pip install --upgrade ufbt -``` +1. Download the latest version of Python on +2. Run the following command in the PowerShell -If you want to learn more about uFBT, visit [the project's page](https://pypi.org/project/ufbt/). + ``` + py -m pip install --upgrade ufbt + ``` -## Connecting the Developer Board to your computer +*** + +## Step 2. Connect the Devboard to PC + +To update the firmware, you need to switch your Developer Board to Bootloader mode, connect to a PC via a USB cable, and make sure that the PC detects the Developer Board: 1. List all of the serial devices on your computer. - **Windows** + - **macOS:** Run the `ls /dev/cu.*` command in the Terminal. - On Windows, go to Device Manager and expand the Ports (COM & LPT) section. + - **Linux:** Run the `ls /dev/tty*` command in the Terminal. - **macOS** + - **Windows:** Go to **Device Manager** and expand the **Ports (COM & LPT)** section. - On macOS, you can run the following command in the Terminal: - ```bash - ls /dev/cu.* - ``` - - **Linux** - - On Linux, you can run the following command in the Terminal: - ```text - ls /dev/tty* - ``` - View the devices in the list. 2. Connect the Developer Board to your computer using a USB-C cable. -![The Developer Board in Wired mode](https://github.com/user-attachments/assets/d13e4e90-d83d-45bf-8787-6eadba590795) -4. Switch your Developer Board to Bootloader mode: - 3.1. Press and hold the **BOOT** button. - 3.2. Press the **RESET** button while holding the **BOOT** button. - 3.3. Release the **BOOT** button. - ![You can easily switch the Dev Board to Bootloader mode](https://github.com/user-attachments/assets/aecc957f-f37b-4bec-af9f-9efd4837152e) -6. Repeat Step 1 and view the name of your Developer Board that appeared in the list. - For example, on macOS: + \image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_update_wired_connection.jpg width=700 - ```shell - /dev/cu.usbmodem01 - ``` +3. Switch your Developer Board to Bootloader mode: -## Flashing the firmware + 1. Press and hold the **BOOT** button. + 2. Press the **RESET** button while holding the **BOOT** button. + 3. Release the **BOOT** button. -To flash the firmware onto your Developer Board, run the following command in the terminal: + \image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_reboot_to_bootloader.png width=700 -```shell +4. Repeat **Step 1** and view the name of your Developer Board that appeared in the list. + +*** + +## Step 3. Flash the firmware + +**On Linux & macOS:** + +``` python3 -m ufbt devboard_flash ``` +**On Windows:** Run the following command in the PowerShell: + +``` +py -m ufbt devboard_flash +``` + You should see the following message: `WiFi board flashed successfully`. -## If flashing failed +### If flashing failed -If you get an error message during the flashing process, such as this: +Occasionally, you might get an error message during the flashing process, such as: -```shell +``` A fatal error occurred: Serial data stream stopped: Possible serial noise or corruption. ``` -Or this: +*or* -```shell +``` FileNotFoundError: [Errno 2] No such file or directory: '/dev/cu.usbmodem01' ``` -Try doing the following: -* Disconnect the Developer Board from your computer, then reconnect it. -* Use a different USB port on your computer. -* Use a different USB-C cable. +To fix it, try doing the following: -## Finishing the installation +- Disconnect the Developer Board from your computer, then reconnect it. After that, switch your Developer Board to Bootloader mode once again, as described in -After flashing the firmware: -1. Reboot the Developer Board by pressing the **RESET** button. ![Reset the Developer Board](https://github.com/user-attachments/assets/7527dd7b-eaa5-4fac-8d67-7ba52e552756) -3. Disconnect and reconnect the USB-C cable. +- Use a different USB port on your computer. + +- Use a different USB-C cable. + +*** + +## Step 4. Finish the installation + +1. Reboot the Developer Board by pressing the **RESET** button. + + \image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_reboot_after_flashing.jpg width=700 + +2. Disconnect and reconnect the USB-C cable. + + You've successfully updated the firmware of your Developer Board! + +If you followed the **Get started with the Devboard** guide, you're ready for the next step: [Step 3. Plug the Devboard into Flipper Zero](#dev_board_get_started_step-3). -The Developer Board should appear as a serial device on your computer. Now, you can use it with the Black Magic Debug client of your choice. diff --git a/documentation/devboard/Get started with the Dev Board.md b/documentation/devboard/Get started with the Dev Board.md index a679ccb11..141bf6411 100644 --- a/documentation/devboard/Get started with the Dev Board.md +++ b/documentation/devboard/Get started with the Dev Board.md @@ -1,175 +1,80 @@ # Get started with the Dev Board {#dev_board_get_started} -The Wi-Fi Developer Board serves as a tool to debug the Flipper Zero firmware. To debug the firmware, the initial step involves compiling the firmware from its source code. This process enables the debugging functionality within the firmware and generates all the necessary files required for debugging purposes. +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_developer_board_box_CDN.jpg width=700 -> [!IMPORTANT] -> -> Building and debugging the Flipper Zero firmware is fully supported on MacOS and Linux. -> Support for Windows is in beta test. - -## Updating the firmware of your Developer Board - -Update the firmware of your Developer Board before using it. For more information, visit [Firmware update on Developer Board](https://docs.flipperzero.one/development/hardware/wifi-debugger-module/update). - -## Installing Git - -You'll need Git installed on your computer to clone the firmware repository. If you don't have Git, install it by doing the following: - -### MacOS - -On MacOS, install the **Xcode Command Line Tools** package, which includes Git as one of the pre-installed command-line utilities, by running in the Terminal the following command: - -```bash -xcode-select --install -``` - -### Linux - -On Linux, you can install Git using your package manager. For example, on Ubuntu, run in the Terminal the following command: - -```bash -sudo apt install git -``` - -For other distributions, refer to your package manager documentation. +Before you start using your Devboard, you need to prepare your Flipper Zero and Devboard for debugging. In this guide, we'll walk you through all the necessary steps and provide links to explore the Devboard's capabilities further. *** -## Building the firmware +## Step 1. Enable Debug Mode on your Flipper Zero -First, clone the firmware repository: +Since the main purpose of the Developer board is to debug applications on Flipper Zero, you first need to enable Debug Mode. To do so, go to **Settings → System** and set **Debug** to **ON**. -```bash -git clone --recursive https://github.com/flipperdevices/flipperzero-firmware.git -cd flipperzero-firmware -``` +\image html https://cdn.flipperzero.one/Flipper_Zero_enamble_debug_CDN.jpg width=700 -Then, run the **Flipper Build Tool** (FBT) to build the firmware: +> [!note] +> Debug Mode needs to be re-enabled after each update of Flipper Zero's firmware. -```bash -./fbt -``` +Debug Mode allows you to debug your apps for Flipper Zero, as well as access debugging options in apps via the user interface and CLI. To learn more about Flipper Zero CLI, visit [Command-line interface in Flipper Docs](https://docs.flipper.net/development/cli). + +\image html https://cdn.flipperzero.one/Flipper_Zero_Command_Line_Interface_CDN.jpg width=700 *** -## Connecting the Developer Board +## Step 2. Update firmware on the Developer Board -The Developer Board can work in the **Wired** mode and two **Wireless** modes: **Wi-Fi access point (AP)** mode and **Wi-Fi client (STA)** mode. The Wired mode is the simplest to set up, but requires a USB Type-C cable. The Wireless modes are more complex to set up, but they allow you to debug your Flipper Zero wirelessly. +The Developer Board comes with stock firmware that may not include all the latest features and bug fixes. To ensure optimal performance, please update your board's firmware using the instructions in [Firmware update on Devboard](#dev_board_fw_update). -> [!TIP] -> -> Use the following credentials when connecting to the Developer Board in **Wi-Fi access point** mode: -> Name: **blackmagic** -> Password: **iamwitcher** +*** -## Wired +## Step 3. Plug the Devboard into Flipper Zero {#dev_board_get_started_step-3} -![The Developer Board in Wired mode](https://github.com/user-attachments/assets/32938d4a-20b7-4a53-8b36-608cf0112c9a) +Once your Developer Board firmware is up to date, you can proceed to plug it into your Flipper Zero. Two important things to keep in mind: -To connect the Developer Board in **Wired** mode, do the following: +1. **Power off your Flipper Zero before plugging in the Developer Board.** -1. Cold-plug the Developer Board by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on. + If you skip this step, you may corrupt the data stored on the microSD card. Connecting external modules with a large capacitive load may affect the microSD card's power supply since both the microSD card and external module are powered from the same 3.3 V power source inside Flipper Zero. -2. On your computer, open the **Terminal** and run the following: +2. **Make sure the Developer Board is inserted all the way in.** - ### MacOS - - ```shell - ls /dev/cu.* - ``` - - ### Linux - - ```bash - ls /dev/tty* - ``` - - Note the list of devices. + If your Flipper Zero isn't in a silicone case, insert the module all the way in so there is no gap between your Flipper Zero and the Devboard. You may need to apply more force to insert it completely. After that, press and hold the **BACK** button to power on your Flipper Zero. -3. Connect the Developer Board to your computer via a USB-C cable. + \image html https://cdn.flipperzero.one/Flipper_Zero_external_module_without_case_CDN.jpg width=700 -4. Rerun the command. Two new devices have to appear: this is the Developer Board. + If your Flipper Zero is in a silicone case, insert the module all the way in so there is no gap in the middle between the silicone case and the module. After that, press and hold the **BACK** button to power on your Flipper Zero. -> [!NOTE] -> -> If the Developer Board doesn't appear in the list of devices, try using a different cable, USB port, or computer. + \image html https://cdn.flipperzero.one/Flipper_Zero_external_module_with_case_CDN.jpg width=700 -
+*** -> [!IMPORTANT] -> -> Flipper Zero logs can only be viewed when the Developer Board is connected via USB. -> The option to view logs over Wi-Fi will be added in future updates. -> For more information, visit [Reading logs via the Dev Board](https://docs.flipperzero.one/development/hardware/wifi-debugger-module/reading-logs). +## Step 4. Connect to a computer -## Wireless +Now, you can connect the Developer Board to your computer via USB or Wi-Fi, depending on your needs. We described both methods in separate documents: -### Wi-Fi access point (AP) mode +- **[Via USB cable](#dev_board_usb_connection)** for debugging in DAP Link or Black Magic mode, and reading logs. +- [Via Wi-Fi](#dev_board_wifi_connection) for debugging in Black Magic mode. -![The Developer Board in Wi-Fi access point mode](https://github.com/user-attachments/assets/1f210e91-3ac8-4f4c-a910-cc7c52b94346) +*** -Out of the box, the Developer Board is configured to work as a **Wi-Fi access point**. This means it'll create its own Wi-Fi network to which you can connect. If your Developer Board doesn't create a Wi-Fi network, it is probably configured to work in **Wi-Fi client** mode. To reset your Developer Board back to **Wi-Fi access point** mode, press and hold the **BOOT** button for 10 seconds, then wait for the module to reboot. +## Next steps -![You can reconfigure the Developer Board mode by pressing and holding the BOOT button](https://github.com/user-attachments/assets/8fee05de-fb1e-475a-b23a-d1ddca9cd701) +You are ready to debug now! To further explore what you can do with the Devboard, check out these pages: -To connect the Developer Board in **Wi-Fi access point** mode, do the following: +- [Debugging via the Devboard](#dev_board_debugging_guide) +- [Devboard debug modes](#dev_board_debug_modes) +- [Reading logs via the Devboard](#dev_board_reading_logs) -1. Cold-plug the Developer Board by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on. -2. Open Wi-Fi settings on your client device (phone, laptop, or other). -3. Connect to the network: - * Name: `blackmagic` - * Password: `iamwitcher` -4. To configure the Developer Board, open a browser and go to `http://192.168.4.1`. +These guides should help you get started with your Devboard. If you have any questions or you want to share your experience, don't hesitate to join our community on [Reddit](https://www.reddit.com/r/flipperzero/) and [Discord](https://discord.com/invite/flipper), where we have a dedicated #wifi-devboard channel. -### Wi-Fi client (STA) mode -![The Developer Board in Wi-Fi client mode](https://github.com/user-attachments/assets/42e7e69e-51b0-4914-b082-431c68bc75d3) -To connect the Developer Board in **Wi-Fi client** mode, you need to configure it to connect to your Wi-Fi network by doing the following: -1. Cold-plug the Developer Board by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on. -2. Connect to the Developer Board in **Wi-Fi access point** mode. -3. In a browser, go to the configuration page on `http://192.168.4.1`. -4. Select the **STA** mode and enter your network's **SSID** (name) and **password**. For convenience, you can click the **+** button to see the list of nearby networks. -5. Save the configuration and reboot the Developer Board. -6. In the Wi-Fi tab, you can set the Developer Board mode -![Developer Board mode](https://github.com/user-attachments/assets/fbeea000-1117-4297-8a0d-5d580123e938) -After rebooting, the Developer Board connects to your Wi-Fi network. You can connect to the device using the mDNS name `blackmagic.local` or the IP address it got from your router (you'll have to figure this out yourself, every router is different). -After connecting to your debugger via [http://blackmagic.local](http://blackmagic.local), you can find its IP address in the **SYS** tab. You can also change the debugger's mode to **AP** or **STA** there. -![In the SYS tab, you can view the IP address of your Developer Board](https://github.com/user-attachments/assets/aa3afc64-a2ec-46a6-a827-eea187a97c04) -## Debugging the firmware -Open the **Terminal** in the `flipperzero-firmware` directory that you cloned earlier and run the following command: -```bash -./fbt flash -``` -This will upload the firmware you've just built to your Flipper Zero via the Developer Board. After that, you can start debugging the firmware using the [GDB](https://www.gnu.org/software/gdb/) debugger. We recommend using **VSCode** with the recommended extensions, and we have pre-made configurations for it. -To debug in **VSCode**, do the following: - -1. In VSCode, open the `flipperzero-firmware` directory. -2. You should see a notification about recommended extensions. Install them. -> [!TIP] -> -> If there were no notifications, open the `Extensions` tab, -> enter `@recommended` in the search bar, -> and install the workspace recommendations. -> -3. In the **Terminal**, run the `./fbt vscode_dist` command. This will generate the VSCode configuration files needed for debugging. -4. In VSCode, open the **Run and Debug** tab and select **Attach FW (blackmagic)** from the dropdown menu. -5. If needed, flash your Flipper Zero with the `./fbt flash` command, then click the **Play** button in the debug sidebar to start the debugging session. -6. Note that starting a debug session halts the execution of the firmware, so you'll need to click the **Continue** button on the toolbar at the top of your VSCode window to continue execution. - -![Click Continue in the toolbar to continue execution of the firmware](https://github.com/user-attachments/assets/74f26bdb-8511-4e5a-8aa8-c44212aa6228) - -To learn about debugging, visit the following pages: - -* [Debugging with GDB](https://sourceware.org/gdb/current/onlinedocs/gdb.pdf) -* [Debugging in VSCode](https://code.visualstudio.com/docs/editor/debugging) diff --git a/documentation/devboard/Reading logs via the Dev Board.md b/documentation/devboard/Reading logs via the Dev Board.md index bed665d40..011b331e8 100644 --- a/documentation/devboard/Reading logs via the Dev Board.md +++ b/documentation/devboard/Reading logs via the Dev Board.md @@ -9,17 +9,19 @@ The Developer Board allows you to read Flipper Zero logs via UART. Unlike readin ## Setting the log level -Depending on your needs, you can set the log level by going to **Main Menu → Settings → Log Level**. To learn more about logging levels, visit [Settings](https://docs.flipperzero.one/basics/settings#d5TAt). +Depending on your needs, you can set the log level by going to **Main Menu → Settings → Log Level**. To learn more about logging levels, visit [Settings](https://docs.flipper.net/basics/settings#d5TAt). -![You can manually set the preferred log level](https://github.com/user-attachments/assets/b1317d01-8b9b-4544-8720-303c87b85324) +\image html https://cdn.flipperzero.one/Flipper_Zero_log_level.jpg "You can manually set the preferred log level" width=700 + +*** ## Viewing Flipper Zero logs Depending on your operating system, you need to install an additional application on your computer to read logs via the Developer Board: -### MacOS +### macOS -On MacOS, you need to install the `minicom` communication program by doing the following: +On macOS, you need to install the **minicom** communication program by doing the following: 1. [Install Homebrew](https://brew.sh/) by running the following command in the Terminal: ```bash @@ -41,9 +43,11 @@ After installation of `minicom` on your macOS computer, you can connect to the D > > The list of devices. 3. Connect the developer board to your computer using a USB Type-C cable. - ![Connect the developer board with a USB-C cable](https://github.com/user-attachments/assets/0f469a31-2dd1-4559-918a-ff3ca3309531) -5. Rerun the command. Two new devices have to appear: this is the Developer Board. - ```bash +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_developer_board_wired.png width=700 + +4. Rerun the command. Two new devices have to appear: this is the Developer Board. + + ```text /dev/cu.usbmodemblackmagic1 ``` ```bash @@ -81,7 +85,8 @@ After installation of `minicom` on your Linux computer, you can connect to the D ``` Note the list of devices. 3. Connect the developer board to your computer using a USB Type-C cable. - ![Connect the developer board with a USB-C cable](https://github.com/user-attachments/assets/0f469a31-2dd1-4559-918a-ff3ca3309531) +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_developer_board_wired.png width=700 + 4. Rerun the command. Two new devices have to appear: this is the Developer Board. ```bash /dev/ttyACM0 @@ -116,12 +121,18 @@ On Windows, do the following: 1. On your computer, [install the PuTTY application](https://www.chiark.greenend.org.uk/\~sgtatham/putty/latest.html). 2. Cold-plug the Developer Board into your Flipper Zero by turning off the Flipper Zero, connecting the developer board, and then turning it back on. 3. Connect the developer board to your computer using a USB Type-C cable. - ![Connect the developer board with a USB-C cable](https://github.com/user-attachments/assets/0f469a31-2dd1-4559-918a-ff3ca3309531) -4. Find the serial port that the developer board is connected to by going to `Device Manager → Ports (COM & LPT)` and looking for a new port that appears when you connect the Wi-Fi developer board. - ![Find the serial port in your Device Manager](https://github.com/user-attachments/assets/aa542fe6-4781-45dc-86f6-e98ab34952b0) -6. Run the `PuTTY` application and select `Serial` as the connection type. -7. Enter the port number you found in the previous step into the `Serial line` field. -8. Set the `Speed` parameter to `230400` and click `Open`. - ![Set speed to 230400](https://github.com/user-attachments/assets/93463c78-9776-479b-a6cc-d68ed712d0c4) -10. View logs of your Flipper Zero in the PuTTY terminal window. -11. To quit, close the PuTTY window. +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_developer_board_wired.png width=700 + +4. Find the serial port that the developer board is connected to by going to **Device Manager → Ports (COM & LPT)** and looking for a new port that appears when you connect the Wi-Fi developer board. +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_Device_Manager.png width=700 + +5. Run the PuTTY application and select **Serial** as the connection type. + +6. Enter the port number you found in the previous step into the **Serial line** field. + +7. Set the **Speed** parameter to **230400** and click **Open**. +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_PuTTy.jpg width=700 + +8. View logs of your Flipper Zero in the PuTTY terminal window. + +9. To quit, close the PuTTY window. diff --git a/documentation/devboard/USB connection to the Devboard.md b/documentation/devboard/USB connection to the Devboard.md new file mode 100644 index 000000000..af0d9113d --- /dev/null +++ b/documentation/devboard/USB connection to the Devboard.md @@ -0,0 +1,22 @@ +# USB connection to the Devboard {#dev_board_usb_connection} + +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_USB_connection_CDN.jpg width=700 + +To connect to the Developer Board via USB, do the following: + +1. If the Devboard isn't connected to your Flipper Zero, turn off your Flipper Zero and connect the Developer Board to it. Then, turn your Flipper Zero back on. + +2. On your computer, check the list of serial devices. + + - **macOS:** On your computer, run `ls /dev/cu.*` in the Terminal. + + - **Linux:** On your computer, run `ls /dev/tty*` in the Terminal. + + - **Windows:** Go to **Device Manager** and expand the **Ports (COM & LPT)** section. + +3. Connect the Devboard to your computer via a USB-C cable. + +4. Repeat **Step 2**. Two new devices will appear — this is the Developer Board. + +> [!warning] +> If the Developer Board doesn't appear in the list of devices, try using a different cable, USB port, or computer. \ No newline at end of file diff --git a/documentation/devboard/Wi-Fi connection to the Devboard.md b/documentation/devboard/Wi-Fi connection to the Devboard.md new file mode 100644 index 000000000..fd3bc3249 --- /dev/null +++ b/documentation/devboard/Wi-Fi connection to the Devboard.md @@ -0,0 +1,60 @@ +# Wi-Fi connection to the Devboard {#dev_board_wifi_connection} + +You can connect to the Developer Board wirelessly in two ways: + +- **Wi-Fi access point mode (default):** The Devboard creates its own Wi-Fi network, which you can connect to in order to access its web interface and debug via Wi-Fi. The downside is that you will need to disconnect from your current Wi-Fi network, resulting in a loss of internet connection. + +- **Wi-Fi client mode:** You can connect to the Devboard through an existing Wi-Fi network, allowing you to access the Devboard web interface and debug via Wi-Fi without losing your internet connection. + +Let's go over both of these modes below. + +*** + +## Wi-Fi access point (AP) mode {#wifi-access-point} + +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_Access_Point_CDN.jpg width=700 + +Out of the box, the Developer Board is configured to work as a Wi-Fi access point. To connect the Developer Board in this mode, do the following: + +1. Plug the Wi-Fi Devboard into your Flipper Zero by turning off your Flipper Zero and connecting the Developer Board, and then turning it back on. + +2. Open Wi-Fi settings on your client device (phone, laptop, or other). + +3. Connect to the network: + + Name: `blackmagic` + Password: `iamwitcher` + + If your computer fails to find the **blackmagic** network, read the [troubleshooting section](#wifi-access-point_troubleshooting) below. + +4. To access the Devboard's web interface, open a browser and go to or . + +### If your computer fails to find the black magic network {#wifi-access-point_troubleshooting} + +- Reset Wi-Fi connection on your computer. + +- The Developer Board is probably configured to work in Wi-Fi client mode. → Reset your Developer Board settings to default by pressing and holding the **BOOT** button for **10 seconds**, then wait for the Devboard to reboot. After the reset, the Devboard will work in Wi-Fi access point mode. + +\image html https://cdn.flipperzero.one/Flipper_Zero_Wi-Fi_devboard_reboot.jpg width=700 + +*** + +## Wi-Fi client (STA) mode {#wifi-client-mode} + +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_STA_CDN.jpg width=700 + +To connect the Developer Board in **Wi-Fi client** mode, you need to configure it to connect to your Wi-Fi network by doing the following: + +1. Plug the Wi-Fi Devboard into your Flipper Zero by turning off your Flipper Zero and connecting the Developer Board, and then turning the device back on. + +2. Connect to the Developer Board in [Wi-Fi access point](#wifi-access-point) mode. + +3. In a browser, go to the Devboard's web interface at or . + +4. Select the **STA** mode and enter your network's **SSID** (name) and **password**. For convenience, you can click the **+** button to see the list of nearby 2.4 GHz networks (5 GHz networks aren't supported). + +5. Save the configuration and reboot the Developer Board. + + \image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_connect_to_WiFi_CDN.jpg width=700 + +6. Now, you can access the Devboard's web interface at [http://blackmagic.local](https://blackmagic.local) via the existing Wi-Fi network without losing connection to the internet. 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/dev_board.dox b/documentation/doxygen/dev_board.dox index 6caa44c70..8d5a2bf35 100644 --- a/documentation/doxygen/dev_board.dox +++ b/documentation/doxygen/dev_board.dox @@ -1,10 +1,37 @@ /** -@page dev_board Developer Board +@page dev_board Wi-Fi Developer Board -[ESP32-based development board](https://shop.flipperzero.one/collections/flipper-zero-accessories/products/wifi-devboard). +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_devboard_laptop_CDN.jpg width=700 + +Wi-Fi-enabled Developer Board brings debugging and firmware update capabilities to your Flipper Zero. The Developer Board is based on the ESP32-S2 MCU with custom firmware incorporating Black Magic Debug and CMSIS-DAP, and is built with ESP-IDF. It can flash and debug various microprocessors and microcontrollers (including the one used in your Flipper Zero) via Wi-Fi or USB cable. + +The Developer Board provides a debug interface, allowing developers to halt program execution, set breakpoints, inspect variables and memory, and step through code execution. + +

+Get your Wi-Fi Developer Board +
+
+ +Check out these guides to get started with the Devboard: - @subpage dev_board_get_started — Quick start for new users +- @subpage dev_board_fw_update — Keep the Developer Board up to date +- @subpage dev_board_usb_connection — Instructions for Windows, macOS and Linux +- @subpage dev_board_wifi_connection — Instructions for Windows, macOS and Linux +- @subpage dev_board_debugging_guide — Learn how it works +- @subpage dev_board_debug_modes — Available modes and how to switch between them - @subpage dev_board_reading_logs — Find out what is currently happening on the system -- @subpage dev_board_fw_update — Keep the developer board up to date + +## Hardware + +The Developer Board is equipped with an [ESP32-S2-WROVER](https://www.espressif.com/en/products/socs/esp32-s2) module, which includes built-in Wi-Fi capabilities. It also offers GPIO pins for easy connectivity to various targets. Additionally, the Developer Board features a USB Type-C connector for data transfer and power supply. For user interaction, the Developer Board has tactile switches. + +\image html https://cdn.flipperzero.one/Flipper_Zero_WiFi_developer_board_hardware_CDN.jpg width=700 + +## Additional resources + +To learn more about the Wi-Fi Developer Board hardware, visit [Schematics in Flipper Docs](https://docs.flipperzero.one/development/hardware/wifi-debugger-module/schematics). + +For additional information about Flipper Zero GPIO pins, visit [GPIO & modules in Flipper Docs](https://docs.flipperzero.one/gpio-and-modules). */ diff --git a/documentation/doxygen/header.html b/documentation/doxygen/header.html index 5cc0aba38..e987e39aa 100644 --- a/documentation/doxygen/header.html +++ b/documentation/doxygen/header.html @@ -52,7 +52,7 @@ $extrastylesheet -
$projectname $projectnumber +
$projectname $projectnumber
$projectbrief
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/fbt.md b/documentation/fbt.md index 9a17e4180..cf64f1eac 100644 --- a/documentation/fbt.md +++ b/documentation/fbt.md @@ -57,15 +57,9 @@ Additionally, `compile_commands.json` is generated in that folder (it is used fo `build/latest` symlink & compilation database are only updated upon *firmware build targets* — that is, when you're re-building the firmware itself. Running other tasks, like firmware flashing or building update bundles *for a different debug/release configuration or hardware target*, does not update `built/latest` dir to point to that configuration. -Running other tasks, like firmware flashing or building update bundles *for a different debug/release configuration or hardware target*, does not update `built/latest` dir to point to that configuration. - ## VSCode integration -`fbt` includes basic development environment configuration for VSCode. Run `./fbt vscode_dist` to deploy it. - -That will copy the initial environment configuration to the `.vscode` folder. - -After that, you can use that configuration by starting VSCode and choosing the firmware root folder in the File > Open Folder menu. +`fbt` includes basic development environment configuration for VS Code. Run `./fbt vscode_dist` to deploy it. That will copy the initial environment configuration to the `.vscode` folder. After that, you can use that configuration by starting VS Code and choosing the firmware root folder in the "File > Open Folder" menu. To use language servers other than the default VS Code C/C++ language server, use `./fbt vscode_dist LANG_SERVER=` instead. 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 c2052b2ca..96d035c4a 100644 --- a/fbt_options.py +++ b/fbt_options.py @@ -116,6 +116,7 @@ FIRMWARE_APPS = { "updater_app", "radio_device_cc1101_ext", "unit_tests", + "js_app", ], } diff --git a/firmware.scons b/firmware.scons index 58c2207dd..aa91c7e3b 100644 --- a/firmware.scons +++ b/firmware.scons @@ -221,6 +221,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/furi/core/kernel.c b/furi/core/kernel.c index f3f84e692..34c562bb3 100644 --- a/furi/core/kernel.c +++ b/furi/core/kernel.c @@ -33,7 +33,7 @@ bool furi_kernel_is_irq_or_masked(void) { } bool furi_kernel_is_running(void) { - return xTaskGetSchedulerState() != taskSCHEDULER_RUNNING; + return xTaskGetSchedulerState() == taskSCHEDULER_RUNNING; } int32_t furi_kernel_lock(void) { @@ -129,6 +129,8 @@ uint32_t furi_kernel_get_tick_frequency(void) { void furi_delay_tick(uint32_t ticks) { furi_check(!furi_kernel_is_irq_or_masked()); + furi_check(furi_thread_get_current_id() != xTaskGetIdleTaskHandle()); + if(ticks == 0U) { taskYIELD(); } else { @@ -138,6 +140,7 @@ void furi_delay_tick(uint32_t ticks) { FuriStatus furi_delay_until_tick(uint32_t tick) { furi_check(!furi_kernel_is_irq_or_masked()); + furi_check(furi_thread_get_current_id() != xTaskGetIdleTaskHandle()); TickType_t tcnt, delay; FuriStatus stat; diff --git a/furi/core/log.c b/furi/core/log.c index f8110b46a..fb0c96711 100644 --- a/furi/core/log.c +++ b/furi/core/log.c @@ -108,10 +108,17 @@ void furi_log_puts(const char* data) { } void furi_log_print_format(FuriLogLevel level, const char* tag, const char* format, ...) { - if(level <= furi_log.log_level && - furi_mutex_acquire(furi_log.mutex, FuriWaitForever) == FuriStatusOk) { - FuriString* string; - string = furi_string_alloc(); + do { + if(level > furi_log.log_level) { + break; + } + + if(furi_mutex_acquire(furi_log.mutex, furi_kernel_is_running() ? FuriWaitForever : 0) != + FuriStatusOk) { + break; + } + + FuriString* string = furi_string_alloc(); const char* color = _FURI_LOG_CLR_RESET; const char* log_letter = " "; @@ -157,7 +164,7 @@ void furi_log_print_format(FuriLogLevel level, const char* tag, const char* form furi_log_puts("\r\n"); furi_mutex_release(furi_log.mutex); - } + } while(0); } void furi_log_print_raw_format(FuriLogLevel level, const char* format, ...) { diff --git a/furi/core/thread.c b/furi/core/thread.c index ea7454f23..73e42b4c0 100644 --- a/furi/core/thread.c +++ b/furi/core/thread.c @@ -1,7 +1,8 @@ -#include "thread.h" +#include "thread_i.h" #include "thread_list_i.h" #include "timer.h" #include "kernel.h" +#include "message_queue.h" #include "memmgr.h" #include "memmgr_heap.h" #include "check.h" @@ -69,6 +70,8 @@ static_assert(offsetof(FuriThread, container) == 0); // Our idle priority should be equal to the one from FreeRTOS static_assert(FuriThreadPriorityIdle == tskIDLE_PRIORITY); +static FuriMessageQueue* furi_thread_scrub_message_queue = NULL; + static size_t __furi_thread_stdout_write(FuriThread* thread, const char* data, size_t size); static int32_t __furi_thread_stdout_flush(FuriThread* thread); @@ -127,7 +130,9 @@ static void furi_thread_body(void* context) { furi_thread_set_state(thread, FuriThreadStateStopping); - vTaskDelete(NULL); + furi_message_queue_put(furi_thread_scrub_message_queue, &thread, FuriWaitForever); + + vTaskSuspend(NULL); furi_thread_catch(); } @@ -161,6 +166,31 @@ static void furi_thread_init_common(FuriThread* thread) { } } +void furi_thread_init(void) { + furi_thread_scrub_message_queue = furi_message_queue_alloc(8, sizeof(FuriThread*)); +} + +void furi_thread_scrub(void) { + FuriThread* thread_to_scrub = NULL; + while(true) { + furi_check( + furi_message_queue_get( + furi_thread_scrub_message_queue, &thread_to_scrub, FuriWaitForever) == + FuriStatusOk); + + TaskHandle_t task = (TaskHandle_t)thread_to_scrub; + + // Delete task: FreeRTOS will remove task from all lists where it may be + vTaskDelete(task); + // Sanity check: ensure that local storage is ours and clear it + furi_check(pvTaskGetThreadLocalStoragePointer(task, 0) == thread_to_scrub); + vTaskSetThreadLocalStoragePointer(task, 0, NULL); + + // Deliver thread stopped callback + furi_thread_set_state(thread_to_scrub, FuriThreadStateStopped); + } +} + FuriThread* furi_thread_alloc(void) { FuriThread* thread = malloc(sizeof(FuriThread)); @@ -360,16 +390,6 @@ void furi_thread_start(FuriThread* thread) { &thread->container) == (TaskHandle_t)thread); } -void furi_thread_cleanup_tcb_event(TaskHandle_t task) { - FuriThread* thread = pvTaskGetThreadLocalStoragePointer(task, 0); - if(thread) { - // clear thread local storage - vTaskSetThreadLocalStoragePointer(task, 0, NULL); - furi_check(thread == (FuriThread*)task); - furi_thread_set_state(thread, FuriThreadStateStopped); - } -} - bool furi_thread_join(FuriThread* thread) { furi_check(thread); // Cannot join a service thread diff --git a/furi/core/thread.h b/furi/core/thread.h index c320fdbc1..ed7aa4553 100644 --- a/furi/core/thread.h +++ b/furi/core/thread.h @@ -21,10 +21,10 @@ extern "C" { * Many of the FuriThread functions MUST ONLY be called when the thread is STOPPED. */ typedef enum { - FuriThreadStateStopped, /**< Thread is stopped and is safe to release */ - FuriThreadStateStopping, /**< Thread is stopping */ - FuriThreadStateStarting, /**< Thread is starting */ - FuriThreadStateRunning, /**< Thread is running */ + FuriThreadStateStopped, /**< Thread is stopped and is safe to release. Event delivered from system init thread(TCB cleanup routine). It is safe to release thread instance. */ + FuriThreadStateStopping, /**< Thread is stopping. Event delivered from child thread. */ + FuriThreadStateStarting, /**< Thread is starting. Event delivered from parent(self) thread. */ + FuriThreadStateRunning, /**< Thread is running. Event delivered from child thread. */ } FuriThreadState; /** @@ -32,6 +32,7 @@ typedef enum { */ typedef enum { FuriThreadPriorityIdle = 0, /**< Idle priority */ + FuriThreadPriorityInit = 4, /**< Init System Thread Priority */ FuriThreadPriorityLowest = 14, /**< Lowest */ FuriThreadPriorityLow = 15, /**< Low */ FuriThreadPriorityNormal = 16, /**< Normal, system default */ @@ -77,13 +78,15 @@ typedef int32_t (*FuriThreadCallback)(void* context); typedef void (*FuriThreadStdoutWriteCallback)(const char* data, size_t size); /** - * @brief State change callback function pointer type. + * @brief State change callback function pointer type. * - * The function to be used as a state callback MUST follow this signature. + * The function to be used as a state callback MUST follow this + * signature. * - * @param[in] pointer to the FuriThread instance that changed the state - * @param[in] state identifier of the state the thread has transitioned to - * @param[in,out] context pointer to a user-specified object + * @param[in] thread to the FuriThread instance that changed the state + * @param[in] state identifier of the state the thread has transitioned + * to + * @param[in,out] context pointer to a user-specified object */ typedef void (*FuriThreadStateCallback)(FuriThread* thread, FuriThreadState state, void* context); diff --git a/furi/core/thread_i.h b/furi/core/thread_i.h new file mode 100644 index 000000000..c6b12a780 --- /dev/null +++ b/furi/core/thread_i.h @@ -0,0 +1,7 @@ +#pragma once + +#include "thread.h" + +void furi_thread_init(void); + +void furi_thread_scrub(void); diff --git a/furi/core/timer.c b/furi/core/timer.c index 5239a6f7c..db842c4af 100644 --- a/furi/core/timer.c +++ b/furi/core/timer.c @@ -24,7 +24,7 @@ const char* furi_timer_get_current_name(void) { return current_timer_name; } -static void TimerCallback(TimerHandle_t hTimer) { +static void furi_timer_callback(TimerHandle_t hTimer) { FuriTimer* instance = pvTimerGetTimerID(hTimer); furi_check(instance); current_timer_name = pcTimerGetName(hTimer); @@ -32,6 +32,18 @@ static void TimerCallback(TimerHandle_t hTimer) { current_timer_name = NULL; } +static void furi_timer_flush_epilogue(void* context, uint32_t arg) { + furi_assert(context); + UNUSED(arg); + + EventGroupHandle_t hEvent = context; + + // See https://github.com/FreeRTOS/FreeRTOS-Kernel/issues/1142 + vTaskSuspendAll(); + xEventGroupSetBits(hEvent, TIMER_DELETED_EVENT); + (void)xTaskResumeAll(); +} + FuriTimer* furi_timer_alloc(FuriTimerCallback func, FuriTimerType type, void* context) { furi_check((furi_kernel_is_irq_or_masked() == 0U) && (func != NULL)); @@ -45,23 +57,13 @@ FuriTimer* furi_timer_alloc(FuriTimerCallback func, FuriTimerType type, void* co const UBaseType_t reload = (type == FuriTimerTypeOnce ? pdFALSE : pdTRUE); const TimerHandle_t hTimer = xTimerCreateStatic( - name, portMAX_DELAY, reload, instance, TimerCallback, &instance->container); + name, portMAX_DELAY, reload, instance, furi_timer_callback, &instance->container); furi_check(hTimer == (TimerHandle_t)instance); return instance; } -static void furi_timer_epilogue(void* context, uint32_t arg) { - furi_assert(context); - UNUSED(arg); - - EventGroupHandle_t hEvent = context; - vTaskSuspendAll(); - xEventGroupSetBits(hEvent, TIMER_DELETED_EVENT); - (void)xTaskResumeAll(); -} - void furi_timer_free(FuriTimer* instance) { furi_check(!furi_kernel_is_irq_or_masked()); furi_check(instance); @@ -69,16 +71,21 @@ void furi_timer_free(FuriTimer* instance) { TimerHandle_t hTimer = (TimerHandle_t)instance; furi_check(xTimerDelete(hTimer, portMAX_DELAY) == pdPASS); + furi_timer_flush(); + + free(instance); +} + +void furi_timer_flush(void) { StaticEventGroup_t event_container = {}; EventGroupHandle_t hEvent = xEventGroupCreateStatic(&event_container); - furi_check(xTimerPendFunctionCall(furi_timer_epilogue, hEvent, 0, portMAX_DELAY) == pdPASS); + furi_check( + xTimerPendFunctionCall(furi_timer_flush_epilogue, hEvent, 0, portMAX_DELAY) == pdPASS); furi_check( xEventGroupWaitBits(hEvent, TIMER_DELETED_EVENT, pdFALSE, pdTRUE, portMAX_DELAY) == TIMER_DELETED_EVENT); vEventGroupDelete(hEvent); - - free(instance); } FuriStatus furi_timer_start(FuriTimer* instance, uint32_t ticks) { @@ -124,6 +131,8 @@ FuriStatus furi_timer_stop(FuriTimer* instance) { furi_check(xTimerStop(hTimer, portMAX_DELAY) == pdPASS); + furi_timer_flush(); + return FuriStatusOk; } diff --git a/furi/core/timer.h b/furi/core/timer.h index 937856c6f..e72ca1dc8 100644 --- a/furi/core/timer.h +++ b/furi/core/timer.h @@ -35,6 +35,12 @@ FuriTimer* furi_timer_alloc(FuriTimerCallback func, FuriTimerType type, void* co */ void furi_timer_free(FuriTimer* instance); +/** Flush timer task control message queue + * + * Ensures that all commands before this point was processed. + */ +void furi_timer_flush(void); + /** Start timer * * @warning This is asynchronous call, real operation will happen as soon as @@ -61,8 +67,7 @@ FuriStatus furi_timer_restart(FuriTimer* instance, uint32_t ticks); /** Stop timer * - * @warning This is asynchronous call, real operation will happen as soon as - * timer service process this request. + * @warning This is synchronous call that will be blocked till timer queue processed. * * @param instance The pointer to FuriTimer instance * diff --git a/furi/furi.c b/furi/furi.c index f4e64ee09..bc7452f13 100644 --- a/furi/furi.c +++ b/furi/furi.c @@ -1,5 +1,7 @@ #include "furi.h" +#include "core/thread_i.h" + #include #include @@ -7,6 +9,7 @@ void furi_init(void) { furi_check(!furi_kernel_is_irq_or_masked()); furi_check(xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED); + furi_thread_init(); furi_log_init(); furi_record_init(); } @@ -18,3 +21,7 @@ void furi_run(void) { /* Start the kernel scheduler */ vTaskStartScheduler(); } + +void furi_background(void) { + furi_thread_scrub(); +} diff --git a/furi/furi.h b/furi/furi.h index d75debe98..6ddf28577 100644 --- a/furi/furi.h +++ b/furi/furi.h @@ -35,6 +35,8 @@ void furi_init(void); void furi_run(void); +void furi_background(void); + #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/helpers/iso14443_4_layer.c b/lib/nfc/helpers/iso14443_4_layer.c index b3570fd7e..ef9baaabc 100644 --- a/lib/nfc/helpers/iso14443_4_layer.c +++ b/lib/nfc/helpers/iso14443_4_layer.c @@ -33,7 +33,7 @@ #define ISO14443_4_BLOCK_PCB_S_CID_MASK (1U << ISO14443_4_BLOCK_PCB_R_CID_OFFSET) #define ISO14443_4_BLOCK_PCB_S_WTX_DESELECT_MASK (3U << ISO14443_4_BLOCK_PCB_S_WTX_DESELECT_OFFSET) -#define ISO14443_4_BLOCK_PCB_BITS_ACTIVE(pcb, mask) (((pcb) & mask) == mask) +#define ISO14443_4_BLOCK_PCB_BITS_ACTIVE(pcb, mask) (((pcb) & (mask)) == (mask)) #define ISO14443_4_BLOCK_PCB_IS_R_BLOCK(pcb) \ ISO14443_4_BLOCK_PCB_BITS_ACTIVE(pcb, ISO14443_4_BLOCK_PCB_R_MASK) @@ -121,13 +121,6 @@ bool iso14443_4_layer_decode_block( bool ret = false; - // TODO: Fix properly! this is a very big kostyl na velosipede - // (bit_buffer_copy_right are called to copy bigger buffer into smaller buffer causing crash on furi check) issue comes iso14443_4a_poller_send_block at line 109 - // Mimicks furi_check()s in bit_buffer_copy_right(): buf=output_data other=block_data start_index=1 - if(!(bit_buffer_get_size_bytes(block_data) > 1)) return ret; - if(!(bit_buffer_get_capacity_bytes(output_data) >= bit_buffer_get_size_bytes(block_data) - 1)) - return ret; - do { if(ISO14443_4_BLOCK_PCB_IS_R_BLOCK(instance->pcb_prev)) { const uint8_t response_pcb = iso14443_4_layer_get_response_pcb(block_data); 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 9c06663f8..e5321b56c 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -746,6 +746,7 @@ Function,+,canvas_draw_str,void,"Canvas*, int32_t, int32_t, const char*" Function,+,canvas_draw_str_aligned,void,"Canvas*, int32_t, int32_t, Align, Align, const char*" Function,+,canvas_draw_triangle,void,"Canvas*, int32_t, int32_t, size_t, size_t, CanvasDirection" Function,+,canvas_draw_xbm,void,"Canvas*, int32_t, int32_t, size_t, size_t, const uint8_t*" +Function,+,canvas_draw_xbm_ex,void,"Canvas*, int32_t, int32_t, size_t, size_t, IconRotation, const uint8_t*" Function,+,canvas_get_font_params,const CanvasFontParameters*,"const Canvas*, Font" Function,+,canvas_glyph_width,size_t,"Canvas*, uint16_t" Function,+,canvas_height,size_t,const Canvas* @@ -1102,6 +1103,7 @@ Function,-,ftello,off_t,FILE* Function,-,ftrylockfile,int,FILE* Function,-,funlockfile,void,FILE* Function,-,funopen,FILE*,"const void*, int (*)(void*, char*, int), int (*)(void*, const char*, int), fpos_t (*)(void*, fpos_t, int), int (*)(void*)" +Function,-,furi_background,void, Function,+,furi_delay_ms,void,uint32_t Function,+,furi_delay_tick,void,uint32_t Function,+,furi_delay_until_tick,FuriStatus,uint32_t @@ -1114,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* @@ -1370,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* @@ -1672,6 +1677,7 @@ Function,+,furi_thread_stdout_write,size_t,"const char*, size_t" Function,+,furi_thread_suspend,void,FuriThreadId Function,+,furi_thread_yield,void, Function,+,furi_timer_alloc,FuriTimer*,"FuriTimerCallback, FuriTimerType, void*" +Function,+,furi_timer_flush,void, Function,+,furi_timer_free,void,FuriTimer* Function,+,furi_timer_get_expire_time,uint32_t,FuriTimer* Function,+,furi_timer_is_running,uint32_t,FuriTimer* @@ -2684,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 @@ -2758,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 d1dcb48d3..d334415c7 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -842,7 +842,7 @@ Function,+,canvas_draw_str,void,"Canvas*, int32_t, int32_t, const char*" Function,+,canvas_draw_str_aligned,void,"Canvas*, int32_t, int32_t, Align, Align, const char*" Function,+,canvas_draw_triangle,void,"Canvas*, int32_t, int32_t, size_t, size_t, CanvasDirection" Function,+,canvas_draw_xbm,void,"Canvas*, int32_t, int32_t, size_t, size_t, const uint8_t*" -Function,+,canvas_draw_xbm_custom,void,"Canvas*, int32_t, int32_t, size_t, size_t, IconRotation, const uint8_t*" +Function,+,canvas_draw_xbm_ex,void,"Canvas*, int32_t, int32_t, size_t, size_t, IconRotation, const uint8_t*" Function,+,canvas_get_font_params,const CanvasFontParameters*,"const Canvas*, Font" Function,+,canvas_glyph_width,size_t,"Canvas*, uint16_t" Function,+,canvas_height,size_t,const Canvas* @@ -1263,6 +1263,7 @@ Function,-,ftello,off_t,FILE* Function,-,ftrylockfile,int,FILE* Function,-,funlockfile,void,FILE* Function,-,funopen,FILE*,"const void*, int (*)(void*, char*, int), int (*)(void*, const char*, int), fpos_t (*)(void*, fpos_t, int), int (*)(void*)" +Function,-,furi_background,void, Function,+,furi_delay_ms,void,uint32_t Function,+,furi_delay_tick,void,uint32_t Function,+,furi_delay_until_tick,FuriStatus,uint32_t @@ -1275,6 +1276,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* @@ -1597,6 +1599,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, @@ -1958,6 +1962,7 @@ Function,+,furi_thread_stdout_write,size_t,"const char*, size_t" Function,+,furi_thread_suspend,void,FuriThreadId Function,+,furi_thread_yield,void, Function,+,furi_timer_alloc,FuriTimer*,"FuriTimerCallback, FuriTimerType, void*" +Function,+,furi_timer_flush,void, Function,+,furi_timer_free,void,FuriTimer* Function,-,furi_timer_get_current_name,const char*, Function,+,furi_timer_get_expire_time,uint32_t,FuriTimer* @@ -3753,6 +3758,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/ble_glue/ble_glue.c b/targets/f7/ble_glue/ble_glue.c index 73bb41bad..fe101b2c9 100644 --- a/targets/f7/ble_glue/ble_glue.c +++ b/targets/f7/ble_glue/ble_glue.c @@ -87,6 +87,8 @@ void ble_glue_init(void) { TL_Init(); ble_glue->shci_mtx = furi_mutex_alloc(FuriMutexTypeNormal); + // Take mutex, SHCI will release it in most unusual way later + furi_check(furi_mutex_acquire(ble_glue->shci_mtx, FuriWaitForever) == FuriStatusOk); // FreeRTOS system task creation ble_event_thread_start(); @@ -248,7 +250,9 @@ void ble_glue_stop(void) { ble_event_thread_stop(); // Free resources furi_mutex_free(ble_glue->shci_mtx); + ble_glue->shci_mtx = NULL; furi_timer_free(ble_glue->hardfault_check_timer); + ble_glue->hardfault_check_timer = NULL; ble_glue_clear_shared_memory(); free(ble_glue); @@ -309,10 +313,13 @@ BleGlueCommandResult ble_glue_force_c2_mode(BleGlueC2Mode desired_mode) { static void ble_sys_status_not_callback(SHCI_TL_CmdStatus_t status) { switch(status) { case SHCI_TL_CmdBusy: - furi_mutex_acquire(ble_glue->shci_mtx, FuriWaitForever); + furi_check( + furi_mutex_acquire( + ble_glue->shci_mtx, furi_kernel_is_running() ? FuriWaitForever : 0) == + FuriStatusOk); break; case SHCI_TL_CmdAvailable: - furi_mutex_release(ble_glue->shci_mtx); + furi_check(furi_mutex_release(ble_glue->shci_mtx) == FuriStatusOk); break; default: break; diff --git a/targets/f7/ble_glue/gap.c b/targets/f7/ble_glue/gap.c index 5b213fec9..e03935ce0 100644 --- a/targets/f7/ble_glue/gap.c +++ b/targets/f7/ble_glue/gap.c @@ -144,7 +144,7 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { event_pckt = (hci_event_pckt*)((hci_uart_pckt*)pckt)->data; furi_check(gap); - furi_mutex_acquire(gap->state_mutex, FuriWaitForever); + furi_check(furi_mutex_acquire(gap->state_mutex, FuriWaitForever) == FuriStatusOk); switch(event_pckt->evt) { case HCI_DISCONNECTION_COMPLETE_EVT_CODE: { @@ -328,7 +328,7 @@ BleEventFlowStatus ble_event_app_notification(void* pckt) { break; } - furi_mutex_release(gap->state_mutex); + furi_check(furi_mutex_release(gap->state_mutex) == FuriStatusOk); return BleEventFlowEnable; } @@ -514,7 +514,7 @@ static void gap_advertise_stop(void) { } void gap_start_advertising(void) { - furi_mutex_acquire(gap->state_mutex, FuriWaitForever); + furi_check(furi_mutex_acquire(gap->state_mutex, FuriWaitForever) == FuriStatusOk); if(gap->state == GapStateIdle) { gap->state = GapStateStartingAdv; FURI_LOG_I(TAG, "Start advertising"); @@ -522,18 +522,18 @@ void gap_start_advertising(void) { GapCommand command = GapCommandAdvFast; furi_check(furi_message_queue_put(gap->command_queue, &command, 0) == FuriStatusOk); } - furi_mutex_release(gap->state_mutex); + furi_check(furi_mutex_release(gap->state_mutex) == FuriStatusOk); } void gap_stop_advertising(void) { - furi_mutex_acquire(gap->state_mutex, FuriWaitForever); + furi_check(furi_mutex_acquire(gap->state_mutex, FuriWaitForever) == FuriStatusOk); if(gap->state > GapStateIdle) { FURI_LOG_I(TAG, "Stop advertising"); gap->enable_adv = false; GapCommand command = GapCommandAdvStop; furi_check(furi_message_queue_put(gap->command_queue, &command, 0) == FuriStatusOk); } - furi_mutex_release(gap->state_mutex); + furi_check(furi_mutex_release(gap->state_mutex) == FuriStatusOk); } static void gap_advertise_timer_callback(void* context) { @@ -604,9 +604,9 @@ uint32_t gap_get_remote_conn_rssi(int8_t* rssi) { GapState gap_get_state(void) { GapState state; if(gap) { - furi_mutex_acquire(gap->state_mutex, FuriWaitForever); + furi_check(furi_mutex_acquire(gap->state_mutex, FuriWaitForever) == FuriStatusOk); state = gap->state; - furi_mutex_release(gap->state_mutex); + furi_check(furi_mutex_release(gap->state_mutex) == FuriStatusOk); } else { state = GapStateUninitialized; } @@ -615,17 +615,21 @@ GapState gap_get_state(void) { void gap_thread_stop(void) { if(gap) { - furi_mutex_acquire(gap->state_mutex, FuriWaitForever); + furi_check(furi_mutex_acquire(gap->state_mutex, FuriWaitForever) == FuriStatusOk); gap->enable_adv = false; GapCommand command = GapCommandKillThread; furi_message_queue_put(gap->command_queue, &command, FuriWaitForever); - furi_mutex_release(gap->state_mutex); + furi_check(furi_mutex_release(gap->state_mutex) == FuriStatusOk); furi_thread_join(gap->thread); furi_thread_free(gap->thread); + gap->thread = NULL; // Free resources furi_mutex_free(gap->state_mutex); + gap->state_mutex = NULL; furi_message_queue_free(gap->command_queue); + gap->command_queue = NULL; furi_timer_free(gap->advertise_timer); + gap->advertise_timer = NULL; ble_event_dispatcher_reset(); free(gap); @@ -642,7 +646,7 @@ static int32_t gap_app(void* context) { FURI_LOG_E(TAG, "Message queue get error: %d", status); continue; } - furi_mutex_acquire(gap->state_mutex, FuriWaitForever); + furi_check(furi_mutex_acquire(gap->state_mutex, FuriWaitForever) == FuriStatusOk); if(command == GapCommandKillThread) { break; } @@ -653,7 +657,7 @@ static int32_t gap_app(void* context) { } else if(command == GapCommandAdvStop) { gap_advertise_stop(); } - furi_mutex_release(gap->state_mutex); + furi_check(furi_mutex_release(gap->state_mutex) == FuriStatusOk); } return 0; 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/furi_hal/furi_hal_spi.c b/targets/f7/furi_hal/furi_hal_spi.c index 49bcd48a1..2a7cb7c25 100644 --- a/targets/f7/furi_hal/furi_hal_spi.c +++ b/targets/f7/furi_hal/furi_hal_spi.c @@ -202,7 +202,7 @@ bool furi_hal_spi_bus_trx_dma( furi_check(size > 0); // If scheduler is not running, use blocking mode - if(furi_kernel_is_running()) { + if(!furi_kernel_is_running()) { return furi_hal_spi_bus_trx(handle, tx_buffer, rx_buffer, size, timeout_ms); } diff --git a/targets/f7/inc/FreeRTOSConfig.h b/targets/f7/inc/FreeRTOSConfig.h index 82cda2c6d..357019ea2 100644 --- a/targets/f7/inc/FreeRTOSConfig.h +++ b/targets/f7/inc/FreeRTOSConfig.h @@ -84,6 +84,7 @@ to exclude the API function. */ #define INCLUDE_xTaskGetCurrentTaskHandle 1 #define INCLUDE_xTaskGetSchedulerState 1 #define INCLUDE_xTimerPendFunctionCall 1 +#define INCLUDE_xTaskGetIdleTaskHandle 1 /* Workaround for various notification issues: * - First one used by system primitives @@ -129,25 +130,11 @@ See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */ #define configMAX_SYSCALL_INTERRUPT_PRIORITY \ (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS)) -/* Normal assert() semantics without relying on the provision of an assert.h -header file. */ -#ifdef DEBUG -#include -#define configASSERT(x) \ - if((x) == 0) { \ - furi_crash("FreeRTOS Assert"); \ - } -#endif - /* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS standard names. */ #define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler -#define USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 1 -#define configOVERRIDE_DEFAULT_TICK_CONFIGURATION \ - 1 /* required only for Keil but does not hurt otherwise */ - #define traceTASK_SWITCHED_IN() \ extern void furi_hal_mpu_set_stack_protection(uint32_t* stack); \ furi_hal_mpu_set_stack_protection((uint32_t*)pxCurrentTCB->pxStack); \ @@ -157,6 +144,14 @@ standard names. */ // referencing `FreeRTOS_errno' here vvvvv because FreeRTOS calls our hook _before_ copying the value into the TCB, hence a manual write to the TCB would get overwritten #define traceTASK_SWITCHED_OUT() FreeRTOS_errno = errno -#define portCLEAN_UP_TCB(pxTCB) \ - extern void furi_thread_cleanup_tcb_event(TaskHandle_t task); \ - furi_thread_cleanup_tcb_event(pxTCB) +/* Normal assert() semantics without relying on the provision of an assert.h +header file. */ +#ifdef DEBUG +#define configASSERT(x) \ + if((x) == 0) { \ + furi_crash("FreeRTOS Assert"); \ + } +#endif + +// Must be last line of config because of recursion +#include diff --git a/targets/f7/src/main.c b/targets/f7/src/main.c index 015c44e98..1b810b9eb 100644 --- a/targets/f7/src/main.c +++ b/targets/f7/src/main.c @@ -15,6 +15,8 @@ int32_t init_task(void* context) { // Init flipper flipper_init(); + furi_background(); + return 0; } @@ -26,7 +28,8 @@ int main(void) { furi_hal_init_early(); furi_hal_set_is_normal_boot(false); - FuriThread* main_thread = furi_thread_alloc_ex("Init", 4096, init_task, NULL); + FuriThread* main_thread = furi_thread_alloc_ex("InitSrv", 1024, init_task, NULL); + furi_thread_set_priority(main_thread, FuriThreadPriorityInit); #ifdef FURI_RAM_EXEC // Prevent entering sleep mode when executed from RAM 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