diff --git a/applications/debug/unit_tests/application.fam b/applications/debug/unit_tests/application.fam index c87305847..dec3283e4 100644 --- a/applications/debug/unit_tests/application.fam +++ b/applications/debug/unit_tests/application.fam @@ -221,6 +221,14 @@ App( requires=["unit_tests"], ) +App( + appid="test_js", + sources=["tests/common/*.c", "tests/js/*.c"], + apptype=FlipperAppType.PLUGIN, + entry_point="get_api", + requires=["unit_tests", "js_app"], +) + App( appid="test_strint", sources=["tests/common/*.c", "tests/strint/*.c"], diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js new file mode 100644 index 000000000..0927595a2 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -0,0 +1,4 @@ +let tests = require("tests"); + +tests.assert_eq(1337, 1337); +tests.assert_eq("hello", "hello"); diff --git a/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js b/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js new file mode 100644 index 000000000..0437b8293 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/event_loop.js @@ -0,0 +1,30 @@ +let tests = require("tests"); +let event_loop = require("event_loop"); + +let ext = { + i: 0, + received: false, +}; + +let queue = event_loop.queue(16); + +event_loop.subscribe(queue.input, function (_, item, tests, ext) { + tests.assert_eq(123, item); + ext.received = true; +}, tests, ext); + +event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) { + ext.i++; + queue.send(123); + if (counter === 10) + event_loop.stop(); + return [queue, counter + 1, ext]; +}, queue, 1, ext); + +event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) { + tests.fail("event loop was not stopped"); +}, tests); + +event_loop.run(); +tests.assert_eq(10, ext.i); +tests.assert_eq(true, ext.received); diff --git a/applications/debug/unit_tests/resources/unit_tests/js/math.js b/applications/debug/unit_tests/resources/unit_tests/js/math.js new file mode 100644 index 000000000..ea8d80f91 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/math.js @@ -0,0 +1,34 @@ +let tests = require("tests"); +let math = require("math"); + +// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16 + +// basics +tests.assert_float_close(5, math.abs(-5), math.EPSILON); +tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON); +tests.assert_float_close(5, math.abs(5), math.EPSILON); +tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON); +tests.assert_float_close(3, math.cbrt(27), math.EPSILON); +tests.assert_float_close(6, math.ceil(5.3), math.EPSILON); +tests.assert_float_close(31, math.clz32(1), math.EPSILON); +tests.assert_float_close(5, math.floor(5.7), math.EPSILON); +tests.assert_float_close(5, math.max(3, 5), math.EPSILON); +tests.assert_float_close(3, math.min(3, 5), math.EPSILON); +tests.assert_float_close(-1, math.sign(-5), math.EPSILON); +tests.assert_float_close(5, math.trunc(5.7), math.EPSILON); + +// trig +tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON); +tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON); +tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON); +tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON); +tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON); +tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON); +tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON); +tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15 +tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16 + +// powers +tests.assert_float_close(5, math.sqrt(25), math.EPSILON); +tests.assert_float_close(8, math.pow(2, 3), math.EPSILON); +tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16 diff --git a/applications/debug/unit_tests/resources/unit_tests/js/storage.js b/applications/debug/unit_tests/resources/unit_tests/js/storage.js new file mode 100644 index 000000000..872b29cfb --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/js/storage.js @@ -0,0 +1,136 @@ +let storage = require("storage"); +let tests = require("tests"); + +let baseDir = "/ext/.tmp/unit_tests"; + +tests.assert_eq(true, storage.rmrf(baseDir)); +tests.assert_eq(true, storage.makeDirectory(baseDir)); + +// write +let file = storage.openFile(baseDir + "/helloworld", "w", "create_always"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.write("Hello, World!")); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// read +file = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.size()); +tests.assert_eq("Hello, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// seek +file = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.isOpen()); +tests.assert_eq(13, file.size()); +tests.assert_eq("Hello, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.seekAbsolute(1)); +tests.assert_eq(true, file.seekRelative(2)); +tests.assert_eq(3, file.tell()); +tests.assert_eq(false, file.eof()); +tests.assert_eq("lo, World!", file.read("ascii", 128)); +tests.assert_eq(true, file.eof()); +tests.assert_eq(true, file.close()); +tests.assert_eq(false, file.isOpen()); + +// byte-level copy +let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing"); +let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always"); +tests.assert_eq(true, !!src); +tests.assert_eq(true, src.isOpen()); +tests.assert_eq(true, !!dst); +tests.assert_eq(true, dst.isOpen()); +tests.assert_eq(true, src.copyTo(dst, 10)); +tests.assert_eq(true, dst.seekAbsolute(0)); +tests.assert_eq("Hello, Wor", dst.read("ascii", 128)); +tests.assert_eq(true, src.copyTo(dst, 3)); +tests.assert_eq(true, dst.seekAbsolute(0)); +tests.assert_eq("Hello, World!", dst.read("ascii", 128)); +tests.assert_eq(true, src.eof()); +tests.assert_eq(true, src.close()); +tests.assert_eq(false, src.isOpen()); +tests.assert_eq(true, dst.eof()); +tests.assert_eq(true, dst.close()); +tests.assert_eq(false, dst.isOpen()); + +// truncate +tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2")); +file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq(true, file.seekAbsolute(5)); +tests.assert_eq(true, file.truncate()); +tests.assert_eq(true, file.close()); +file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing"); +tests.assert_eq(true, !!file); +tests.assert_eq("Hello", file.read("ascii", 128)); +tests.assert_eq(true, file.close()); + +// existence +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2")); +tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123")); +tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir)); +tests.assert_eq(true, storage.directoryExists(baseDir)); +tests.assert_eq(true, storage.fileOrDirExists(baseDir)); +tests.assert_eq(true, storage.remove(baseDir + "/helloworld2")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2")); + +// stat +let stat = storage.stat(baseDir + "/helloworld"); +tests.assert_eq(true, !!stat); +tests.assert_eq(baseDir + "/helloworld", stat.path); +tests.assert_eq(false, stat.isDirectory); +tests.assert_eq(13, stat.size); + +// rename +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); + +// copy +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123")); +tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld")); +tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123")); + +// next avail +tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20)); + +// fs info +let fsInfo = storage.fsInfo("/ext"); +tests.assert_eq(true, !!fsInfo); +tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/ +fsInfo = storage.fsInfo("/int"); +tests.assert_eq(true, !!fsInfo); +tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); + +// path operations +tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test")); +tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt")); +tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub")); +tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test")); + +// dir +let entries = storage.readDirectory(baseDir); +tests.assert_eq(true, !!entries); +// FIXME: (-nofl) this test suite assumes that files are listed by +// `readDirectory` in the exact order that they were created, which is not +// something that is actually guaranteed. +// Possible solution: sort and compare the array. +tests.assert_eq("helloworld", entries[0].path); +tests.assert_eq("helloworld123", entries[1].path); + +tests.assert_eq(true, storage.rmrf(baseDir)); +tests.assert_eq(true, storage.makeDirectory(baseDir)); diff --git a/applications/debug/unit_tests/tests/js/js_test.c b/applications/debug/unit_tests/tests/js/js_test.c new file mode 100644 index 000000000..af590e899 --- /dev/null +++ b/applications/debug/unit_tests/tests/js/js_test.c @@ -0,0 +1,88 @@ +#include "../test.h" // IWYU pragma: keep + +#include +#include +#include + +#include +#include + +#include + +#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js") + +typedef enum { + JsTestsFinished = 1, + JsTestsError = 2, +} JsTestFlag; + +typedef struct { + FuriEventFlag* event_flags; + FuriString* error_string; +} JsTestCallbackContext; + +static void js_test_callback(JsThreadEvent event, const char* msg, void* param) { + JsTestCallbackContext* context = param; + if(event == JsThreadEventPrint) { + FURI_LOG_I("js_test", "%s", msg); + } else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) { + context->error_string = furi_string_alloc_set_str(msg); + furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError); + } else if(event == JsThreadEventDone) { + furi_event_flag_set(context->event_flags, JsTestsFinished); + } +} + +static void js_test_run(const char* script_path) { + JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext)); + context->event_flags = furi_event_flag_alloc(); + + JsThread* thread = js_thread_run(script_path, js_test_callback, context); + uint32_t flags = furi_event_flag_wait( + context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever); + if(flags & FuriFlagError) { + // getting the flags themselves should not fail + furi_crash(); + } + + FuriString* error_string = context->error_string; + + js_thread_stop(thread); + furi_event_flag_free(context->event_flags); + free(context); + + if(flags & JsTestsError) { + // memory leak: not freeing the FuriString if the tests fail, + // because mu_fail executes a return + // + // who cares tho? + mu_fail(furi_string_get_cstr(error_string)); + } +} + +MU_TEST(js_test_basic) { + js_test_run(JS_SCRIPT_PATH("basic")); +} +MU_TEST(js_test_math) { + js_test_run(JS_SCRIPT_PATH("math")); +} +MU_TEST(js_test_event_loop) { + js_test_run(JS_SCRIPT_PATH("event_loop")); +} +MU_TEST(js_test_storage) { + js_test_run(JS_SCRIPT_PATH("storage")); +} + +MU_TEST_SUITE(test_js) { + MU_RUN_TEST(js_test_basic); + MU_RUN_TEST(js_test_math); + MU_RUN_TEST(js_test_event_loop); + MU_RUN_TEST(js_test_storage); +} + +int run_minunit_test_js(void) { + MU_RUN_SUITE(test_js); + return MU_EXIT_CODE; +} + +TEST_API_DEFINE(run_minunit_test_js) diff --git a/applications/debug/unit_tests/tests/minunit.h b/applications/debug/unit_tests/tests/minunit.h index 9310cfc9c..9ca3bb403 100644 --- a/applications/debug/unit_tests/tests/minunit.h +++ b/applications/debug/unit_tests/tests/minunit.h @@ -31,7 +31,7 @@ extern "C" { #include #if defined(_MSC_VER) && _MSC_VER < 1900 #define snprintf _snprintf -#define __func__ __FUNCTION__ +#define __func__ __FUNCTION__ //-V1059 #endif #elif defined(__unix__) || defined(__unix) || defined(unix) || \ @@ -56,7 +56,7 @@ extern "C" { #endif #if __GNUC__ >= 5 && !defined(__STDC_VERSION__) -#define __func__ __extension__ __FUNCTION__ +#define __func__ __extension__ __FUNCTION__ //-V1059 #endif #else @@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...); MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;) /* Test runner */ +//-V:MU_RUN_TEST:550 #define MU_RUN_TEST(test) \ MU__SAFE_BLOCK( \ if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \ diff --git a/applications/debug/unit_tests/unit_test_api_table_i.h b/applications/debug/unit_tests/unit_test_api_table_i.h index 50524e5b7..10b089022 100644 --- a/applications/debug/unit_tests/unit_test_api_table_i.h +++ b/applications/debug/unit_tests/unit_test_api_table_i.h @@ -7,7 +7,7 @@ #include #include -#include +#include static constexpr auto unit_tests_api_table = sort(create_array_t( API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)), @@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t( xQueueGenericSend, BaseType_t, (QueueHandle_t, const void* const, TickType_t, const BaseType_t)), - API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)), - API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)), API_METHOD( - furi_event_loop_subscribe_message_queue, - void, - (FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)), - API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)), - API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)), - API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)), + js_thread_run, + JsThread*, + (const char* script_path, JsThreadCallback callback, void* context)), + API_METHOD(js_thread_stop, void, (JsThread * worker)), API_VARIABLE(PB_Main_msg, PB_Main_msg_t))); diff --git a/applications/main/nfc/application.fam b/applications/main/nfc/application.fam index ae5372910..58660253f 100644 --- a/applications/main/nfc/application.fam +++ b/applications/main/nfc/application.fam @@ -272,6 +272,15 @@ App( sources=["plugins/supported_cards/skylanders.c"], ) +App( + appid="hworld_parser", + apptype=FlipperAppType.PLUGIN, + entry_point="hworld_plugin_ep", + targets=["f7"], + requires=["nfc"], + sources=["plugins/supported_cards/hworld.c"], +) + App( appid="nfc_cli", targets=["f7"], diff --git a/applications/main/nfc/plugins/supported_cards/hworld.c b/applications/main/nfc/plugins/supported_cards/hworld.c new file mode 100644 index 000000000..674e7b955 --- /dev/null +++ b/applications/main/nfc/plugins/supported_cards/hworld.c @@ -0,0 +1,243 @@ +// Flipper Zero parser for H World Hotel Key Cards +// H World operates around 10,000 hotels, most of which in mainland China +// Reverse engineering and parser written by @Torron (Github: @zinongli) +#include "nfc_supported_card_plugin.h" +#include +#include +#include +#include +#include +#include + +#define TAG "H World" +#define ROOM_SECTOR 1 +#define VIP_SECTOR 5 +#define ROOM_SECTOR_KEY_BLOCK 7 +#define VIP_SECTOR_KEY_BLOCK 23 +#define ACCESS_INFO_BLOCK 5 +#define ROOM_NUM_DECIMAL_BLOCK 6 +#define H_WORLD_YEAR_OFFSET 2000 + +typedef struct { + uint64_t a; + uint64_t b; +} MfClassicKeyPair; + +static MfClassicKeyPair hworld_standard_keys[] = { + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 000 + {.a = 0x543071543071, .b = 0x5F01015F0101}, // 001 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 005 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 006 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 007 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 008 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 009 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 010 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 011 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 012 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015 +}; + +static MfClassicKeyPair hworld_vip_keys[] = { + {.a = 0x000000000000, .b = 0xFFFFFFFFFFFF}, // 000 + {.a = 0x543071543071, .b = 0x5F01015F0101}, // 001 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 005 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 006 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 007 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 008 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 009 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 010 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 011 + {.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 012 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014 + {.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015 +}; + +static bool hworld_verify(Nfc* nfc) { + bool verified = false; + + do { + const uint8_t block_num = mf_classic_get_first_block_num_of_sector(ROOM_SECTOR); + + MfClassicKey standard_key = {0}; + bit_lib_num_to_bytes_be( + hworld_standard_keys[ROOM_SECTOR].a, COUNT_OF(standard_key.data), standard_key.data); + + MfClassicAuthContext auth_context; + MfClassicError standard_error = mf_classic_poller_sync_auth( + nfc, block_num, &standard_key, MfClassicKeyTypeA, &auth_context); + + if(standard_error != MfClassicErrorNone) { + FURI_LOG_D(TAG, "Failed static key check for block %u", block_num); + break; + } + + MfClassicKey vip_key = {0}; + bit_lib_num_to_bytes_be( + hworld_vip_keys[VIP_SECTOR].b, COUNT_OF(vip_key.data), vip_key.data); + + MfClassicError vip_error = mf_classic_poller_sync_auth( + nfc, block_num, &vip_key, MfClassicKeyTypeB, &auth_context); + + if(vip_error == MfClassicErrorNone) { + FURI_LOG_D(TAG, "VIP card detected"); + } else { + FURI_LOG_D(TAG, "Standard card detected"); + } + + verified = true; + } while(false); + + return verified; +} + +static bool hworld_read(Nfc* nfc, NfcDevice* device) { + furi_assert(nfc); + furi_assert(device); + + bool is_read = false; + + MfClassicData* data = mf_classic_alloc(); + nfc_device_copy_data(device, NfcProtocolMfClassic, data); + + do { + MfClassicType type = MfClassicType1k; + MfClassicError standard_error = mf_classic_poller_sync_detect_type(nfc, &type); + MfClassicError vip_error = MfClassicErrorNotPresent; + if(standard_error != MfClassicErrorNone) break; + data->type = type; + + MfClassicDeviceKeys standard_keys = {}; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be( + hworld_standard_keys[i].a, sizeof(MfClassicKey), standard_keys.key_a[i].data); + FURI_BIT_SET(standard_keys.key_a_mask, i); + bit_lib_num_to_bytes_be( + hworld_standard_keys[i].b, sizeof(MfClassicKey), standard_keys.key_b[i].data); + FURI_BIT_SET(standard_keys.key_b_mask, i); + } + + standard_error = mf_classic_poller_sync_read(nfc, &standard_keys, data); + if(standard_error == MfClassicErrorNone) { + FURI_LOG_I(TAG, "Standard card successfully read"); + } else { + MfClassicDeviceKeys vip_keys = {}; + for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) { + bit_lib_num_to_bytes_be( + hworld_vip_keys[i].a, sizeof(MfClassicKey), vip_keys.key_a[i].data); + FURI_BIT_SET(vip_keys.key_a_mask, i); + bit_lib_num_to_bytes_be( + hworld_vip_keys[i].b, sizeof(MfClassicKey), vip_keys.key_b[i].data); + FURI_BIT_SET(vip_keys.key_b_mask, i); + } + + vip_error = mf_classic_poller_sync_read(nfc, &vip_keys, data); + + if(vip_error == MfClassicErrorNone) { + FURI_LOG_I(TAG, "VIP card successfully read"); + } else { + break; + } + } + + nfc_device_set_data(device, NfcProtocolMfClassic, data); + + is_read = (standard_error == MfClassicErrorNone) | (vip_error == MfClassicErrorNone); + } while(false); + + mf_classic_free(data); + + return is_read; +} + +bool hworld_parse(const NfcDevice* device, FuriString* parsed_data) { + furi_assert(device); + const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic); + bool parsed = false; + + do { + // Check card type + if(data->type != MfClassicType1k) break; + + // Check static key for verificaiton + const uint8_t* data_room_sec_key_a_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[0]; + const uint8_t* data_room_sec_key_b_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[10]; + uint64_t data_room_sec_key_a = bit_lib_get_bits_64(data_room_sec_key_a_ptr, 0, 48); + uint64_t data_room_sec_key_b = bit_lib_get_bits_64(data_room_sec_key_b_ptr, 0, 48); + if((data_room_sec_key_a != hworld_standard_keys[ROOM_SECTOR].a) | + (data_room_sec_key_b != hworld_standard_keys[ROOM_SECTOR].b)) + break; + + // Check whether this card is VIP + const uint8_t* data_vip_sec_key_b_ptr = &data->block[VIP_SECTOR_KEY_BLOCK].data[10]; + uint64_t data_vip_sec_key_b = bit_lib_get_bits_64(data_vip_sec_key_b_ptr, 0, 48); + bool is_hworld_vip = (data_vip_sec_key_b == hworld_vip_keys[VIP_SECTOR].b); + uint8_t room_floor = data->block[ACCESS_INFO_BLOCK].data[13]; + uint8_t room_num = data->block[ACCESS_INFO_BLOCK].data[14]; + + // Check in date & time + uint16_t check_in_year = data->block[ACCESS_INFO_BLOCK].data[2] + H_WORLD_YEAR_OFFSET; + uint8_t check_in_month = data->block[ACCESS_INFO_BLOCK].data[3]; + uint8_t check_in_day = data->block[ACCESS_INFO_BLOCK].data[4]; + uint8_t check_in_hour = data->block[ACCESS_INFO_BLOCK].data[5]; + uint8_t check_in_minute = data->block[ACCESS_INFO_BLOCK].data[6]; + + // Expire date & time + uint16_t expire_year = data->block[ACCESS_INFO_BLOCK].data[7] + H_WORLD_YEAR_OFFSET; + uint8_t expire_month = data->block[ACCESS_INFO_BLOCK].data[8]; + uint8_t expire_day = data->block[ACCESS_INFO_BLOCK].data[9]; + uint8_t expire_hour = data->block[ACCESS_INFO_BLOCK].data[10]; + uint8_t expire_minute = data->block[ACCESS_INFO_BLOCK].data[11]; + + furi_string_cat_printf(parsed_data, "\e#H World Card\n"); + furi_string_cat_printf( + parsed_data, "%s\n", is_hworld_vip ? "VIP card" : "Standard room key"); + furi_string_cat_printf(parsed_data, "Room Num: %u%02u\n", room_floor, room_num); + furi_string_cat_printf( + parsed_data, + "Check-in Date: \n%04u-%02d-%02d\n%02d:%02d:00\n", + check_in_year, + check_in_month, + check_in_day, + check_in_hour, + check_in_minute); + furi_string_cat_printf( + parsed_data, + "Expiration Date: \n%04u-%02d-%02d\n%02d:%02d:00", + expire_year, + expire_month, + expire_day, + expire_hour, + expire_minute); + parsed = true; + } while(false); + return parsed; +} + +/* Actual implementation of app<>plugin interface */ +static const NfcSupportedCardsPlugin hworld_plugin = { + .protocol = NfcProtocolMfClassic, + .verify = hworld_verify, + .read = hworld_read, + .parse = hworld_parse, +}; + +/* Plugin descriptor to comply with basic plugin specification */ +static const FlipperAppPluginDescriptor hworld_plugin_descriptor = { + .appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID, + .ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION, + .entry_point = &hworld_plugin, +}; + +/* Plugin entry point - must return a pointer to const descriptor */ +const FlipperAppPluginDescriptor* hworld_plugin_ep(void) { + return &hworld_plugin_descriptor; +} diff --git a/applications/services/gui/canvas.h b/applications/services/gui/canvas.h index 86f7e9808..efd314687 100644 --- a/applications/services/gui/canvas.h +++ b/applications/services/gui/canvas.h @@ -298,15 +298,14 @@ void canvas_draw_xbm( /** Draw rotated XBM bitmap * - * @param canvas Canvas instance - * @param x x coordinate - * @param y y coordinate - * @param[in] width bitmap width - * @param[in] height bitmap height - * @param[in] rotation bitmap rotation - * @param bitmap pointer to XBM bitmap data + * @param canvas Canvas instance + * @param x x coordinate + * @param y y coordinate + * @param[in] width bitmap width + * @param[in] height bitmap height + * @param[in] rotation bitmap rotation + * @param bitmap_data pointer to XBM bitmap data */ - void canvas_draw_xbm_ex( Canvas* canvas, int32_t x, diff --git a/applications/services/gui/modules/text_input.h b/applications/services/gui/modules/text_input.h index 6733c01cc..1c020d186 100644 --- a/applications/services/gui/modules/text_input.h +++ b/applications/services/gui/modules/text_input.h @@ -65,6 +65,13 @@ void text_input_set_result_callback( size_t text_buffer_size, bool clear_default_text); +/** + * @brief Sets the minimum length of a TextInput + * @param [in] text_input TextInput + * @param [in] minimum_length Minimum input length + */ +void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length); + void text_input_set_validator( TextInput* text_input, TextInputValidatorCallback callback, diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 63878fc19..6db4d8241 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -5,6 +5,12 @@ #define VIEW_DISPATCHER_QUEUE_LEN (16U) ViewDispatcher* view_dispatcher_alloc(void) { + ViewDispatcher* dispatcher = view_dispatcher_alloc_ex(furi_event_loop_alloc()); + dispatcher->is_event_loop_owned = true; + return dispatcher; +} + +ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop) { ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher)); view_dispatcher->view_port = view_port_alloc(); @@ -16,7 +22,7 @@ ViewDispatcher* view_dispatcher_alloc(void) { ViewDict_init(view_dispatcher->views); - view_dispatcher->event_loop = furi_event_loop_alloc(); + view_dispatcher->event_loop = loop; view_dispatcher->input_queue = furi_message_queue_alloc(VIEW_DISPATCHER_QUEUE_LEN, sizeof(InputEvent)); @@ -57,7 +63,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) { furi_message_queue_free(view_dispatcher->input_queue); furi_message_queue_free(view_dispatcher->event_queue); - furi_event_loop_free(view_dispatcher->event_loop); + if(view_dispatcher->is_event_loop_owned) furi_event_loop_free(view_dispatcher->event_loop); // Free dispatcher free(view_dispatcher); } @@ -85,6 +91,7 @@ void view_dispatcher_set_tick_event_callback( ViewDispatcherTickEventCallback callback, uint32_t tick_period) { furi_check(view_dispatcher); + furi_check(view_dispatcher->is_event_loop_owned); view_dispatcher->tick_event_callback = callback; view_dispatcher->tick_period = tick_period; } @@ -106,11 +113,12 @@ void view_dispatcher_run(ViewDispatcher* view_dispatcher) { uint32_t tick_period = view_dispatcher->tick_period == 0 ? FuriWaitForever : view_dispatcher->tick_period; - furi_event_loop_tick_set( - view_dispatcher->event_loop, - tick_period, - view_dispatcher_handle_tick_event, - view_dispatcher); + if(view_dispatcher->is_event_loop_owned) + furi_event_loop_tick_set( + view_dispatcher->event_loop, + tick_period, + view_dispatcher_handle_tick_event, + view_dispatcher); furi_event_loop_run(view_dispatcher->event_loop); diff --git a/applications/services/gui/view_dispatcher.h b/applications/services/gui/view_dispatcher.h index 9fbf89791..5820bcad3 100644 --- a/applications/services/gui/view_dispatcher.h +++ b/applications/services/gui/view_dispatcher.h @@ -47,6 +47,15 @@ typedef void (*ViewDispatcherTickEventCallback)(void* context); */ ViewDispatcher* view_dispatcher_alloc(void); +/** Allocate ViewDispatcher instance with an externally owned event loop. If + * this constructor is used instead of `view_dispatcher_alloc`, the burden of + * freeing the event loop is placed on the caller. + * + * @param loop pointer to FuriEventLoop instance + * @return pointer to ViewDispatcher instance + */ +ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop); + /** Free ViewDispatcher instance * * @warning All added views MUST be removed using view_dispatcher_remove_view() @@ -97,6 +106,10 @@ void view_dispatcher_set_navigation_event_callback( /** Set tick event handler * + * @warning Requires the event loop to be owned by the view dispatcher, i.e. + * it should have been instantiated with `view_dispatcher_alloc`, not + * `view_dispatcher_alloc_ex`. + * * @param view_dispatcher ViewDispatcher instance * @param callback ViewDispatcherTickEventCallback * @param tick_period callback call period diff --git a/applications/services/gui/view_dispatcher_i.h b/applications/services/gui/view_dispatcher_i.h index c6c8dc665..3d84b5499 100644 --- a/applications/services/gui/view_dispatcher_i.h +++ b/applications/services/gui/view_dispatcher_i.h @@ -14,6 +14,7 @@ DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT struct ViewDispatcher { + bool is_event_loop_owned; FuriEventLoop* event_loop; FuriMessageQueue* input_queue; FuriMessageQueue* event_queue; diff --git a/applications/services/storage/storage.h b/applications/services/storage/storage.h index ea0ff24ad..c28a5e10f 100644 --- a/applications/services/storage/storage.h +++ b/applications/services/storage/storage.h @@ -377,7 +377,7 @@ void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, Furi * @param storage pointer to a storage API instance. * @param source pointer to a zero-terminated string containing the source path. * @param dest pointer to a zero-terminated string containing the destination path. - * @return FSE_OK if the migration was successfull completed, any other error code on failure. + * @return FSE_OK if the migration was successfully completed, any other error code on failure. */ FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest); @@ -425,7 +425,7 @@ bool storage_common_is_subdir(Storage* storage, const char* parent, const char* /******************* Error Functions *******************/ /** - * @brief Get the textual description of a numeric error identifer. + * @brief Get the textual description of a numeric error identifier. * * @param error_id numeric identifier of the error in question. * @return pointer to a statically allocated zero-terminated string containing the respective error text. diff --git a/documentation/Doxyfile b/documentation/Doxyfile index 5031f8d59..e3cc3f6fa 100644 --- a/documentation/Doxyfile +++ b/documentation/Doxyfile @@ -1106,7 +1106,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = +IMAGE_PATH = $(DOXY_SRC_ROOT)/documentation/images # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program diff --git a/documentation/images/dialog.png b/documentation/images/dialog.png new file mode 100644 index 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 8fbd78faa..f3396f98c 100644 --- a/fbt_options.py +++ b/fbt_options.py @@ -75,6 +75,7 @@ FIRMWARE_APPS = { "updater_app", "radio_device_cc1101_ext", "unit_tests", + "js_app", ], } diff --git a/firmware.scons b/firmware.scons index 62b1184eb..4c5e05873 100644 --- a/firmware.scons +++ b/firmware.scons @@ -216,6 +216,7 @@ fwelf = fwenv["FW_ELF"] = fwenv.Program( sources, LIBS=fwenv["TARGET_CFG"].linker_dependencies, ) +Depends(fwelf, fwenv["LINKER_SCRIPT_PATH"]) # Firmware depends on everything child builders returned # Depends(fwelf, lib_targets) diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index f4f008a71..b622aa7a1 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -418,6 +418,18 @@ void furi_event_loop_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* o FURI_CRITICAL_EXIT(); } +bool furi_event_loop_is_subscribed(FuriEventLoop* instance, FuriEventLoopObject* object) { + furi_check(instance); + furi_check(instance->thread_id == furi_thread_get_current_id()); + FURI_CRITICAL_ENTER(); + + FuriEventLoopItem* const* item = FuriEventLoopTree_cget(instance->tree, object); + bool result = !!item; + + FURI_CRITICAL_EXIT(); + return result; +} + /* * Private Event Loop Item functions */ diff --git a/furi/core/event_loop.h b/furi/core/event_loop.h index af5987101..6c5ba432c 100644 --- a/furi/core/event_loop.h +++ b/furi/core/event_loop.h @@ -289,6 +289,23 @@ void furi_event_loop_subscribe_mutex( */ void furi_event_loop_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* object); +/** + * @brief Checks if the loop is subscribed to an object of any kind + * + * @param instance Event Loop instance + * @param object Object to check + */ +bool furi_event_loop_is_subscribed(FuriEventLoop* instance, FuriEventLoopObject* object); + +/** + * @brief Convenience function for `if(is_subscribed()) unsubscribe()` + */ +static inline void + furi_event_loop_maybe_unsubscribe(FuriEventLoop* instance, FuriEventLoopObject* object) { + if(furi_event_loop_is_subscribed(instance, object)) + furi_event_loop_unsubscribe(instance, object); +} + #ifdef __cplusplus } #endif diff --git a/lib/mjs/mjs_core.c b/lib/mjs/mjs_core.c index bcdcb364a..f3e28a5ba 100644 --- a/lib/mjs/mjs_core.c +++ b/lib/mjs/mjs_core.c @@ -103,6 +103,7 @@ struct mjs* mjs_create(void* context) { sizeof(struct mjs_object), MJS_OBJECT_ARENA_SIZE, MJS_OBJECT_ARENA_INC_SIZE); + mjs->object_arena.destructor = mjs_obj_destructor; gc_arena_init( &mjs->property_arena, sizeof(struct mjs_property), diff --git a/lib/mjs/mjs_object.c b/lib/mjs/mjs_object.c index 2aea1bd46..60bacf514 100644 --- a/lib/mjs/mjs_object.c +++ b/lib/mjs/mjs_object.c @@ -9,6 +9,7 @@ #include "mjs_primitive.h" #include "mjs_string.h" #include "mjs_util.h" +#include "furi.h" #include "common/mg_str.h" @@ -20,6 +21,19 @@ MJS_PRIVATE mjs_val_t mjs_object_to_value(struct mjs_object* o) { } } +MJS_PRIVATE void mjs_obj_destructor(struct mjs* mjs, void* cell) { + struct mjs_object* obj = cell; + mjs_val_t obj_val = mjs_object_to_value(obj); + + struct mjs_property* destructor = mjs_get_own_property( + mjs, obj_val, MJS_DESTRUCTOR_PROP_NAME, strlen(MJS_DESTRUCTOR_PROP_NAME)); + if(!destructor) return; + if(!mjs_is_foreign(destructor->value)) return; + + mjs_custom_obj_destructor_t destructor_fn = mjs_get_ptr(mjs, destructor->value); + if(destructor_fn) destructor_fn(mjs, obj_val); +} + MJS_PRIVATE struct mjs_object* get_object_struct(mjs_val_t v) { struct mjs_object* ret = NULL; if(mjs_is_null(v)) { @@ -293,7 +307,8 @@ mjs_val_t * start from the end so the constructed object more closely resembles * the definition. */ - while(def->name != NULL) def++; + while(def->name != NULL) + def++; for(def--; def >= defs; def--) { mjs_val_t v = MJS_UNDEFINED; const char* ptr = (const char*)base + def->offset; diff --git a/lib/mjs/mjs_object.h b/lib/mjs/mjs_object.h index 1c4810385..870486d06 100644 --- a/lib/mjs/mjs_object.h +++ b/lib/mjs/mjs_object.h @@ -50,6 +50,11 @@ MJS_PRIVATE mjs_err_t mjs_set_internal( */ MJS_PRIVATE void mjs_op_create_object(struct mjs* mjs); +/* + * Cell destructor for object arena + */ +MJS_PRIVATE void mjs_obj_destructor(struct mjs* mjs, void* cell); + #define MJS_PROTO_PROP_NAME "__p" /* Make it < 5 chars */ #if defined(__cplusplus) diff --git a/lib/mjs/mjs_object_public.h b/lib/mjs/mjs_object_public.h index f9f06c616..1a021a9d8 100644 --- a/lib/mjs/mjs_object_public.h +++ b/lib/mjs/mjs_object_public.h @@ -119,6 +119,14 @@ int mjs_del(struct mjs* mjs, mjs_val_t obj, const char* name, size_t len); */ mjs_val_t mjs_next(struct mjs* mjs, mjs_val_t obj, mjs_val_t* iterator); +typedef void (*mjs_custom_obj_destructor_t)(struct mjs* mjs, mjs_val_t object); + +/* + * Destructor property name. If set, must be a foreign pointer to a function + * that will be called just before the object is freed. + */ +#define MJS_DESTRUCTOR_PROP_NAME "__d" + #if defined(__cplusplus) } #endif /* __cplusplus */ diff --git a/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c b/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c index 32cc8f198..2519fb90c 100644 --- a/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c +++ b/lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.c @@ -65,10 +65,8 @@ static NfcCommand iso14443_4a_listener_run(NfcGenericEvent event, void* context) if(instance->state == Iso14443_4aListenerStateIdle) { if(bit_buffer_get_size_bytes(rx_buffer) == 2 && bit_buffer_get_byte(rx_buffer, 0) == ISO14443_4A_CMD_READ_ATS) { - if(iso14443_4a_listener_send_ats(instance, &instance->data->ats_data) != + if(iso14443_4a_listener_send_ats(instance, &instance->data->ats_data) == Iso14443_4aErrorNone) { - command = NfcCommandContinue; - } else { instance->state = Iso14443_4aListenerStateActive; } } @@ -93,7 +91,6 @@ static NfcCommand iso14443_4a_listener_run(NfcGenericEvent event, void* context) if(instance->callback) { command = instance->callback(instance->generic_event, instance->context); } - command = NfcCommandContinue; } return command; diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index e6b025c8c..7943c4cfc 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,78.0,, +Version,+,77.2,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1116,6 +1116,7 @@ Function,+,furi_event_flag_set,uint32_t,"FuriEventFlag*, uint32_t" Function,+,furi_event_flag_wait,uint32_t,"FuriEventFlag*, uint32_t, uint32_t, uint32_t" Function,+,furi_event_loop_alloc,FuriEventLoop*, Function,+,furi_event_loop_free,void,FuriEventLoop* +Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObject*" Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* @@ -1372,6 +1373,8 @@ Function,-,furi_hal_resources_deinit_early,void, Function,+,furi_hal_resources_get_ext_pin_number,int32_t,const GpioPin* Function,-,furi_hal_resources_init,void, Function,-,furi_hal_resources_init_early,void, +Function,+,furi_hal_resources_pin_by_name,const GpioPinRecord*,const char* +Function,+,furi_hal_resources_pin_by_number,const GpioPinRecord*,uint8_t Function,-,furi_hal_rtc_deinit_early,void, Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,DateTime* @@ -2687,6 +2690,7 @@ Function,+,text_input_get_validator_callback_context,void*,TextInput* Function,+,text_input_get_view,View*,TextInput* Function,+,text_input_reset,void,TextInput* Function,+,text_input_set_header_text,void,"TextInput*, const char*" +Function,+,text_input_set_minimum_length,void,"TextInput*, size_t" Function,+,text_input_set_result_callback,void,"TextInput*, TextInputCallback, void*, char*, size_t, _Bool" Function,+,text_input_set_validator,void,"TextInput*, TextInputValidatorCallback, void*" Function,-,tgamma,double,double @@ -2761,6 +2765,7 @@ Function,+,view_allocate_model,void,"View*, ViewModelType, size_t" Function,+,view_commit_model,void,"View*, _Bool" Function,+,view_dispatcher_add_view,void,"ViewDispatcher*, uint32_t, View*" Function,+,view_dispatcher_alloc,ViewDispatcher*, +Function,+,view_dispatcher_alloc_ex,ViewDispatcher*,FuriEventLoop* Function,+,view_dispatcher_attach_to_gui,void,"ViewDispatcher*, Gui*, ViewDispatcherType" Function,+,view_dispatcher_enable_queue,void,ViewDispatcher* Function,+,view_dispatcher_free,void,ViewDispatcher* diff --git a/targets/f18/furi_hal/furi_hal_resources.c b/targets/f18/furi_hal/furi_hal_resources.c index 45ca3e6c4..2e3654435 100644 --- a/targets/f18/furi_hal/furi_hal_resources.c +++ b/targets/f18/furi_hal/furi_hal_resources.c @@ -354,3 +354,19 @@ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio) { } return -1; } + +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(strcasecmp(name, record->name) == 0) return record; + } + return NULL; +} + +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(record->number == number) return record; + } + return NULL; +} diff --git a/targets/f18/furi_hal/furi_hal_resources.h b/targets/f18/furi_hal/furi_hal_resources.h index 8f6173eb9..9a0d04cb6 100644 --- a/targets/f18/furi_hal/furi_hal_resources.h +++ b/targets/f18/furi_hal/furi_hal_resources.h @@ -121,6 +121,26 @@ void furi_hal_resources_init(void); */ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio); +/** + * @brief Finds a pin by its name + * + * @param name case-insensitive pin name to look for (e.g. `"Pc3"`, `"pA4"`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name); + +/** + * @brief Finds a pin by its number + * + * @param name pin number to look for (e.g. `7`, `4`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number); + #ifdef __cplusplus } #endif diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index b6128aa30..8358958f4 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,78.0,, +Version,+,77.2,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -1256,6 +1256,7 @@ Function,+,furi_event_flag_set,uint32_t,"FuriEventFlag*, uint32_t" Function,+,furi_event_flag_wait,uint32_t,"FuriEventFlag*, uint32_t, uint32_t, uint32_t" Function,+,furi_event_loop_alloc,FuriEventLoop*, Function,+,furi_event_loop_free,void,FuriEventLoop* +Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObject*" Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* @@ -1576,6 +1577,8 @@ Function,-,furi_hal_resources_deinit_early,void, Function,+,furi_hal_resources_get_ext_pin_number,int32_t,const GpioPin* Function,-,furi_hal_resources_init,void, Function,-,furi_hal_resources_init_early,void, +Function,+,furi_hal_resources_pin_by_name,const GpioPinRecord*,const char* +Function,+,furi_hal_resources_pin_by_number,const GpioPinRecord*,uint8_t Function,+,furi_hal_rfid_comp_set_callback,void,"FuriHalRfidCompCallback, void*" Function,+,furi_hal_rfid_comp_start,void, Function,+,furi_hal_rfid_comp_stop,void, @@ -3691,6 +3694,7 @@ Function,+,view_allocate_model,void,"View*, ViewModelType, size_t" Function,+,view_commit_model,void,"View*, _Bool" Function,+,view_dispatcher_add_view,void,"ViewDispatcher*, uint32_t, View*" Function,+,view_dispatcher_alloc,ViewDispatcher*, +Function,+,view_dispatcher_alloc_ex,ViewDispatcher*,FuriEventLoop* Function,+,view_dispatcher_attach_to_gui,void,"ViewDispatcher*, Gui*, ViewDispatcherType" Function,+,view_dispatcher_enable_queue,void,ViewDispatcher* Function,+,view_dispatcher_free,void,ViewDispatcher* diff --git a/targets/f7/furi_hal/furi_hal_resources.c b/targets/f7/furi_hal/furi_hal_resources.c index 486c24230..123ebc420 100644 --- a/targets/f7/furi_hal/furi_hal_resources.c +++ b/targets/f7/furi_hal/furi_hal_resources.c @@ -288,3 +288,19 @@ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio) { } return -1; } + +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(strcasecmp(name, record->name) == 0) return record; + } + return NULL; +} + +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number) { + for(size_t i = 0; i < gpio_pins_count; i++) { + const GpioPinRecord* record = &gpio_pins[i]; + if(record->number == number) return record; + } + return NULL; +} diff --git a/targets/f7/furi_hal/furi_hal_resources.h b/targets/f7/furi_hal/furi_hal_resources.h index c01b2207f..ec8794cc1 100644 --- a/targets/f7/furi_hal/furi_hal_resources.h +++ b/targets/f7/furi_hal/furi_hal_resources.h @@ -227,6 +227,26 @@ void furi_hal_resources_init(void); */ int32_t furi_hal_resources_get_ext_pin_number(const GpioPin* gpio); +/** + * @brief Finds a pin by its name + * + * @param name case-insensitive pin name to look for (e.g. `"Pc3"`, `"pA4"`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_name(const char* name); + +/** + * @brief Finds a pin by its number + * + * @param name pin number to look for (e.g. `7`, `4`) + * + * @return a pointer to the corresponding `GpioPinRecord` if such a pin exists, + * `NULL` otherwise. + */ +const GpioPinRecord* furi_hal_resources_pin_by_number(uint8_t number); + #ifdef __cplusplus } #endif diff --git a/targets/f7/stm32wb55xx_flash.ld b/targets/f7/stm32wb55xx_flash.ld index 3fb789645..524da6fc3 100644 --- a/targets/f7/stm32wb55xx_flash.ld +++ b/targets/f7/stm32wb55xx_flash.ld @@ -3,7 +3,7 @@ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _stack_end = 0x20030000; /* end of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ -_stack_size = 0x1000; /* required amount of stack */ +_stack_size = 0x200; /* required amount of stack */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K diff --git a/targets/f7/stm32wb55xx_ram_fw.ld b/targets/f7/stm32wb55xx_ram_fw.ld index cae30b6e9..f0e8ad678 100644 --- a/targets/f7/stm32wb55xx_ram_fw.ld +++ b/targets/f7/stm32wb55xx_ram_fw.ld @@ -3,7 +3,7 @@ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _stack_end = 0x20030000; /* end of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ -_stack_size = 0x1000; /* required amount of stack */ +_stack_size = 0x200; /* required amount of stack */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..2655a8b97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "CommonJS", + "typeRoots": [ + "./applications/system/js_app/types" + ], + "noLib": true, + }, + "include": [ + "./applications/system/js_app/examples/apps/Scripts", + "./applications/debug/unit_tests/resources/unit_tests/js", + "./applications/system/js_app/types/global.d.ts", + ] +} \ No newline at end of file