diff --git a/.gitignore b/.gitignore index 888c6a6b2..cd919937a 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,7 @@ PVS-Studio.log .gdbinit -/fbt_options_local.py \ No newline at end of file +/fbt_options_local.py + +# JS packages +node_modules/ diff --git a/applications/debug/event_loop_blink_test/event_loop_blink_test.c b/applications/debug/event_loop_blink_test/event_loop_blink_test.c index 7f00e63f2..1cddfa323 100644 --- a/applications/debug/event_loop_blink_test/event_loop_blink_test.c +++ b/applications/debug/event_loop_blink_test/event_loop_blink_test.c @@ -82,7 +82,7 @@ static void view_port_input_callback(InputEvent* input_event, void* context) { furi_message_queue_put(app->input_queue, input_event, 0); } -static bool input_queue_callback(FuriEventLoopObject* object, void* context) { +static void input_queue_callback(FuriEventLoopObject* object, void* context) { FuriMessageQueue* queue = object; EventLoopBlinkTestApp* app = context; @@ -107,8 +107,6 @@ static bool input_queue_callback(FuriEventLoopObject* object, void* context) { furi_event_loop_stop(app->event_loop); } } - - return true; } static void blink_timer_callback(void* context) { diff --git a/applications/debug/unit_tests/resources/unit_tests/js/basic.js b/applications/debug/unit_tests/resources/unit_tests/js/basic.js index 0927595a2..a08041e9f 100644 --- a/applications/debug/unit_tests/resources/unit_tests/js/basic.js +++ b/applications/debug/unit_tests/resources/unit_tests/js/basic.js @@ -1,4 +1,15 @@ let tests = require("tests"); +let flipper = require("flipper"); tests.assert_eq(1337, 1337); tests.assert_eq("hello", "hello"); + +tests.assert_eq("compatible", sdkCompatibilityStatus(0, 1)); +tests.assert_eq("firmwareTooOld", sdkCompatibilityStatus(100500, 0)); +tests.assert_eq("firmwareTooNew", sdkCompatibilityStatus(-100500, 0)); +tests.assert_eq(true, doesSdkSupport(["baseline"])); +tests.assert_eq(false, doesSdkSupport(["abobus", "other-nonexistent-feature"])); + +tests.assert_eq("flipperdevices", flipper.firmwareVendor); +tests.assert_eq(0, flipper.jsSdkVersion[0]); +tests.assert_eq(1, flipper.jsSdkVersion[1]); diff --git a/applications/debug/unit_tests/tests/furi/furi_event_loop.c b/applications/debug/unit_tests/tests/furi/furi_event_loop.c deleted file mode 100644 index 291181c77..000000000 --- a/applications/debug/unit_tests/tests/furi/furi_event_loop.c +++ /dev/null @@ -1,205 +0,0 @@ -#include "../test.h" -#include -#include - -#include -#include - -#define TAG "TestFuriEventLoop" - -#define EVENT_LOOP_EVENT_COUNT (256u) - -typedef struct { - FuriMessageQueue* mq; - - FuriEventLoop* producer_event_loop; - uint32_t producer_counter; - - FuriEventLoop* consumer_event_loop; - uint32_t consumer_counter; -} TestFuriData; - -bool test_furi_event_loop_producer_mq_callback(FuriEventLoopObject* object, void* context) { - furi_check(context); - - TestFuriData* data = context; - furi_check(data->mq == object, "Invalid queue"); - - FURI_LOG_I( - TAG, "producer_mq_callback: %lu %lu", data->producer_counter, data->consumer_counter); - - if(data->producer_counter == EVENT_LOOP_EVENT_COUNT / 2) { - furi_event_loop_unsubscribe(data->producer_event_loop, data->mq); - furi_event_loop_subscribe_message_queue( - data->producer_event_loop, - data->mq, - FuriEventLoopEventOut, - test_furi_event_loop_producer_mq_callback, - data); - } - - if(data->producer_counter == EVENT_LOOP_EVENT_COUNT) { - furi_event_loop_stop(data->producer_event_loop); - return false; - } - - data->producer_counter++; - furi_check( - furi_message_queue_put(data->mq, &data->producer_counter, 0) == FuriStatusOk, - "furi_message_queue_put failed"); - furi_delay_us(furi_hal_random_get() % 1000); - - return true; -} - -int32_t test_furi_event_loop_producer(void* p) { - furi_check(p); - - TestFuriData* data = p; - - FURI_LOG_I(TAG, "producer start 1st run"); - - data->producer_event_loop = furi_event_loop_alloc(); - furi_event_loop_subscribe_message_queue( - data->producer_event_loop, - data->mq, - FuriEventLoopEventOut, - test_furi_event_loop_producer_mq_callback, - data); - - furi_event_loop_run(data->producer_event_loop); - - // 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags - xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits); - - furi_event_loop_unsubscribe(data->producer_event_loop, data->mq); - furi_event_loop_free(data->producer_event_loop); - - FURI_LOG_I(TAG, "producer start 2nd run"); - - data->producer_counter = 0; - data->producer_event_loop = furi_event_loop_alloc(); - - furi_event_loop_subscribe_message_queue( - data->producer_event_loop, - data->mq, - FuriEventLoopEventOut, - test_furi_event_loop_producer_mq_callback, - data); - - furi_event_loop_run(data->producer_event_loop); - - furi_event_loop_unsubscribe(data->producer_event_loop, data->mq); - furi_event_loop_free(data->producer_event_loop); - - FURI_LOG_I(TAG, "producer end"); - - return 0; -} - -bool test_furi_event_loop_consumer_mq_callback(FuriEventLoopObject* object, void* context) { - furi_check(context); - - TestFuriData* data = context; - furi_check(data->mq == object); - - furi_delay_us(furi_hal_random_get() % 1000); - furi_check(furi_message_queue_get(data->mq, &data->consumer_counter, 0) == FuriStatusOk); - - FURI_LOG_I( - TAG, "consumer_mq_callback: %lu %lu", data->producer_counter, data->consumer_counter); - - if(data->consumer_counter == EVENT_LOOP_EVENT_COUNT / 2) { - furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq); - furi_event_loop_subscribe_message_queue( - data->consumer_event_loop, - data->mq, - FuriEventLoopEventIn, - test_furi_event_loop_consumer_mq_callback, - data); - } - - if(data->consumer_counter == EVENT_LOOP_EVENT_COUNT) { - furi_event_loop_stop(data->consumer_event_loop); - return false; - } - - return true; -} - -int32_t test_furi_event_loop_consumer(void* p) { - furi_check(p); - - TestFuriData* data = p; - - FURI_LOG_I(TAG, "consumer start 1st run"); - - data->consumer_event_loop = furi_event_loop_alloc(); - furi_event_loop_subscribe_message_queue( - data->consumer_event_loop, - data->mq, - FuriEventLoopEventIn, - test_furi_event_loop_consumer_mq_callback, - data); - - furi_event_loop_run(data->consumer_event_loop); - - // 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags - xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits); - - furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq); - furi_event_loop_free(data->consumer_event_loop); - - FURI_LOG_I(TAG, "consumer start 2nd run"); - - data->consumer_counter = 0; - data->consumer_event_loop = furi_event_loop_alloc(); - furi_event_loop_subscribe_message_queue( - data->consumer_event_loop, - data->mq, - FuriEventLoopEventIn, - test_furi_event_loop_consumer_mq_callback, - data); - - furi_event_loop_run(data->consumer_event_loop); - - furi_event_loop_unsubscribe(data->consumer_event_loop, data->mq); - furi_event_loop_free(data->consumer_event_loop); - - FURI_LOG_I(TAG, "consumer end"); - - return 0; -} - -void test_furi_event_loop(void) { - TestFuriData data = {}; - - data.mq = furi_message_queue_alloc(16, sizeof(uint32_t)); - - FuriThread* producer_thread = furi_thread_alloc(); - furi_thread_set_name(producer_thread, "producer_thread"); - furi_thread_set_stack_size(producer_thread, 1 * 1024); - furi_thread_set_callback(producer_thread, test_furi_event_loop_producer); - furi_thread_set_context(producer_thread, &data); - furi_thread_start(producer_thread); - - FuriThread* consumer_thread = furi_thread_alloc(); - furi_thread_set_name(consumer_thread, "consumer_thread"); - furi_thread_set_stack_size(consumer_thread, 1 * 1024); - furi_thread_set_callback(consumer_thread, test_furi_event_loop_consumer); - furi_thread_set_context(consumer_thread, &data); - furi_thread_start(consumer_thread); - - // Wait for thread to complete their tasks - furi_thread_join(producer_thread); - furi_thread_join(consumer_thread); - - // The test itself - mu_assert_int_eq(data.producer_counter, data.consumer_counter); - mu_assert_int_eq(data.producer_counter, EVENT_LOOP_EVENT_COUNT); - - // Release memory - furi_thread_free(consumer_thread); - furi_thread_free(producer_thread); - furi_message_queue_free(data.mq); -} diff --git a/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c b/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c new file mode 100644 index 000000000..73f38ab77 --- /dev/null +++ b/applications/debug/unit_tests/tests/furi/furi_event_loop_test.c @@ -0,0 +1,490 @@ +#include "../test.h" +#include +#include + +#include +#include + +#define TAG "TestFuriEventLoop" + +#define MESSAGE_COUNT (256UL) +#define EVENT_FLAG_COUNT (23UL) +#define PRIMITIVE_COUNT (4UL) +#define RUN_COUNT (2UL) + +typedef struct { + FuriEventLoop* event_loop; + uint32_t message_queue_count; + uint32_t stream_buffer_count; + uint32_t event_flag_count; + uint32_t semaphore_count; + uint32_t primitives_tested; +} TestFuriEventLoopThread; + +typedef struct { + FuriMessageQueue* message_queue; + FuriStreamBuffer* stream_buffer; + FuriEventFlag* event_flag; + FuriSemaphore* semaphore; + + TestFuriEventLoopThread producer; + TestFuriEventLoopThread consumer; +} TestFuriEventLoopData; + +static void test_furi_event_loop_pending_callback(void* context) { + furi_check(context); + + TestFuriEventLoopThread* test_thread = context; + furi_check(test_thread->primitives_tested < PRIMITIVE_COUNT); + + test_thread->primitives_tested++; + FURI_LOG_I(TAG, "primitives tested: %lu", test_thread->primitives_tested); + + if(test_thread->primitives_tested == PRIMITIVE_COUNT) { + furi_event_loop_stop(test_thread->event_loop); + } +} + +static void test_furi_event_loop_thread_init(TestFuriEventLoopThread* test_thread) { + memset(test_thread, 0, sizeof(TestFuriEventLoopThread)); + test_thread->event_loop = furi_event_loop_alloc(); +} + +static void test_furi_event_loop_thread_run_and_cleanup(TestFuriEventLoopThread* test_thread) { + furi_event_loop_run(test_thread->event_loop); + // 2 EventLoop index, 0xFFFFFFFF - all possible flags, emulate uncleared flags + xTaskNotifyIndexed(xTaskGetCurrentTaskHandle(), 2, 0xFFFFFFFF, eSetBits); + furi_event_loop_free(test_thread->event_loop); +} + +static void test_furi_event_loop_producer_message_queue_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->message_queue == object); + + FURI_LOG_I( + TAG, + "producer MessageQueue: %lu %lu", + data->producer.message_queue_count, + data->consumer.message_queue_count); + + if(data->producer.message_queue_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->message_queue); + furi_event_loop_subscribe_message_queue( + data->producer.event_loop, + data->message_queue, + FuriEventLoopEventOut, + test_furi_event_loop_producer_message_queue_callback, + data); + + } else if(data->producer.message_queue_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->message_queue); + furi_event_loop_pend_callback( + data->producer.event_loop, test_furi_event_loop_pending_callback, &data->producer); + return; + } + + data->producer.message_queue_count++; + + furi_check( + furi_message_queue_put(data->message_queue, &data->producer.message_queue_count, 0) == + FuriStatusOk); + + furi_delay_us(furi_hal_random_get() % 100); +} + +static void test_furi_event_loop_producer_stream_buffer_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->stream_buffer == object); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + FURI_LOG_I( + TAG, + "producer StreamBuffer: %lu %lu", + producer->stream_buffer_count, + consumer->stream_buffer_count); + + if(producer->stream_buffer_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(producer->event_loop, data->stream_buffer); + furi_event_loop_subscribe_stream_buffer( + producer->event_loop, + data->stream_buffer, + FuriEventLoopEventOut, + test_furi_event_loop_producer_stream_buffer_callback, + data); + + } else if(producer->stream_buffer_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(producer->event_loop, data->stream_buffer); + furi_event_loop_pend_callback( + producer->event_loop, test_furi_event_loop_pending_callback, producer); + return; + } + + producer->stream_buffer_count++; + + furi_check( + furi_stream_buffer_send( + data->stream_buffer, &producer->stream_buffer_count, sizeof(uint32_t), 0) == + sizeof(uint32_t)); + + furi_delay_us(furi_hal_random_get() % 100); +} + +static void + test_furi_event_loop_producer_event_flag_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->event_flag == object); + + const uint32_t producer_flags = (1UL << data->producer.event_flag_count); + const uint32_t consumer_flags = (1UL << data->consumer.event_flag_count); + + FURI_LOG_I(TAG, "producer EventFlag: 0x%06lX 0x%06lX", producer_flags, consumer_flags); + + furi_check(furi_event_flag_set(data->event_flag, producer_flags) & producer_flags); + + if(data->producer.event_flag_count == EVENT_FLAG_COUNT / 2) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->event_flag); + furi_event_loop_subscribe_event_flag( + data->producer.event_loop, + data->event_flag, + FuriEventLoopEventOut, + test_furi_event_loop_producer_event_flag_callback, + data); + + } else if(data->producer.event_flag_count == EVENT_FLAG_COUNT) { + furi_event_loop_unsubscribe(data->producer.event_loop, data->event_flag); + furi_event_loop_pend_callback( + data->producer.event_loop, test_furi_event_loop_pending_callback, &data->producer); + return; + } + + data->producer.event_flag_count++; + + furi_delay_us(furi_hal_random_get() % 100); +} + +static void + test_furi_event_loop_producer_semaphore_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->semaphore == object); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + FURI_LOG_I( + TAG, "producer Semaphore: %lu %lu", producer->semaphore_count, consumer->semaphore_count); + furi_check(furi_semaphore_release(data->semaphore) == FuriStatusOk); + + if(producer->semaphore_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(producer->event_loop, data->semaphore); + furi_event_loop_subscribe_semaphore( + producer->event_loop, + data->semaphore, + FuriEventLoopEventOut, + test_furi_event_loop_producer_semaphore_callback, + data); + + } else if(producer->semaphore_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(producer->event_loop, data->semaphore); + furi_event_loop_pend_callback( + producer->event_loop, test_furi_event_loop_pending_callback, producer); + return; + } + + data->producer.semaphore_count++; + + furi_delay_us(furi_hal_random_get() % 100); +} + +static int32_t test_furi_event_loop_producer(void* p) { + furi_check(p); + + TestFuriEventLoopData* data = p; + TestFuriEventLoopThread* producer = &data->producer; + + for(uint32_t i = 0; i < RUN_COUNT; ++i) { + FURI_LOG_I(TAG, "producer start run %lu", i); + + test_furi_event_loop_thread_init(producer); + + furi_event_loop_subscribe_message_queue( + producer->event_loop, + data->message_queue, + FuriEventLoopEventOut, + test_furi_event_loop_producer_message_queue_callback, + data); + furi_event_loop_subscribe_stream_buffer( + producer->event_loop, + data->stream_buffer, + FuriEventLoopEventOut, + test_furi_event_loop_producer_stream_buffer_callback, + data); + furi_event_loop_subscribe_event_flag( + producer->event_loop, + data->event_flag, + FuriEventLoopEventOut, + test_furi_event_loop_producer_event_flag_callback, + data); + furi_event_loop_subscribe_semaphore( + producer->event_loop, + data->semaphore, + FuriEventLoopEventOut, + test_furi_event_loop_producer_semaphore_callback, + data); + + test_furi_event_loop_thread_run_and_cleanup(producer); + } + + FURI_LOG_I(TAG, "producer end"); + + return 0; +} + +static void test_furi_event_loop_consumer_message_queue_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->message_queue == object); + + furi_delay_us(furi_hal_random_get() % 100); + + furi_check( + furi_message_queue_get(data->message_queue, &data->consumer.message_queue_count, 0) == + FuriStatusOk); + + FURI_LOG_I( + TAG, + "consumer MessageQueue: %lu %lu", + data->producer.message_queue_count, + data->consumer.message_queue_count); + + if(data->consumer.message_queue_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->message_queue); + furi_event_loop_subscribe_message_queue( + data->consumer.event_loop, + data->message_queue, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_message_queue_callback, + data); + + } else if(data->consumer.message_queue_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->message_queue); + furi_event_loop_pend_callback( + data->consumer.event_loop, test_furi_event_loop_pending_callback, &data->consumer); + } +} + +static void test_furi_event_loop_consumer_stream_buffer_callback( + FuriEventLoopObject* object, + void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->stream_buffer == object); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + furi_delay_us(furi_hal_random_get() % 100); + + furi_check( + furi_stream_buffer_receive( + data->stream_buffer, &consumer->stream_buffer_count, sizeof(uint32_t), 0) == + sizeof(uint32_t)); + + FURI_LOG_I( + TAG, + "consumer StreamBuffer: %lu %lu", + producer->stream_buffer_count, + consumer->stream_buffer_count); + + if(consumer->stream_buffer_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(consumer->event_loop, data->stream_buffer); + furi_event_loop_subscribe_stream_buffer( + consumer->event_loop, + data->stream_buffer, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_stream_buffer_callback, + data); + + } else if(consumer->stream_buffer_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->stream_buffer); + furi_event_loop_pend_callback( + consumer->event_loop, test_furi_event_loop_pending_callback, consumer); + } +} + +static void + test_furi_event_loop_consumer_event_flag_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->event_flag == object); + + furi_delay_us(furi_hal_random_get() % 100); + + const uint32_t producer_flags = (1UL << data->producer.event_flag_count); + const uint32_t consumer_flags = (1UL << data->consumer.event_flag_count); + + furi_check( + furi_event_flag_wait(data->event_flag, consumer_flags, FuriFlagWaitAny, 0) & + consumer_flags); + + FURI_LOG_I(TAG, "consumer EventFlag: 0x%06lX 0x%06lX", producer_flags, consumer_flags); + + if(data->consumer.event_flag_count == EVENT_FLAG_COUNT / 2) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->event_flag); + furi_event_loop_subscribe_event_flag( + data->consumer.event_loop, + data->event_flag, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_event_flag_callback, + data); + + } else if(data->consumer.event_flag_count == EVENT_FLAG_COUNT) { + furi_event_loop_unsubscribe(data->consumer.event_loop, data->event_flag); + furi_event_loop_pend_callback( + data->consumer.event_loop, test_furi_event_loop_pending_callback, &data->consumer); + return; + } + + data->consumer.event_flag_count++; +} + +static void + test_furi_event_loop_consumer_semaphore_callback(FuriEventLoopObject* object, void* context) { + furi_check(context); + + TestFuriEventLoopData* data = context; + furi_check(data->semaphore == object); + + furi_delay_us(furi_hal_random_get() % 100); + + TestFuriEventLoopThread* producer = &data->producer; + TestFuriEventLoopThread* consumer = &data->consumer; + + furi_check(furi_semaphore_acquire(data->semaphore, 0) == FuriStatusOk); + + FURI_LOG_I( + TAG, "consumer Semaphore: %lu %lu", producer->semaphore_count, consumer->semaphore_count); + + if(consumer->semaphore_count == MESSAGE_COUNT / 2) { + furi_event_loop_unsubscribe(consumer->event_loop, data->semaphore); + furi_event_loop_subscribe_semaphore( + consumer->event_loop, + data->semaphore, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_semaphore_callback, + data); + + } else if(consumer->semaphore_count == MESSAGE_COUNT) { + furi_event_loop_unsubscribe(consumer->event_loop, data->semaphore); + furi_event_loop_pend_callback( + consumer->event_loop, test_furi_event_loop_pending_callback, consumer); + return; + } + + data->consumer.semaphore_count++; +} + +static int32_t test_furi_event_loop_consumer(void* p) { + furi_check(p); + + TestFuriEventLoopData* data = p; + TestFuriEventLoopThread* consumer = &data->consumer; + + for(uint32_t i = 0; i < RUN_COUNT; ++i) { + FURI_LOG_I(TAG, "consumer start run %lu", i); + + test_furi_event_loop_thread_init(consumer); + + furi_event_loop_subscribe_message_queue( + consumer->event_loop, + data->message_queue, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_message_queue_callback, + data); + furi_event_loop_subscribe_stream_buffer( + consumer->event_loop, + data->stream_buffer, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_stream_buffer_callback, + data); + furi_event_loop_subscribe_event_flag( + consumer->event_loop, + data->event_flag, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_event_flag_callback, + data); + furi_event_loop_subscribe_semaphore( + consumer->event_loop, + data->semaphore, + FuriEventLoopEventIn, + test_furi_event_loop_consumer_semaphore_callback, + data); + + test_furi_event_loop_thread_run_and_cleanup(consumer); + } + + FURI_LOG_I(TAG, "consumer end"); + + return 0; +} + +void test_furi_event_loop(void) { + TestFuriEventLoopData data = {}; + + data.message_queue = furi_message_queue_alloc(16, sizeof(uint32_t)); + data.stream_buffer = furi_stream_buffer_alloc(16, sizeof(uint32_t)); + data.event_flag = furi_event_flag_alloc(); + data.semaphore = furi_semaphore_alloc(8, 0); + + FuriThread* producer_thread = + furi_thread_alloc_ex("producer_thread", 1 * 1024, test_furi_event_loop_producer, &data); + furi_thread_start(producer_thread); + + FuriThread* consumer_thread = + furi_thread_alloc_ex("consumer_thread", 1 * 1024, test_furi_event_loop_consumer, &data); + furi_thread_start(consumer_thread); + + // Wait for thread to complete their tasks + furi_thread_join(producer_thread); + furi_thread_join(consumer_thread); + + TestFuriEventLoopThread* producer = &data.producer; + TestFuriEventLoopThread* consumer = &data.consumer; + + // The test itself + mu_assert_int_eq(producer->message_queue_count, consumer->message_queue_count); + mu_assert_int_eq(producer->message_queue_count, MESSAGE_COUNT); + mu_assert_int_eq(producer->stream_buffer_count, consumer->stream_buffer_count); + mu_assert_int_eq(producer->stream_buffer_count, MESSAGE_COUNT); + mu_assert_int_eq(producer->event_flag_count, consumer->event_flag_count); + mu_assert_int_eq(producer->event_flag_count, EVENT_FLAG_COUNT); + mu_assert_int_eq(producer->semaphore_count, consumer->semaphore_count); + mu_assert_int_eq(producer->semaphore_count, MESSAGE_COUNT); + + // Release memory + furi_thread_free(consumer_thread); + furi_thread_free(producer_thread); + + furi_message_queue_free(data.message_queue); + furi_stream_buffer_free(data.stream_buffer); + furi_event_flag_free(data.event_flag); + furi_semaphore_free(data.semaphore); +} diff --git a/applications/debug/unit_tests/tests/furi/furi_primitives_test.c b/applications/debug/unit_tests/tests/furi/furi_primitives_test.c new file mode 100644 index 000000000..d9ad03039 --- /dev/null +++ b/applications/debug/unit_tests/tests/furi/furi_primitives_test.c @@ -0,0 +1,103 @@ +#include +#include "../test.h" // IWYU pragma: keep + +#define MESSAGE_QUEUE_CAPACITY (16U) +#define MESSAGE_QUEUE_ELEMENT_SIZE (sizeof(uint32_t)) + +#define STREAM_BUFFER_SIZE (32U) +#define STREAM_BUFFER_TRG_LEVEL (STREAM_BUFFER_SIZE / 2U) + +typedef struct { + FuriMessageQueue* message_queue; + FuriStreamBuffer* stream_buffer; +} TestFuriPrimitivesData; + +static void test_furi_message_queue(TestFuriPrimitivesData* data) { + FuriMessageQueue* message_queue = data->message_queue; + + mu_assert_int_eq(0, furi_message_queue_get_count(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_capacity(message_queue)); + mu_assert_int_eq( + MESSAGE_QUEUE_ELEMENT_SIZE, furi_message_queue_get_message_size(message_queue)); + + for(uint32_t i = 0;; ++i) { + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY - i, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(i, furi_message_queue_get_count(message_queue)); + + if(furi_message_queue_put(message_queue, &i, 0) != FuriStatusOk) { + break; + } + } + + mu_assert_int_eq(0, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_count(message_queue)); + + for(uint32_t i = 0;; ++i) { + mu_assert_int_eq(i, furi_message_queue_get_space(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY - i, furi_message_queue_get_count(message_queue)); + + uint32_t value; + if(furi_message_queue_get(message_queue, &value, 0) != FuriStatusOk) { + break; + } + + mu_assert_int_eq(i, value); + } + + mu_assert_int_eq(0, furi_message_queue_get_count(message_queue)); + mu_assert_int_eq(MESSAGE_QUEUE_CAPACITY, furi_message_queue_get_space(message_queue)); +} + +static void test_furi_stream_buffer(TestFuriPrimitivesData* data) { + FuriStreamBuffer* stream_buffer = data->stream_buffer; + + mu_assert(furi_stream_buffer_is_empty(stream_buffer), "Must be empty"); + mu_assert(!furi_stream_buffer_is_full(stream_buffer), "Must be not full"); + mu_assert_int_eq(0, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq(STREAM_BUFFER_SIZE, furi_stream_buffer_spaces_available(stream_buffer)); + + for(uint8_t i = 0;; ++i) { + mu_assert_int_eq(i, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq( + STREAM_BUFFER_SIZE - i, furi_stream_buffer_spaces_available(stream_buffer)); + + if(furi_stream_buffer_send(stream_buffer, &i, sizeof(uint8_t), 0) != sizeof(uint8_t)) { + break; + } + } + + mu_assert(!furi_stream_buffer_is_empty(stream_buffer), "Must be not empty"); + mu_assert(furi_stream_buffer_is_full(stream_buffer), "Must be full"); + mu_assert_int_eq(STREAM_BUFFER_SIZE, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq(0, furi_stream_buffer_spaces_available(stream_buffer)); + + for(uint8_t i = 0;; ++i) { + mu_assert_int_eq( + STREAM_BUFFER_SIZE - i, furi_stream_buffer_bytes_available(stream_buffer)); + mu_assert_int_eq(i, furi_stream_buffer_spaces_available(stream_buffer)); + + uint8_t value; + if(furi_stream_buffer_receive(stream_buffer, &value, sizeof(uint8_t), 0) != + sizeof(uint8_t)) { + break; + } + + mu_assert_int_eq(i, value); + } +} + +// This is a stub that needs expanding +void test_furi_primitives(void) { + TestFuriPrimitivesData data = { + .message_queue = + furi_message_queue_alloc(MESSAGE_QUEUE_CAPACITY, MESSAGE_QUEUE_ELEMENT_SIZE), + .stream_buffer = furi_stream_buffer_alloc(STREAM_BUFFER_SIZE, STREAM_BUFFER_TRG_LEVEL), + }; + + test_furi_message_queue(&data); + test_furi_stream_buffer(&data); + + furi_message_queue_free(data.message_queue); + furi_stream_buffer_free(data.stream_buffer); +} diff --git a/applications/debug/unit_tests/tests/furi/furi_test.c b/applications/debug/unit_tests/tests/furi/furi_test.c index 2a76d5184..193a8124d 100644 --- a/applications/debug/unit_tests/tests/furi/furi_test.c +++ b/applications/debug/unit_tests/tests/furi/furi_test.c @@ -9,6 +9,7 @@ void test_furi_pubsub(void); void test_furi_memmgr(void); void test_furi_event_loop(void); void test_errno_saving(void); +void test_furi_primitives(void); static int foo = 0; @@ -47,6 +48,10 @@ MU_TEST(mu_test_errno_saving) { test_errno_saving(); } +MU_TEST(mu_test_furi_primitives) { + test_furi_primitives(); +} + MU_TEST_SUITE(test_suite) { MU_SUITE_CONFIGURE(&test_setup, &test_teardown); MU_RUN_TEST(test_check); @@ -57,6 +62,7 @@ MU_TEST_SUITE(test_suite) { MU_RUN_TEST(mu_test_furi_memmgr); MU_RUN_TEST(mu_test_furi_event_loop); MU_RUN_TEST(mu_test_errno_saving); + MU_RUN_TEST(mu_test_furi_primitives); } int run_minunit_test_furi(void) { diff --git a/applications/examples/example_event_loop/application.fam b/applications/examples/example_event_loop/application.fam index a37ffb1a0..15a7c8837 100644 --- a/applications/examples/example_event_loop/application.fam +++ b/applications/examples/example_event_loop/application.fam @@ -1,3 +1,12 @@ +App( + appid="example_event_loop_event_flags", + name="Example: Event Loop Event Flags", + apptype=FlipperAppType.EXTERNAL, + sources=["example_event_loop_event_flags.c"], + entry_point="example_event_loop_event_flags_app", + fap_category="Examples", +) + App( appid="example_event_loop_timer", name="Example: Event Loop Timer", diff --git a/applications/examples/example_event_loop/example_event_loop_event_flags.c b/applications/examples/example_event_loop/example_event_loop_event_flags.c new file mode 100644 index 000000000..5d0acf7f1 --- /dev/null +++ b/applications/examples/example_event_loop/example_event_loop_event_flags.c @@ -0,0 +1,173 @@ +/** + * @file example_event_loop_event_flags.c + * @brief Example application demonstrating the use of the FuriEventFlag primitive in FuriEventLoop instances. + * + * This application receives keystrokes from the input service and sets the appropriate flags, + * which are subsequently processed in the event loop + */ + +#include +#include +#include + +#include + +#define TAG "ExampleEventLoopEventFlags" + +typedef struct { + Gui* gui; + ViewPort* view_port; + FuriEventLoop* event_loop; + FuriEventFlag* event_flag; +} EventLoopEventFlagsApp; + +typedef enum { + EventLoopEventFlagsOk = (1 << 0), + EventLoopEventFlagsUp = (1 << 1), + EventLoopEventFlagsDown = (1 << 2), + EventLoopEventFlagsLeft = (1 << 3), + EventLoopEventFlagsRight = (1 << 4), + EventLoopEventFlagsBack = (1 << 5), + EventLoopEventFlagsExit = (1 << 6), +} EventLoopEventFlags; + +#define EVENT_LOOP_EVENT_FLAGS_MASK \ + (EventLoopEventFlagsOk | EventLoopEventFlagsUp | EventLoopEventFlagsDown | \ + EventLoopEventFlagsLeft | EventLoopEventFlagsRight | EventLoopEventFlagsBack | \ + EventLoopEventFlagsExit) + +// This function is executed in the GUI context each time an input event occurs (e.g. the user pressed a key) +static void event_loop_event_flags_app_input_callback(InputEvent* event, void* context) { + furi_assert(context); + EventLoopEventFlagsApp* app = context; + UNUSED(app); + + if(event->type == InputTypePress) { + if(event->key == InputKeyOk) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsOk); + } else if(event->key == InputKeyUp) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsUp); + } else if(event->key == InputKeyDown) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsDown); + } else if(event->key == InputKeyLeft) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsLeft); + } else if(event->key == InputKeyRight) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsRight); + } else if(event->key == InputKeyBack) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsBack); + } + } else if(event->type == InputTypeLong) { + if(event->key == InputKeyBack) { + furi_event_flag_set(app->event_flag, EventLoopEventFlagsExit); + } + } +} + +// This function is executed each time a new event flag is inserted in the input event flag. +static void + event_loop_event_flags_app_event_flags_callback(FuriEventLoopObject* object, void* context) { + furi_assert(context); + EventLoopEventFlagsApp* app = context; + + furi_assert(object == app->event_flag); + + EventLoopEventFlags events = + furi_event_flag_wait(app->event_flag, EVENT_LOOP_EVENT_FLAGS_MASK, FuriFlagWaitAny, 0); + furi_check((events) != 0); + + if(events & EventLoopEventFlagsOk) { + FURI_LOG_I(TAG, "Press \"Ok\""); + } + if(events & EventLoopEventFlagsUp) { + FURI_LOG_I(TAG, "Press \"Up\""); + } + if(events & EventLoopEventFlagsDown) { + FURI_LOG_I(TAG, "Press \"Down\""); + } + if(events & EventLoopEventFlagsLeft) { + FURI_LOG_I(TAG, "Press \"Left\""); + } + if(events & EventLoopEventFlagsRight) { + FURI_LOG_I(TAG, "Press \"Right\""); + } + if(events & EventLoopEventFlagsBack) { + FURI_LOG_I(TAG, "Press \"Back\""); + } + if(events & EventLoopEventFlagsExit) { + FURI_LOG_I(TAG, "Exit App"); + furi_event_loop_stop(app->event_loop); + } +} + +static EventLoopEventFlagsApp* event_loop_event_flags_app_alloc(void) { + EventLoopEventFlagsApp* app = malloc(sizeof(EventLoopEventFlagsApp)); + + // Create event loop instances. + app->event_loop = furi_event_loop_alloc(); + // Create event flag instances. + app->event_flag = furi_event_flag_alloc(); + + // Create GUI instance. + app->gui = furi_record_open(RECORD_GUI); + app->view_port = view_port_alloc(); + // Gain exclusive access to the input events + view_port_input_callback_set(app->view_port, event_loop_event_flags_app_input_callback, app); + gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); + + // Notify the event loop about incoming messages in the event flag + furi_event_loop_subscribe_event_flag( + app->event_loop, + app->event_flag, + FuriEventLoopEventIn | FuriEventLoopEventFlagEdge, + event_loop_event_flags_app_event_flags_callback, + app); + + return app; +} + +static void event_loop_event_flags_app_free(EventLoopEventFlagsApp* app) { + gui_remove_view_port(app->gui, app->view_port); + + furi_record_close(RECORD_GUI); + app->gui = NULL; + + // Delete all instances + view_port_free(app->view_port); + app->view_port = NULL; + + // IMPORTANT: The user code MUST unsubscribe from all events before deleting the event loop. + // Failure to do so will result in a crash. + furi_event_loop_unsubscribe(app->event_loop, app->event_flag); + + furi_event_flag_free(app->event_flag); + app->event_flag = NULL; + + furi_event_loop_free(app->event_loop); + app->event_loop = NULL; + + free(app); +} + +static void event_loop_event_flags_app_run(EventLoopEventFlagsApp* app) { + FURI_LOG_I(TAG, "Press keys to see them printed here."); + FURI_LOG_I(TAG, "Quickly press different keys to generate events."); + FURI_LOG_I(TAG, "Long press \"Back\" to exit app."); + + // Run the application event loop. This call will block until the application is about to exit. + furi_event_loop_run(app->event_loop); +} + +/******************************************************************* + * vvv START HERE vvv + * + * The application's entry point - referenced in application.fam + *******************************************************************/ +int32_t example_event_loop_event_flags_app(void* arg) { + UNUSED(arg); + + EventLoopEventFlagsApp* app = event_loop_event_flags_app_alloc(); + event_loop_event_flags_app_run(app); + event_loop_event_flags_app_free(app); + + return 0; +} diff --git a/applications/examples/example_event_loop/example_event_loop_multi.c b/applications/examples/example_event_loop/example_event_loop_multi.c index ebfb00911..ae748da55 100644 --- a/applications/examples/example_event_loop/example_event_loop_multi.c +++ b/applications/examples/example_event_loop/example_event_loop_multi.c @@ -52,7 +52,7 @@ typedef struct { */ // This function is executed each time the data is taken out of the stream buffer. It is used to restart the worker timer. -static bool +static void event_loop_multi_app_stream_buffer_worker_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMultiAppWorker* worker = context; @@ -62,8 +62,6 @@ static bool FURI_LOG_I(TAG, "Data was removed from buffer"); // Restart the timer to generate another block of random data. furi_event_loop_timer_start(worker->timer, WORKER_DATA_INTERVAL_MS); - - return true; } // This function is executed when the worker timer expires. The timer will NOT restart automatically @@ -152,7 +150,7 @@ static void event_loop_multi_app_input_callback(InputEvent* event, void* context } // This function is executed each time new data is available in the stream buffer. -static bool +static void event_loop_multi_app_stream_buffer_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMultiApp* app = context; @@ -172,12 +170,10 @@ static bool FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str)); furi_string_free(tmp_str); - - return true; } // This function is executed each time a new message is inserted in the input queue. -static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) { +static void event_loop_multi_app_input_queue_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMultiApp* app = context; @@ -222,8 +218,6 @@ static bool event_loop_multi_app_input_queue_callback(FuriEventLoopObject* objec // Not a long press, just print the key's name. FURI_LOG_I(TAG, "Short press: %s", input_get_key_name(event.key)); } - - return true; } // This function is executed each time the countdown timer expires. diff --git a/applications/examples/example_event_loop/example_event_loop_mutex.c b/applications/examples/example_event_loop/example_event_loop_mutex.c index d043f3f89..20bf7af4b 100644 --- a/applications/examples/example_event_loop/example_event_loop_mutex.c +++ b/applications/examples/example_event_loop/example_event_loop_mutex.c @@ -59,7 +59,7 @@ static int32_t event_loop_mutex_app_worker_thread(void* context) { } // This function is being run each time when the mutex gets released -static bool event_loop_mutex_app_event_callback(FuriEventLoopObject* object, void* context) { +static void event_loop_mutex_app_event_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopMutexApp* app = context; @@ -82,8 +82,6 @@ static bool event_loop_mutex_app_event_callback(FuriEventLoopObject* object, voi MUTEX_EVENT_AND_FLAGS, event_loop_mutex_app_event_callback, app); - - return true; } static EventLoopMutexApp* event_loop_mutex_app_alloc(void) { diff --git a/applications/examples/example_event_loop/example_event_loop_stream_buffer.c b/applications/examples/example_event_loop/example_event_loop_stream_buffer.c index 65dbd83cf..6f7280973 100644 --- a/applications/examples/example_event_loop/example_event_loop_stream_buffer.c +++ b/applications/examples/example_event_loop/example_event_loop_stream_buffer.c @@ -54,7 +54,7 @@ static int32_t event_loop_stream_buffer_app_worker_thread(void* context) { } // This function is being run each time when the number of bytes in the buffer is above its trigger level. -static bool +static void event_loop_stream_buffer_app_event_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); EventLoopStreamBufferApp* app = context; @@ -76,8 +76,6 @@ static bool FURI_LOG_I(TAG, "Received data: %s", furi_string_get_cstr(tmp_str)); furi_string_free(tmp_str); - - return true; } static EventLoopStreamBufferApp* event_loop_stream_buffer_app_alloc(void) { diff --git a/applications/main/nfc/plugins/supported_cards/clipper.c b/applications/main/nfc/plugins/supported_cards/clipper.c index 90e8a8919..3c306c9bc 100644 --- a/applications/main/nfc/plugins/supported_cards/clipper.c +++ b/applications/main/nfc/plugins/supported_cards/clipper.c @@ -140,6 +140,19 @@ static const IdMapping actransit_zones[] = { }; static const size_t kNumACTransitZones = COUNT(actransit_zones); +// Instead of persisting individual Station IDs, Caltrain saves Zone numbers. +// https://www.caltrain.com/stations-zones +static const IdMapping caltrain_zones[] = { + {.id = 0x0001, .name = "Zone 1"}, + {.id = 0x0002, .name = "Zone 2"}, + {.id = 0x0003, .name = "Zone 3"}, + {.id = 0x0004, .name = "Zone 4"}, + {.id = 0x0005, .name = "Zone 5"}, + {.id = 0x0006, .name = "Zone 6"}, +}; + +static const size_t kNumCaltrainZones = COUNT(caltrain_zones); + // // Full agency+zone mapping. // @@ -150,6 +163,7 @@ static const struct { } agency_zone_map[] = { {.agency_id = 0x0001, .zone_map = actransit_zones, .zone_count = kNumACTransitZones}, {.agency_id = 0x0004, .zone_map = bart_zones, .zone_count = kNumBARTZones}, + {.agency_id = 0x0006, .zone_map = caltrain_zones, .zone_count = kNumCaltrainZones}, {.agency_id = 0x0012, .zone_map = muni_zones, .zone_count = kNumMUNIZones}}; static const size_t kNumAgencyZoneMaps = COUNT(agency_zone_map); diff --git a/applications/main/nfc/plugins/supported_cards/ndef.c b/applications/main/nfc/plugins/supported_cards/ndef.c index 820324421..fb2c4da48 100644 --- a/applications/main/nfc/plugins/supported_cards/ndef.c +++ b/applications/main/nfc/plugins/supported_cards/ndef.c @@ -582,7 +582,7 @@ bool ndef_parse_record( NdefTnf tnf, const char* type, uint8_t type_len) { - FURI_LOG_D(TAG, "payload type: %.*s len: %d", type_len, type, len); + FURI_LOG_D(TAG, "payload type: %.*s len: %hu", type_len, type, len); if(!len) { furi_string_cat(ndef->output, "Empty\n"); return true; @@ -702,9 +702,9 @@ bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, pos += id_len; if(smart_poster) { - furi_string_cat_printf(ndef->output, "\e*> SP-R%d: ", record_num); + furi_string_cat_printf(ndef->output, "\e*> SP-R%zu: ", record_num); } else { - furi_string_cat_printf(ndef->output, "\e*> M%d-R%d: ", message_num, record_num); + furi_string_cat_printf(ndef->output, "\e*> M%zu-R%zu: ", message_num, record_num); } if(!ndef_parse_record(ndef, pos, payload_len, flags_tnf.type_name_format, type, type_len)) { if(type_was_allocated) free(type); @@ -721,7 +721,7 @@ bool ndef_parse_message(Ndef* ndef, size_t pos, size_t len, size_t message_num, if(smart_poster) { furi_string_cat(ndef->output, "\e*> SP: Empty\n\n"); } else { - furi_string_cat_printf(ndef->output, "\e*> M%d: Empty\n\n", message_num); + furi_string_cat_printf(ndef->output, "\e*> M%zu: Empty\n\n", message_num); } } @@ -949,7 +949,7 @@ static bool ndef_mfc_parse(const NfcDevice* device, FuriString* parsed_data) { } else { data_block = 93 + (sector - 32) * 15; } - FURI_LOG_D(TAG, "data_block: %d", data_block); + FURI_LOG_D(TAG, "data_block: %zu", data_block); size_t data_start = data_block * MF_CLASSIC_BLOCK_SIZE; size_t parsed = ndef_parse_tlv(&ndef, data_start, data_size - data_start, total_parsed); @@ -982,7 +982,7 @@ static bool ndef_slix_parse(const NfcDevice* device, FuriString* parsed_data) { const uint16_t block_count = iso15693_3_get_block_count(data); const uint8_t* blocks = simple_array_cget_data(data->block_data); - // TODO: Find some way to check for other iso15693 NDEF cards and + // TODO(-nofl): Find some way to check for other iso15693 NDEF cards and // split this to also support non-slix iso15693 NDEF tags // Rest of the code works on iso15693 too, but uses SLIX layout assumptions if(block_size != SLIX_BLOCK_SIZE) { diff --git a/applications/services/dolphin/dolphin.c b/applications/services/dolphin/dolphin.c index 5d8dc61cb..09feee40f 100644 --- a/applications/services/dolphin/dolphin.c +++ b/applications/services/dolphin/dolphin.c @@ -212,7 +212,7 @@ static void dolphin_update_clear_limits_timer_period(void* context) { FURI_LOG_D(TAG, "Daily limits reset in %lu ms", time_to_clear_limits); } -static bool dolphin_process_event(FuriEventLoopObject* object, void* context) { +static void dolphin_process_event(FuriEventLoopObject* object, void* context) { UNUSED(object); Dolphin* dolphin = context; @@ -264,8 +264,6 @@ static bool dolphin_process_event(FuriEventLoopObject* object, void* context) { } dolphin_event_release(&event); - - return true; } static void dolphin_storage_callback(const void* message, void* context) { diff --git a/applications/services/gui/view_dispatcher.c b/applications/services/gui/view_dispatcher.c index 6db4d8241..e85ff2b20 100644 --- a/applications/services/gui/view_dispatcher.c +++ b/applications/services/gui/view_dispatcher.c @@ -376,7 +376,7 @@ void view_dispatcher_update(View* view, void* context) { } } -bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context) { +void view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); ViewDispatcher* instance = context; furi_assert(instance->event_queue == object); @@ -384,11 +384,9 @@ bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* conte uint32_t event; furi_check(furi_message_queue_get(instance->event_queue, &event, 0) == FuriStatusOk); view_dispatcher_handle_custom_event(instance, event); - - return true; } -bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context) { +void view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); ViewDispatcher* instance = context; furi_assert(instance->input_queue == object); @@ -396,6 +394,4 @@ bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* conte InputEvent input; furi_check(furi_message_queue_get(instance->input_queue, &input, 0) == FuriStatusOk); view_dispatcher_handle_input(instance, &input); - - return true; } diff --git a/applications/services/gui/view_dispatcher_i.h b/applications/services/gui/view_dispatcher_i.h index 3d84b5499..a5f87d75c 100644 --- a/applications/services/gui/view_dispatcher_i.h +++ b/applications/services/gui/view_dispatcher_i.h @@ -57,7 +57,7 @@ void view_dispatcher_set_current_view(ViewDispatcher* view_dispatcher, View* vie void view_dispatcher_update(View* view, void* context); /** ViewDispatcher run event loop event callback */ -bool view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context); +void view_dispatcher_run_event_callback(FuriEventLoopObject* object, void* context); /** ViewDispatcher run event loop input callback */ -bool view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context); +void view_dispatcher_run_input_callback(FuriEventLoopObject* object, void* context); diff --git a/applications/services/notification/notification_messages.c b/applications/services/notification/notification_messages.c index 8b7916226..3dc154654 100644 --- a/applications/services/notification/notification_messages.c +++ b/applications/services/notification/notification_messages.c @@ -593,3 +593,7 @@ const NotificationSequence sequence_lcd_contrast_update = { &message_lcd_contrast_update, NULL, }; + +const NotificationSequence sequence_empty = { + NULL, +}; diff --git a/applications/services/notification/notification_messages.h b/applications/services/notification/notification_messages.h index 873bb37a8..3960d93b7 100644 --- a/applications/services/notification/notification_messages.h +++ b/applications/services/notification/notification_messages.h @@ -145,6 +145,9 @@ extern const NotificationSequence sequence_audiovisual_alert; // LCD extern const NotificationSequence sequence_lcd_contrast_update; +// Wait for notification queue become empty +extern const NotificationSequence sequence_empty; + #ifdef __cplusplus } #endif diff --git a/applications/services/power/power_service/power.c b/applications/services/power/power_service/power.c index 2773e9fe8..8d387612a 100644 --- a/applications/services/power/power_service/power.c +++ b/applications/services/power/power_service/power.c @@ -380,7 +380,7 @@ static void power_handle_reboot(PowerBootMode mode) { furi_hal_power_reset(); } -static bool power_message_callback(FuriEventLoopObject* object, void* context) { +static void power_message_callback(FuriEventLoopObject* object, void* context) { furi_assert(context); Power* power = context; @@ -412,8 +412,6 @@ static bool power_message_callback(FuriEventLoopObject* object, void* context) { if(msg.lock) { api_lock_unlock(msg.lock); } - - return true; } static void power_tick_callback(void* context) { diff --git a/applications/settings/application.fam b/applications/settings/application.fam index cc4b9703d..1d6db35a7 100644 --- a/applications/settings/application.fam +++ b/applications/settings/application.fam @@ -5,6 +5,7 @@ App( provides=[ "passport", "system_settings", + "clock_settings", "about", ], ) diff --git a/applications/settings/clock_settings/application.fam b/applications/settings/clock_settings/application.fam new file mode 100644 index 000000000..206848aa3 --- /dev/null +++ b/applications/settings/clock_settings/application.fam @@ -0,0 +1,17 @@ +App( + appid="clock_settings", + name="Clock & Alarm", + apptype=FlipperAppType.SETTINGS, + entry_point="clock_settings", + requires=["gui"], + provides=["clock_settings_start"], + stack_size=1 * 1024, + order=90, +) + +App( + appid="clock_settings_start", + apptype=FlipperAppType.STARTUP, + entry_point="clock_settings_start", + order=1000, +) diff --git a/applications/settings/clock_settings/clock_settings.c b/applications/settings/clock_settings/clock_settings.c new file mode 100644 index 000000000..455f1deea --- /dev/null +++ b/applications/settings/clock_settings/clock_settings.c @@ -0,0 +1,71 @@ +#include "clock_settings.h" + +#include +#include + +static bool clock_settings_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + ClockSettings* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool clock_settings_back_event_callback(void* context) { + furi_assert(context); + ClockSettings* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +ClockSettings* clock_settings_alloc() { + ClockSettings* app = malloc(sizeof(ClockSettings)); + + app->gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&clock_settings_scene_handlers, app); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, clock_settings_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, clock_settings_back_event_callback); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + app->pwm_view = + clock_settings_module_alloc(view_dispatcher_get_event_loop(app->view_dispatcher)); + view_dispatcher_add_view( + app->view_dispatcher, ClockSettingsViewPwm, clock_settings_module_get_view(app->pwm_view)); + + scene_manager_next_scene(app->scene_manager, ClockSettingsSceneStart); + + return app; +} + +void clock_settings_free(ClockSettings* app) { + furi_assert(app); + + // Views + view_dispatcher_remove_view(app->view_dispatcher, ClockSettingsViewPwm); + + clock_settings_module_free(app->pwm_view); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + // Close records + furi_record_close(RECORD_GUI); + + free(app); +} + +int32_t clock_settings(void* p) { + UNUSED(p); + ClockSettings* clock_settings = clock_settings_alloc(); + + view_dispatcher_run(clock_settings->view_dispatcher); + + clock_settings_free(clock_settings); + + return 0; +} diff --git a/applications/settings/clock_settings/clock_settings.h b/applications/settings/clock_settings/clock_settings.h new file mode 100644 index 000000000..db404ec8e --- /dev/null +++ b/applications/settings/clock_settings/clock_settings.h @@ -0,0 +1,31 @@ +#pragma once + +#include "scenes/clock_settings_scene.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include "views/clock_settings_module.h" + +typedef struct ClockSettings ClockSettings; + +struct ClockSettings { + Gui* gui; + ViewDispatcher* view_dispatcher; + SceneManager* scene_manager; + ClockSettingsModule* pwm_view; +}; + +typedef enum { + ClockSettingsViewPwm, +} ClockSettingsView; + +typedef enum { + ClockSettingsCustomEventNone, +} ClockSettingsCustomEvent; diff --git a/applications/settings/clock_settings/clock_settings_alarm.c b/applications/settings/clock_settings/clock_settings_alarm.c new file mode 100644 index 000000000..7b096ef70 --- /dev/null +++ b/applications/settings/clock_settings/clock_settings_alarm.c @@ -0,0 +1,177 @@ +#include +#include + +#include +#include + +#include +#include + +#include + +#define TAG "ClockSettingsAlarm" + +typedef struct { + DateTime now; + IconAnimation* icon; +} ClockSettingsAlramModel; + +const NotificationSequence sequence_alarm = { + &message_force_speaker_volume_setting_1f, + &message_force_vibro_setting_on, + &message_force_display_brightness_setting_1f, + &message_vibro_on, + + &message_display_backlight_on, + &message_note_c7, + &message_delay_250, + + &message_display_backlight_off, + &message_note_c4, + &message_delay_250, + + &message_display_backlight_on, + &message_note_c7, + &message_delay_250, + + &message_display_backlight_off, + &message_note_c4, + &message_delay_250, + + &message_sound_off, + &message_vibro_off, + NULL, +}; + +static void clock_settings_alarm_draw_callback(Canvas* canvas, void* ctx) { + ClockSettingsAlramModel* model = ctx; + char buffer[64] = {}; + + canvas_draw_icon_animation(canvas, 5, 6, model->icon); + + canvas_set_font(canvas, FontBigNumbers); + snprintf(buffer, sizeof(buffer), "%02u:%02u", model->now.hour, model->now.minute); + canvas_draw_str(canvas, 58, 32, buffer); + + canvas_set_font(canvas, FontPrimary); + snprintf( + buffer, + sizeof(buffer), + "%02u.%02u.%04u", + model->now.day, + model->now.month, + model->now.year); + canvas_draw_str(canvas, 60, 44, buffer); +} + +static void clock_settings_alarm_input_callback(InputEvent* input_event, void* ctx) { + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +void clock_settings_alarm_animation_callback(IconAnimation* instance, void* context) { + UNUSED(instance); + ViewPort* view_port = context; + view_port_update(view_port); +} + +int32_t clock_settings_alarm(void* p) { + UNUSED(p); + + // View Model + ClockSettingsAlramModel model; + + furi_hal_rtc_get_datetime(&model.now); + model.icon = icon_animation_alloc(&A_Alarm_47x39); + + // Alloc message queue + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + // Configure view port + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, clock_settings_alarm_draw_callback, &model); + view_port_input_callback_set(view_port, clock_settings_alarm_input_callback, event_queue); + + // Register view port in GUI + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + notification_message(notification, &sequence_alarm); + + icon_animation_set_update_callback( + model.icon, clock_settings_alarm_animation_callback, view_port); + icon_animation_start(model.icon); + + // Process events + InputEvent event; + bool running = true; + while(running) { + if(furi_message_queue_get(event_queue, &event, 2000) == FuriStatusOk) { + if(event.type == InputTypePress) { + running = false; + } + } else { + notification_message(notification, &sequence_alarm); + furi_hal_rtc_get_datetime(&model.now); + view_port_update(view_port); + } + } + + icon_animation_stop(model.icon); + + notification_message_block(notification, &sequence_empty); + furi_record_close(RECORD_NOTIFICATION); + + view_port_enabled_set(view_port, false); + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + furi_message_queue_free(event_queue); + furi_record_close(RECORD_GUI); + + icon_animation_free(model.icon); + + return 0; +} + +FuriThread* clock_settings_alarm_thread = NULL; + +static void clock_settings_alarm_thread_state_callback( + FuriThread* thread, + FuriThreadState state, + void* context) { + furi_assert(clock_settings_alarm_thread == thread); + UNUSED(context); + + if(state == FuriThreadStateStopped) { + furi_thread_free(thread); + clock_settings_alarm_thread = NULL; + } +} + +static void clock_settings_alarm_start(void* context, uint32_t arg) { + UNUSED(context); + UNUSED(arg); + + FURI_LOG_I(TAG, "spawning alarm thread"); + + if(clock_settings_alarm_thread) return; + + clock_settings_alarm_thread = + furi_thread_alloc_ex("ClockAlarm", 1024, clock_settings_alarm, NULL); + furi_thread_set_state_callback( + clock_settings_alarm_thread, clock_settings_alarm_thread_state_callback); + furi_thread_start(clock_settings_alarm_thread); +} + +static void clock_settings_alarm_isr(void* context) { + UNUSED(context); + furi_timer_pending_callback(clock_settings_alarm_start, NULL, 0); +} + +void clock_settings_start(void) { +#ifndef FURI_RAM_EXEC + furi_hal_rtc_set_alarm_callback(clock_settings_alarm_isr, NULL); +#endif +} diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene.c b/applications/settings/clock_settings/scenes/clock_settings_scene.c new file mode 100644 index 000000000..13a1c3395 --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene.c @@ -0,0 +1,30 @@ +#include "../clock_settings.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const clock_settings_scene_on_enter_handlers[])(void*) = { +#include "clock_settings_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const clock_settings_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "clock_settings_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const clock_settings_scene_on_exit_handlers[])(void* context) = { +#include "clock_settings_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers clock_settings_scene_handlers = { + .on_enter_handlers = clock_settings_scene_on_enter_handlers, + .on_event_handlers = clock_settings_scene_on_event_handlers, + .on_exit_handlers = clock_settings_scene_on_exit_handlers, + .scene_num = ClockSettingsSceneNum, +}; diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene.h b/applications/settings/clock_settings/scenes/clock_settings_scene.h new file mode 100644 index 000000000..d0582252c --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) ClockSettingsScene##id, +typedef enum { +#include "clock_settings_scene_config.h" + ClockSettingsSceneNum, +} ClockSettingsScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers clock_settings_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "clock_settings_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "clock_settings_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "clock_settings_scene_config.h" +#undef ADD_SCENE diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene_config.h b/applications/settings/clock_settings/scenes/clock_settings_scene_config.h new file mode 100644 index 000000000..496b3f7af --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene_config.h @@ -0,0 +1 @@ +ADD_SCENE(clock_settings, start, Start) diff --git a/applications/settings/clock_settings/scenes/clock_settings_scene_start.c b/applications/settings/clock_settings/scenes/clock_settings_scene_start.c new file mode 100644 index 000000000..81cf58a74 --- /dev/null +++ b/applications/settings/clock_settings/scenes/clock_settings_scene_start.c @@ -0,0 +1,32 @@ +#include "../clock_settings.h" +#include + +#define TAG "SceneStart" + +typedef enum { + SubmenuIndexPwm, + SubmenuIndexClockOutput, +} SubmenuIndex; + +void clock_settings_scene_start_submenu_callback(void* context, uint32_t index) { + ClockSettings* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void clock_settings_scene_start_on_enter(void* context) { + ClockSettings* app = context; + + view_dispatcher_switch_to_view(app->view_dispatcher, ClockSettingsViewPwm); +} + +bool clock_settings_scene_start_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + + return false; +} + +void clock_settings_scene_start_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/settings/clock_settings/views/clock_settings_module.c b/applications/settings/clock_settings/views/clock_settings_module.c new file mode 100644 index 000000000..9ab5773a5 --- /dev/null +++ b/applications/settings/clock_settings/views/clock_settings_module.c @@ -0,0 +1,438 @@ +#include "clock_settings_module.h" + +#include +#include +#include + +#define TAG "ClockSettingsModule" + +struct ClockSettingsModule { + FuriEventLoopTimer* timer; + View* view; +}; + +typedef struct { + DateTime current; + DateTime alarm; + bool alarm_enabled; + bool editing; + + uint8_t row; + uint8_t column; +} ClockSettingsModuleViewModel; + +typedef enum { + EditStateNone, + EditStateActive, + EditStateActiveEditing, +} EditState; + +#define get_state(m, r, c) \ + ((m)->row == (r) && (m)->column == (c) ? \ + ((m)->editing ? EditStateActiveEditing : EditStateActive) : \ + EditStateNone) + +#define ROW_0_Y (4) +#define ROW_0_H (20) + +#define ROW_1_Y (30) +#define ROW_1_H (12) + +#define ROW_2_Y (48) +#define ROW_2_H (12) + +#define ROW_COUNT 3 +#define COLUMN_COUNT 3 + +static inline void clock_settings_module_cleanup_date(DateTime* dt) { + uint8_t day_per_month = + datetime_get_days_per_month(datetime_is_leap_year(dt->year), dt->month); + if(dt->day > day_per_month) { + dt->day = day_per_month; + } +} + +static inline void clock_settings_module_draw_block( + Canvas* canvas, + int32_t x, + int32_t y, + size_t w, + size_t h, + Font font, + EditState state, + const char* text) { + canvas_set_color(canvas, ColorBlack); + if(state != EditStateNone) { + if(state == EditStateActiveEditing) { + canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5); + canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5); + } + canvas_draw_rbox(canvas, x, y, w, h, 1); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_draw_rframe(canvas, x, y, w, h, 1); + } + + canvas_set_font(canvas, font); + canvas_draw_str_aligned(canvas, x + w / 2, y + h / 2, AlignCenter, AlignCenter, text); + if(state != EditStateNone) { + canvas_set_color(canvas, ColorBlack); + } +} + +static void + clock_settings_module_draw_time_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, ROW_0_Y + 15, "Time"); + + snprintf(buffer, sizeof(buffer), "%02u", model->current.hour); + clock_settings_module_draw_block( + canvas, 32, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 0), buffer); + canvas_draw_box(canvas, 62, ROW_0_Y + ROW_0_H - 7, 2, 2); + canvas_draw_box(canvas, 62, ROW_0_Y + ROW_0_H - 7 - 6, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->current.minute); + clock_settings_module_draw_block( + canvas, 66, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1), buffer); + canvas_draw_box(canvas, 96, ROW_0_Y + ROW_0_H - 7, 2, 2); + canvas_draw_box(canvas, 96, ROW_0_Y + ROW_0_H - 7 - 6, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->current.second); + clock_settings_module_draw_block( + canvas, 100, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2), buffer); +} + +static void + clock_settings_module_draw_date_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, ROW_1_Y + 9, "Date"); + // Day + snprintf(buffer, sizeof(buffer), "%02u", model->current.day); + clock_settings_module_draw_block( + canvas, 44, ROW_1_Y, 17, ROW_1_H, FontPrimary, get_state(model, 1, 0), buffer); + canvas_draw_box(canvas, 71 - 6, ROW_1_Y + ROW_1_H - 4, 2, 2); + // Month + snprintf(buffer, sizeof(buffer), "%02u", model->current.month); + clock_settings_module_draw_block( + canvas, 71, ROW_1_Y, 17, ROW_1_H, FontPrimary, get_state(model, 1, 1), buffer); + canvas_draw_box(canvas, 98 - 6, ROW_1_Y + ROW_1_H - 4, 2, 2); + // Year + snprintf(buffer, sizeof(buffer), "%04u", model->current.year); + clock_settings_module_draw_block( + canvas, 98, ROW_1_Y, 30, ROW_1_H, FontPrimary, get_state(model, 1, 2), buffer); +} + +static void + clock_settings_module_draw_alarm_callback(Canvas* canvas, ClockSettingsModuleViewModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, ROW_2_Y + 9, "Alarm"); + + snprintf(buffer, sizeof(buffer), "%02u", model->alarm.hour); + clock_settings_module_draw_block( + canvas, 58, ROW_2_Y, 17, ROW_2_H, FontPrimary, get_state(model, 2, 0), buffer); + canvas_draw_box(canvas, 81 - 4, ROW_2_Y + ROW_2_H - 4, 2, 2); + canvas_draw_box(canvas, 81 - 4, ROW_2_Y + ROW_2_H - 4 - 4, 2, 2); + + snprintf(buffer, sizeof(buffer), "%02u", model->alarm.minute); + clock_settings_module_draw_block( + canvas, 81, ROW_2_Y, 17, ROW_2_H, FontPrimary, get_state(model, 2, 1), buffer); + + clock_settings_module_draw_block( + canvas, + 106, + ROW_2_Y, + 22, + ROW_2_H, + FontPrimary, + get_state(model, 2, 2), + model->alarm_enabled ? "On" : "Off"); +} + +static void clock_settings_module_draw_callback(Canvas* canvas, void* _model) { + ClockSettingsModuleViewModel* model = _model; + clock_settings_module_draw_time_callback(canvas, model); + clock_settings_module_draw_date_callback(canvas, model); + clock_settings_module_draw_alarm_callback(canvas, model); +} + +static bool clock_settings_module_input_navigation_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->row > 0) model->row--; + } else if(event->key == InputKeyDown) { + if(model->row < ROW_COUNT - 1) model->row++; + } else if(event->key == InputKeyOk) { + model->editing = !model->editing; + } else if(event->key == InputKeyRight) { + if(model->column < COLUMN_COUNT - 1) model->column++; + } else if(event->key == InputKeyLeft) { + if(model->column > 0) model->column--; + } else if(event->key == InputKeyBack && model->editing) { + model->editing = false; + } else { + return false; + } + + return true; +} + +static bool clock_settings_module_input_time_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->column == 0) { + model->current.hour++; + model->current.hour = model->current.hour % 24; + } else if(model->column == 1) { + model->current.minute++; + model->current.minute = model->current.minute % 60; + } else if(model->column == 2) { + model->current.second++; + model->current.second = model->current.second % 60; + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->current.hour > 0) { + model->current.hour--; + } else { + model->current.hour = 23; + } + model->current.hour = model->current.hour % 24; + } else if(model->column == 1) { + if(model->current.minute > 0) { + model->current.minute--; + } else { + model->current.minute = 59; + } + model->current.minute = model->current.minute % 60; + } else if(model->column == 2) { + if(model->current.second > 0) { + model->current.second--; + } else { + model->current.second = 59; + } + model->current.second = model->current.second % 60; + } else { + furi_crash(); + } + } else { + return clock_settings_module_input_navigation_callback(event, model); + } + + return true; +} + +static bool clock_settings_module_input_date_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->column == 0) { + if(model->current.day < 31) model->current.day++; + } else if(model->column == 1) { + if(model->current.month < 12) { + model->current.month++; + } + } else if(model->column == 2) { + if(model->current.year < 2099) { + model->current.year++; + } + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->current.day > 1) { + model->current.day--; + } + } else if(model->column == 1) { + if(model->current.month > 1) { + model->current.month--; + } + } else if(model->column == 2) { + if(model->current.year > 2000) { + model->current.year--; + } + } else { + furi_crash(); + } + } else { + return clock_settings_module_input_navigation_callback(event, model); + } + + clock_settings_module_cleanup_date(&model->current); + + return true; +} + +static bool clock_settings_module_input_alarm_callback( + InputEvent* event, + ClockSettingsModuleViewModel* model) { + if(event->key == InputKeyUp) { + if(model->column == 0) { + model->alarm.hour++; + model->alarm.hour = model->alarm.hour % 24; + } else if(model->column == 1) { + model->alarm.minute++; + model->alarm.minute = model->alarm.minute % 60; + } else if(model->column == 2) { + model->alarm_enabled = !model->alarm_enabled; + } else { + furi_crash(); + } + } else if(event->key == InputKeyDown) { + if(model->column == 0) { + if(model->alarm.hour > 0) { + model->alarm.hour--; + } else { + model->alarm.hour = 23; + } + model->alarm.hour = model->alarm.hour % 24; + } else if(model->column == 1) { + if(model->alarm.minute > 0) { + model->alarm.minute--; + } else { + model->alarm.minute = 59; + } + model->alarm.minute = model->alarm.minute % 60; + } else if(model->column == 2) { + model->alarm_enabled = !model->alarm_enabled; + } else { + furi_crash(); + } + } else { + return clock_settings_module_input_navigation_callback(event, model); + } + + return true; +} + +static bool clock_settings_module_input_callback(InputEvent* event, void* context) { + furi_assert(context); + + ClockSettingsModule* instance = context; + bool consumed = false; + + with_view_model( + instance->view, + ClockSettingsModuleViewModel * model, + { + if(event->type == InputTypeShort || event->type == InputTypeRepeat) { + bool previous_editing = model->editing; + if(model->editing) { + if(model->row == 0) { + consumed = clock_settings_module_input_time_callback(event, model); + } else if(model->row == 1) { + consumed = clock_settings_module_input_date_callback(event, model); + } else if(model->row == 2) { + consumed = clock_settings_module_input_alarm_callback(event, model); + } else { + furi_crash(); + } + } else { + consumed = clock_settings_module_input_navigation_callback(event, model); + } + + // Switching between navigate/edit + if(model->editing != previous_editing) { + if(model->row == 2) { + if(!model->editing) { + // Disable alarm + furi_hal_rtc_set_alarm(NULL, false); + // Set new alarm + furi_hal_rtc_set_alarm(&model->alarm, model->alarm_enabled); + // Confirm + model->alarm_enabled = furi_hal_rtc_get_alarm(&model->alarm); + } + } else { + if(model->editing) { + // stop timer to prevent mess with current date time + furi_event_loop_timer_stop(instance->timer); + } else { + // save date time and restart timer + furi_hal_rtc_set_datetime(&model->current); + furi_event_loop_timer_start(instance->timer, 1000); + } + } + } + } + }, + true); + + return consumed; +} + +static void clock_settings_module_timer_callback(void* context) { + furi_assert(context); + ClockSettingsModule* instance = context; + + DateTime dt; + furi_hal_rtc_get_datetime(&dt); + with_view_model( + instance->view, ClockSettingsModuleViewModel * model, { model->current = dt; }, true); +} + +static void clock_settings_module_view_enter_callback(void* context) { + furi_assert(context); + ClockSettingsModule* instance = context; + + clock_settings_module_timer_callback(context); + + DateTime alarm; + bool enabled = furi_hal_rtc_get_alarm(&alarm); + + with_view_model( + instance->view, + ClockSettingsModuleViewModel * model, + { + model->alarm = alarm; + model->alarm_enabled = enabled; + }, + true); + + furi_event_loop_timer_start(instance->timer, 1000); +} + +static void clock_settings_module_view_exit_callback(void* context) { + furi_assert(context); + ClockSettingsModule* instance = context; + furi_event_loop_timer_stop(instance->timer); +} + +ClockSettingsModule* clock_settings_module_alloc(FuriEventLoop* event_loop) { + ClockSettingsModule* instance = malloc(sizeof(ClockSettingsModule)); + + instance->timer = furi_event_loop_timer_alloc( + event_loop, clock_settings_module_timer_callback, FuriEventLoopTimerTypePeriodic, instance); + instance->view = view_alloc(); + view_set_enter_callback(instance->view, clock_settings_module_view_enter_callback); + view_set_exit_callback(instance->view, clock_settings_module_view_exit_callback); + view_allocate_model( + instance->view, ViewModelTypeLocking, sizeof(ClockSettingsModuleViewModel)); + with_view_model( + instance->view, ClockSettingsModuleViewModel * model, { model->row = 0; }, false); + view_set_context(instance->view, instance); + view_set_draw_callback(instance->view, clock_settings_module_draw_callback); + view_set_input_callback(instance->view, clock_settings_module_input_callback); + + return instance; +} + +void clock_settings_module_free(ClockSettingsModule* instance) { + furi_assert(instance); + view_free(instance->view); + free(instance); +} + +View* clock_settings_module_get_view(ClockSettingsModule* instance) { + furi_assert(instance); + return instance->view; +} diff --git a/applications/settings/clock_settings/views/clock_settings_module.h b/applications/settings/clock_settings/views/clock_settings_module.h new file mode 100644 index 000000000..1ae1dee0e --- /dev/null +++ b/applications/settings/clock_settings/views/clock_settings_module.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +typedef struct ClockSettingsModule ClockSettingsModule; +typedef void (*ClockSettingsModuleViewCallback)( + uint8_t channel_id, + uint32_t freq, + uint8_t duty, + void* context); + +ClockSettingsModule* clock_settings_module_alloc(FuriEventLoop* event_loop); + +void clock_settings_module_free(ClockSettingsModule* instance); + +View* clock_settings_module_get_view(ClockSettingsModule* instance); + +void clock_settings_module_set( + ClockSettingsModule* instance, + const DateTime* datetime, + bool enabled); + +bool clock_settings_module_get(ClockSettingsModule* instance, DateTime* datetime); diff --git a/applications/system/application.fam b/applications/system/application.fam index c5f81defa..9a7ae40b1 100644 --- a/applications/system/application.fam +++ b/applications/system/application.fam @@ -5,7 +5,6 @@ App( provides=[ "updater_app", "js_app", - "js_app_start", # "archive", ], ) diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 5402bada7..26e61cf66 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -6,6 +6,16 @@ App( stack_size=2 * 1024, resources="examples", order=0, + provides=["js_app_start"], + sources=[ + "js_app.c", + "js_modules.c", + "js_thread.c", + "plugin_api/app_api_table.cpp", + "views/console_view.c", + "modules/js_flipper.c", + "modules/js_tests.c", + ], ) App( @@ -13,6 +23,7 @@ App( apptype=FlipperAppType.STARTUP, entry_point="js_app_on_system_start", order=160, + sources=["js_app.c"], ) App( @@ -96,6 +107,7 @@ App( entry_point="js_gui_file_picker_ep", requires=["js_app"], sources=["modules/js_gui/file_picker.c"], + fap_libs=["assets"], ) App( diff --git a/applications/system/js_app/examples/apps/Scripts/interactive.js b/applications/system/js_app/examples/apps/Scripts/interactive.js new file mode 100644 index 000000000..40ca98c30 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/interactive.js @@ -0,0 +1,93 @@ +let eventLoop = require("event_loop"); +let gui = require("gui"); +let dialog = require("gui/dialog"); +let textInput = require("gui/text_input"); +let loading = require("gui/loading"); +let storage = require("storage"); + +// No eval() or exec() so need to run code from file, and filename must be unique +storage.makeDirectory("/ext/.tmp"); +storage.makeDirectory("/ext/.tmp/js"); +storage.rmrf("/ext/.tmp/js/repl") +storage.makeDirectory("/ext/.tmp/js/repl") +let ctx = { + tmpTemplate: "/ext/.tmp/js/repl/", + tmpNumber: 0, + persistentScope: {}, +}; + +let views = { + dialog: dialog.makeWith({ + header: "Interactive Console", + text: "Press OK to Start", + center: "Run Some JS" + }), + textInput: textInput.makeWith({ + header: "Type JavaScript Code:", + minLength: 0, + maxLength: 256, + defaultText: "2+2", + defaultTextClear: true, + }), + loading: loading.make(), +}; + +eventLoop.subscribe(views.dialog.input, function (_sub, button, gui, views) { + if (button === "center") { + gui.viewDispatcher.switchTo(views.textInput); + } +}, gui, views); + +eventLoop.subscribe(views.textInput.input, function (_sub, text, gui, views, ctx) { + gui.viewDispatcher.switchTo(views.loading); + + let path = ctx.tmpTemplate + (ctx.tmpNumber++).toString(); + let file = storage.openFile(path, "w", "create_always"); + file.write(text); + file.close(); + + // Hide GUI before running, we want to see console and avoid deadlock if code fails + gui.viewDispatcher.sendTo("back"); + let result = load(path, ctx.persistentScope); // Load runs JS and returns last value on stack + storage.remove(path); + + // Must convert to string explicitly + if (result === null) { // mJS: typeof null === "null", ECMAScript: typeof null === "object", IDE complains when checking "null" type + result = "null"; + } else if (typeof result === "string") { + result = "'" + result + "'"; + } else if (typeof result === "number") { + result = result.toString(); + } else if (typeof result === "bigint") { // mJS doesn't support BigInt() but might aswell check + result = "bigint"; + } else if (typeof result === "boolean") { + result = result ? "true" : "false"; + } else if (typeof result === "symbol") { // mJS doesn't support Symbol() but might aswell check + result = "symbol"; + } else if (typeof result === "undefined") { + result = "undefined"; + } else if (typeof result === "object") { + result = "object"; // JSON.stringify() is not implemented + } else if (typeof result === "function") { + result = "function"; + } else { + result = "unknown type: " + typeof result; + } + + gui.viewDispatcher.sendTo("front"); + views.dialog.set("header", "JS Returned:"); + views.dialog.set("text", result); + gui.viewDispatcher.switchTo(views.dialog); + views.textInput.set("defaultText", text); +}, gui, views, ctx); + +eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) { + eventLoop.stop(); +}, eventLoop); + +gui.viewDispatcher.switchTo(views.dialog); + +// Message behind GUI if something breaks +print("If you're stuck here, something went wrong, re-run the script") +eventLoop.run(); +print("\n\nFinished correctly :)") diff --git a/applications/system/js_app/examples/apps/Scripts/js_examples/gpio.js b/applications/system/js_app/examples/apps/Scripts/js_examples/gpio.js index f3b4bc121..24d0f0286 100644 --- a/applications/system/js_app/examples/apps/Scripts/js_examples/gpio.js +++ b/applications/system/js_app/examples/apps/Scripts/js_examples/gpio.js @@ -19,7 +19,7 @@ eventLoop.subscribe(eventLoop.timer("periodic", 1000), function (_, _item, led, // 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"); + print("PC0 is at", pot.readAnalog(), "mV"); }, pot); // the program will just exit unless this is here diff --git a/applications/system/js_app/examples/apps/Scripts/load.js b/applications/system/js_app/examples/apps/Scripts/load.js new file mode 100644 index 000000000..82b2d2046 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/load.js @@ -0,0 +1,3 @@ +let math = load(__dirname + "/load_api.js"); +let result = math.add(5, 10); +print(result); diff --git a/applications/system/js_app/examples/apps/Scripts/path.js b/applications/system/js_app/examples/apps/Scripts/path.js new file mode 100644 index 000000000..0be31b81d --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/path.js @@ -0,0 +1,9 @@ +let storage = require("storage"); + +print("script has __dirname of" + __dirname); +print("script has __filename of" + __filename); +if (storage.fileExists(__dirname + "/math.js")) { + print("math.js exist here."); +} else { + print("math.js does not exist here."); +} diff --git a/applications/system/js_app/examples/apps/Scripts/storage.js b/applications/system/js_app/examples/apps/Scripts/storage.js new file mode 100644 index 000000000..c0ec8bfa4 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/storage.js @@ -0,0 +1,29 @@ +let storage = require("storage"); +let path = "/ext/storage.test"; + +print("File exists:", storage.fileExists(path)); + +print("Writing..."); +let file = storage.openFile(path, "w", "create_always"); +file.write("Hello "); +file.close(); + +print("File exists:", storage.fileExists(path)); + +file = storage.openFile(path, "w", "open_append"); +file.write("World!"); +file.close(); + +print("Reading..."); +file = storage.openFile(path, "r", "open_existing"); +let text = file.read("ascii", 128); +file.close(); +print(text); + +print("Removing...") +storage.remove(path); + +print("Done") + +// You don't need to close the file after each operation, this is just to show some different ways to use the API +// There's also many more functions and options, check type definitions in firmware repo \ No newline at end of file diff --git a/applications/system/js_app/examples/apps/Scripts/stringutils.js b/applications/system/js_app/examples/apps/Scripts/stringutils.js new file mode 100644 index 000000000..b2facb237 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/stringutils.js @@ -0,0 +1,19 @@ +let sampleText = "Hello, World!"; + +let lengthOfText = "Length of text: " + sampleText.length.toString(); +print(lengthOfText); + +let start = 7; +let end = 12; +let substringResult = sampleText.slice(start, end); +print(substringResult); + +let searchStr = "World"; +let result2 = sampleText.indexOf(searchStr).toString(); +print(result2); + +let upperCaseText = "Text in upper case: " + sampleText.toUpperCase(); +print(upperCaseText); + +let lowerCaseText = "Text in lower case: " + sampleText.toLowerCase(); +print(lowerCaseText); diff --git a/applications/system/js_app/js_modules.c b/applications/system/js_app/js_modules.c index 38ff46f75..bffa553a8 100644 --- a/applications/system/js_app/js_modules.c +++ b/applications/system/js_app/js_modules.c @@ -1,6 +1,8 @@ #include #include "js_modules.h" #include +#include +#include #include "modules/js_flipper.h" #ifdef FW_CFG_unit_tests @@ -76,6 +78,12 @@ JsModuleData* js_find_loaded_module(JsModules* instance, const char* name) { } mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_len) { + // Ignore the initial part of the module name + const char* optional_module_prefix = "@" JS_SDK_VENDOR "/fz-sdk/"; + if(strncmp(name, optional_module_prefix, strlen(optional_module_prefix)) == 0) { + name += strlen(optional_module_prefix); + } + // Check if module is already installed JsModuleData* module_inst = js_find_loaded_module(modules, name); if(module_inst) { //-V547 @@ -175,3 +183,133 @@ void* js_module_get(JsModules* modules, const char* name) { furi_string_free(module_name); return module_inst ? module_inst->context : NULL; } + +typedef enum { + JsSdkCompatStatusCompatible, + JsSdkCompatStatusFirmwareTooOld, + JsSdkCompatStatusFirmwareTooNew, +} JsSdkCompatStatus; + +/** + * @brief Checks compatibility between the firmware and the JS SDK version + * expected by the script + */ +static JsSdkCompatStatus + js_internal_sdk_compatibility_status(int32_t exp_major, int32_t exp_minor) { + if(exp_major < JS_SDK_MAJOR) return JsSdkCompatStatusFirmwareTooNew; + if(exp_major > JS_SDK_MAJOR || exp_minor > JS_SDK_MINOR) + return JsSdkCompatStatusFirmwareTooOld; + return JsSdkCompatStatusCompatible; +} + +#define JS_SDK_COMPAT_ARGS \ + int32_t major, minor; \ + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&major), JS_ARG_INT32(&minor)); + +void js_sdk_compatibility_status(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + switch(status) { + case JsSdkCompatStatusCompatible: + mjs_return(mjs, mjs_mk_string(mjs, "compatible", ~0, 0)); + return; + case JsSdkCompatStatusFirmwareTooOld: + mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooOld", ~0, 0)); + return; + case JsSdkCompatStatusFirmwareTooNew: + mjs_return(mjs, mjs_mk_string(mjs, "firmwareTooNew", ~0, 0)); + return; + } +} + +void js_is_sdk_compatible(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + mjs_return(mjs, mjs_mk_boolean(mjs, status == JsSdkCompatStatusCompatible)); +} + +/** + * @brief Asks the user whether to continue executing an incompatible script + */ +static bool js_internal_compat_ask_user(const char* message) { + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* dialog = dialog_message_alloc(); + dialog_message_set_header(dialog, message, 64, 0, AlignCenter, AlignTop); + dialog_message_set_text( + dialog, "This script may not\nwork as expected", 79, 32, AlignCenter, AlignCenter); + dialog_message_set_icon(dialog, &I_Warning_30x23, 0, 18); + dialog_message_set_buttons(dialog, "Go back", NULL, "Run anyway"); + DialogMessageButton choice = dialog_message_show(dialogs, dialog); + dialog_message_free(dialog); + furi_record_close(RECORD_DIALOGS); + return choice == DialogMessageButtonRight; +} + +void js_check_sdk_compatibility(struct mjs* mjs) { + JS_SDK_COMPAT_ARGS; + JsSdkCompatStatus status = js_internal_sdk_compatibility_status(major, minor); + if(status != JsSdkCompatStatusCompatible) { + FURI_LOG_E( + TAG, + "Script requests JS SDK %ld.%ld, firmware provides JS SDK %d.%d", + major, + minor, + JS_SDK_MAJOR, + JS_SDK_MINOR); + + const char* message = (status == JsSdkCompatStatusFirmwareTooOld) ? "Outdated Firmware" : + "Outdated Script"; + if(!js_internal_compat_ask_user(message)) { + JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script"); + } + } +} + +static const char* extra_features[] = { + "baseline", // dummy "feature" +}; + +/** + * @brief Determines whether a feature is supported + */ +static bool js_internal_supports(const char* feature) { + for(size_t i = 0; i < COUNT_OF(extra_features); i++) { // -V1008 + if(strcmp(feature, extra_features[i]) == 0) return true; + } + return false; +} + +/** + * @brief Determines whether all of the requested features are supported + */ +static bool js_internal_supports_all_of(struct mjs* mjs, mjs_val_t feature_arr) { + furi_assert(mjs_is_array(feature_arr)); + + for(size_t i = 0; i < mjs_array_length(mjs, feature_arr); i++) { + mjs_val_t feature = mjs_array_get(mjs, feature_arr, i); + const char* feature_str = mjs_get_string(mjs, &feature, NULL); + if(!feature_str) return false; + + if(!js_internal_supports(feature_str)) return false; + } + + return true; +} + +void js_does_sdk_support(struct mjs* mjs) { + mjs_val_t features; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + mjs_return(mjs, mjs_mk_boolean(mjs, js_internal_supports_all_of(mjs, features))); +} + +void js_check_sdk_features(struct mjs* mjs) { + mjs_val_t features; + JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&features)); + if(!js_internal_supports_all_of(mjs, features)) { + FURI_LOG_E(TAG, "Script requests unsupported features"); + + if(!js_internal_compat_ask_user("Unsupported Feature")) { + JS_ERROR_AND_RETURN(mjs, MJS_NOT_IMPLEMENTED_ERROR, "Incompatible script"); + } + } +} diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 788715872..1dfd59521 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -9,6 +9,10 @@ #define PLUGIN_APP_ID "js" #define PLUGIN_API_VERSION 1 +#define JS_SDK_VENDOR "flipperdevices" +#define JS_SDK_MAJOR 0 +#define JS_SDK_MINOR 1 + /** * @brief Returns the foreign pointer in `obj["_"]` */ @@ -275,3 +279,28 @@ mjs_val_t js_module_require(JsModules* modules, const char* name, size_t name_le * @returns Pointer to module context, NULL if the module is not instantiated */ void* js_module_get(JsModules* modules, const char* name); + +/** + * @brief `sdkCompatibilityStatus` function + */ +void js_sdk_compatibility_status(struct mjs* mjs); + +/** + * @brief `isSdkCompatible` function + */ +void js_is_sdk_compatible(struct mjs* mjs); + +/** + * @brief `checkSdkCompatibility` function + */ +void js_check_sdk_compatibility(struct mjs* mjs); + +/** + * @brief `doesSdkSupport` function + */ +void js_does_sdk_support(struct mjs* mjs); + +/** + * @brief `checkSdkFeatures` function + */ +void js_check_sdk_features(struct mjs* mjs); diff --git a/applications/system/js_app/js_thread.c b/applications/system/js_app/js_thread.c index 83f9e604c..b433e9edf 100644 --- a/applications/system/js_app/js_thread.c +++ b/applications/system/js_app/js_thread.c @@ -269,11 +269,48 @@ static int32_t js_thread(void* arg) { mjs_set(mjs, global, "parseInt", ~0, MJS_MK_FN(js_parse_int)); mjs_val_t console_obj = mjs_mk_object(mjs); - mjs_set(mjs, console_obj, "log", ~0, MJS_MK_FN(js_console_log)); - mjs_set(mjs, console_obj, "warn", ~0, MJS_MK_FN(js_console_warn)); - mjs_set(mjs, console_obj, "error", ~0, MJS_MK_FN(js_console_error)); - mjs_set(mjs, console_obj, "debug", ~0, MJS_MK_FN(js_console_debug)); - mjs_set(mjs, global, "console", ~0, console_obj); + + if(worker->path) { + FuriString* dirpath = furi_string_alloc(); + path_extract_dirname(furi_string_get_cstr(worker->path), dirpath); + mjs_set( + mjs, + global, + "__filename", + ~0, + mjs_mk_string( + mjs, furi_string_get_cstr(worker->path), furi_string_size(worker->path), true)); + mjs_set( + mjs, + global, + "__dirname", + ~0, + mjs_mk_string(mjs, furi_string_get_cstr(dirpath), furi_string_size(dirpath), true)); + furi_string_free(dirpath); + } + + JS_ASSIGN_MULTI(mjs, global) { + JS_FIELD("print", MJS_MK_FN(js_print)); + JS_FIELD("delay", MJS_MK_FN(js_delay)); + JS_FIELD("toString", MJS_MK_FN(js_global_to_string)); + JS_FIELD("parseInt", MJS_MK_FN(js_parse_int)); + JS_FIELD("ffi_address", MJS_MK_FN(js_ffi_address)); + JS_FIELD("require", MJS_MK_FN(js_require)); + JS_FIELD("console", console_obj); + + JS_FIELD("sdkCompatibilityStatus", MJS_MK_FN(js_sdk_compatibility_status)); + JS_FIELD("isSdkCompatible", MJS_MK_FN(js_is_sdk_compatible)); + JS_FIELD("checkSdkCompatibility", MJS_MK_FN(js_check_sdk_compatibility)); + JS_FIELD("doesSdkSupport", MJS_MK_FN(js_does_sdk_support)); + JS_FIELD("checkSdkFeatures", MJS_MK_FN(js_check_sdk_features)); + } + + JS_ASSIGN_MULTI(mjs, console_obj) { + JS_FIELD("log", MJS_MK_FN(js_console_log)); + JS_FIELD("warn", MJS_MK_FN(js_console_warn)); + JS_FIELD("error", MJS_MK_FN(js_console_error)); + JS_FIELD("debug", MJS_MK_FN(js_console_debug)); + } mjs_set_ffi_resolver(mjs, js_dlsym, worker->resolver); diff --git a/applications/system/js_app/modules/js_badusb.c b/applications/system/js_app/modules/js_badusb.c index 7a42d0a3a..daf744843 100644 --- a/applications/system/js_app/modules/js_badusb.c +++ b/applications/system/js_app/modules/js_badusb.c @@ -204,6 +204,22 @@ static void js_badusb_quit(struct mjs* mjs) { mjs_return(mjs, MJS_UNDEFINED); } +static void js_badusb_quit(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst); + furi_assert(badusb); + + if(badusb->usb_if_prev == NULL) { + mjs_prepend_errorf(mjs, MJS_INTERNAL_ERROR, "HID is not started"); + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + js_badusb_quit_free(badusb); + + mjs_return(mjs, MJS_UNDEFINED); +} + static void js_badusb_is_connected(struct mjs* mjs) { mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); JsBadusbInst* badusb = mjs_get_ptr(mjs, obj_inst); 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 index c4f0d1bee..7f45c1a0f 100644 --- 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 @@ -80,7 +80,7 @@ static void js_event_loop_callback_generic(void* param) { /** * @brief Handles non-timer events */ -static bool js_event_loop_callback(void* object, void* param) { +static void js_event_loop_callback(void* object, void* param) { JsEventLoopCallbackContext* context = param; if(context->transformer) { @@ -102,8 +102,6 @@ static bool js_event_loop_callback(void* object, void* param) { } js_event_loop_callback_generic(param); - - return true; } /** diff --git a/applications/system/js_app/modules/js_flipper.c b/applications/system/js_app/modules/js_flipper.c index 43c675e10..eeaa2c8a0 100644 --- a/applications/system/js_app/modules/js_flipper.c +++ b/applications/system/js_app/modules/js_flipper.c @@ -27,11 +27,19 @@ static void js_flipper_get_battery(struct mjs* mjs) { void* js_flipper_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { UNUSED(modules); + mjs_val_t sdk_vsn = mjs_mk_array(mjs); + mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MAJOR)); + mjs_array_push(mjs, sdk_vsn, mjs_mk_number(mjs, JS_SDK_MINOR)); + 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)); - mjs_set(mjs, flipper_obj, "getBatteryCharge", ~0, MJS_MK_FN(js_flipper_get_battery)); *object = flipper_obj; + JS_ASSIGN_MULTI(mjs, flipper_obj) { + JS_FIELD("getModel", MJS_MK_FN(js_flipper_get_model)); + JS_FIELD("getName", MJS_MK_FN(js_flipper_get_name)); + JS_FIELD("getBatteryCharge", MJS_MK_FN(js_flipper_get_battery)); + JS_FIELD("firmwareVendor", mjs_mk_string(mjs, JS_SDK_VENDOR, ~0, false)); + JS_FIELD("jsSdkVersion", sdk_vsn); + } return (void*)1; } diff --git a/applications/system/js_app/modules/js_gpio.c b/applications/system/js_app/modules/js_gpio.c index d2d65da4d..ae3fefd71 100644 --- a/applications/system/js_app/modules/js_gpio.c +++ b/applications/system/js_app/modules/js_gpio.c @@ -220,7 +220,7 @@ static void js_gpio_interrupt(struct mjs* mjs) { * let gpio = require("gpio"); * let pot = gpio.get("pc0"); * pot.init({ direction: "in", inMode: "analog" }); - * print("voltage:" pot.read_analog(), "mV"); + * print("voltage:" pot.readAnalog(), "mV"); * ``` */ static void js_gpio_read_analog(struct mjs* mjs) { @@ -273,7 +273,7 @@ static void js_gpio_get(struct mjs* mjs) { 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, "readAnalog", ~0, MJS_MK_FN(js_gpio_read_analog)); mjs_set(mjs, manager, "interrupt", ~0, MJS_MK_FN(js_gpio_interrupt)); mjs_return(mjs, manager); diff --git a/applications/system/js_app/modules/js_gui/file_picker.c b/applications/system/js_app/modules/js_gui/file_picker.c index b9736ba35..49cf5e89d 100644 --- a/applications/system/js_app/modules/js_gui/file_picker.c +++ b/applications/system/js_app/modules/js_gui/file_picker.c @@ -1,17 +1,6 @@ #include "../../js_modules.h" #include - -// File icon -#include -static const uint8_t _I_file_10px_0[] = { - 0x00, 0x7f, 0x00, 0xa1, 0x00, 0x2d, 0x01, 0xe1, 0x01, 0x0d, 0x01, - 0x01, 0x01, 0x7d, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0x01, -}; -static const uint8_t* const _I_file_10px[] = {_I_file_10px_0}; - -static const Icon I_file_10px = - {.width = 10, .height = 10, .frame_count = 1, .frame_rate = 0, .frames = _I_file_10px}; -// File icon end +#include static void js_gui_file_picker_pick_file(struct mjs* mjs) { const char *base_path, *extension; diff --git a/applications/system/js_app/modules/js_gui/text_input.c b/applications/system/js_app/modules/js_gui/text_input.c index e93bbfad0..bb3a6cfa3 100644 --- a/applications/system/js_app/modules/js_gui/text_input.c +++ b/applications/system/js_app/modules/js_gui/text_input.c @@ -85,7 +85,7 @@ static bool default_text_assign( context->buffer = realloc(context->buffer, context->buffer_size); //-V701 } // Also trim excess previous data with strlcpy() - strlcpy(context->buffer, value.string, context->buffer_size); + strlcpy(context->buffer, value.string, context->buffer_size); //-V575 text_input_set_result_callback( input, (TextInputCallback)input_callback, diff --git a/applications/system/js_app/packages/create-fz-app/README.md b/applications/system/js_app/packages/create-fz-app/README.md new file mode 100644 index 000000000..cf6ddbc91 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/README.md @@ -0,0 +1,20 @@ +# Flipper Zero JavaScript SDK Wizard +This package contains an interactive wizard that lets you scaffold a JavaScript +application for Flipper Zero. + +## Getting started +Create your application using the interactive wizard: +```shell +npx @flipperdevices/create-fz-app@latest +``` + +Then, enter the directory with your application and launch it: +```shell +cd my-flip-app +npm start +``` + +You are free to use `pnpm` or `yarn` instead of `npm`. + +## Documentation +Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html) diff --git a/applications/system/js_app/packages/create-fz-app/index.js b/applications/system/js_app/packages/create-fz-app/index.js new file mode 100755 index 000000000..0bfe9376e --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/index.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +import prompts from "prompts"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "url"; +import { spawnSync } from "node:child_process"; +import { replaceInFileSync } from "replace-in-file"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +(async () => { + const { name, pkgManager, confirm } = await prompts([ + { + type: "text", + name: "name", + message: "What is the name of your project?", + initial: "my-flip-app" + }, + { + type: "select", + name: "pkgManager", + message: "What package manager should your project use?", + choices: [ + { title: "npm", value: "npm" }, + { title: "pnpm", value: "pnpm" }, + { title: "yarn", value: "yarn" }, + ], + }, + { + type: "confirm", + name: "confirm", + message: "Create project?", + initial: true, + }, + ]); + + if (!confirm) + return; + + if (fs.existsSync(name)) { + const { replace } = await prompts([ + { + type: "confirm", + name: "replace", + message: `File or directory \`${name}\` already exists. Continue anyway?`, + initial: false, + }, + ]); + if (!replace) + return; + } + + fs.rmSync(name, { recursive: true, force: true }); + + console.log("Copying files..."); + fs.cpSync(path.resolve(__dirname, "template"), name, { recursive: true }); + replaceInFileSync({ files: `${name}/**/*`, from: //g, to: name }); + + console.log("Installing packages..."); + spawnSync("bash", ["-c", `cd ${name} && ${pkgManager} install`], { + cwd: process.cwd(), + detached: true, + stdio: "inherit", + }); + + console.log(`Done! Created ${name}. Run \`cd ${name} && ${pkgManager} start\` to run it on your Flipper.`); +})(); diff --git a/applications/system/js_app/packages/create-fz-app/package.json b/applications/system/js_app/packages/create-fz-app/package.json new file mode 100644 index 000000000..216423396 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "@flipperdevices/create-fz-app", + "version": "0.1.0", + "description": "Template package for JS apps Flipper Zero", + "bin": "index.js", + "type": "module", + "keywords": [ + "flipper", + "flipper zero" + ], + "author": "Flipper Devices", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git+https://github.com/flipperdevices/flipperzero-firmware.git", + "directory": "applications/system/js_app/packages/create-fz-app" + }, + "dependencies": { + "prompts": "^2.4.2", + "replace-in-file": "^8.2.0" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml b/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml new file mode 100644 index 000000000..58f20a385 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/pnpm-lock.yaml @@ -0,0 +1,373 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 + replace-in-file: + specifier: ^8.2.0 + version: 8.2.0 + +packages: + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + replace-in-file@8.2.0: + resolution: {integrity: sha512-hMsQtdYHwWviQT5ZbNsgfu0WuCiNlcUSnnD+aHAL081kbU9dPkPocDaHlDvAHKydTWWpx1apfcEcmvIyQk3CpQ==} + engines: {node: '>=18'} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + chalk@5.3.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + escalade@3.2.0: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + get-caller-file@2.0.5: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + is-fullwidth-code-point@3.0.0: {} + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + kleur@3.0.3: {} + + lru-cache@10.4.3: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + package-json-from-dist@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + replace-in-file@8.2.0: + dependencies: + chalk: 5.3.0 + glob: 10.4.5 + yargs: 17.7.2 + + require-directory@2.1.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/applications/system/js_app/packages/create-fz-app/template/.gitignore b/applications/system/js_app/packages/create-fz-app/template/.gitignore new file mode 100644 index 000000000..aa57f8d03 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/.gitignore @@ -0,0 +1,2 @@ +/dist +node_modules/ diff --git a/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 b/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 new file mode 100644 index 000000000..e545841c5 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/fz-sdk.config.json5 @@ -0,0 +1,23 @@ +{ + build: { + // Where to put the compiled file + output: "dist/.js", + + // Whether to reduce the final file size at the cost of readability and + // clarity of error messages + minify: false, + + // Set this to `false` if you've thoroughly read the documentation and + // are sure that you can use manual version checks to your advantage + enforceSdkVersion: true, + }, + + upload: { + // Where to grab the file from. If you're not doing any extra processing + // after the SDK, this should match `build.output` + input: "dist/.js", + + // Where to put the file on the device + output: "/ext/apps/Scripts/.js", + }, +} diff --git a/applications/system/js_app/packages/create-fz-app/template/index.ts b/applications/system/js_app/packages/create-fz-app/template/index.ts new file mode 100644 index 000000000..6291e3e13 --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/index.ts @@ -0,0 +1,30 @@ +// import modules +// caution: `eventLoop` HAS to be imported before `gui`, and `gui` HAS to be +// imported before any `gui` submodules. +import * as eventLoop from "@flipperdevices/fz-sdk/event_loop"; +import * as gui from "@flipperdevices/fz-sdk/gui"; +import * as dialog from "@flipperdevices/fz-sdk/gui/dialog"; + +// a common pattern is to declare all the views that your app uses on one object +const views = { + dialog: dialog.makeWith({ + header: "Hello from ", + text: "Check out index.ts and\nchange something :)", + center: "Gonna do that!", + }), +}; + +// stop app on center button press +eventLoop.subscribe(views.dialog.input, (_sub, button, eventLoop) => { + if (button === "center") + eventLoop.stop(); +}, eventLoop); + +// stop app on back button press +eventLoop.subscribe(gui.viewDispatcher.navigation, (_sub, _item, eventLoop) => { + eventLoop.stop(); +}, eventLoop); + +// run app +gui.viewDispatcher.switchTo(views.dialog); +eventLoop.run(); diff --git a/applications/system/js_app/packages/create-fz-app/template/package.json b/applications/system/js_app/packages/create-fz-app/template/package.json new file mode 100644 index 000000000..7acdeccaa --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/package.json @@ -0,0 +1,12 @@ +{ + "name": "", + "version": "1.0.0", + "scripts": { + "build": "tsc && node node_modules/@flipperdevices/fz-sdk/sdk.js build", + "start": "npm run build && node node_modules/@flipperdevices/fz-sdk/sdk.js upload" + }, + "devDependencies": { + "@flipperdevices/fz-sdk": "^0.1", + "typescript": "^5.6.3" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/create-fz-app/template/tsconfig.json b/applications/system/js_app/packages/create-fz-app/template/tsconfig.json new file mode 100644 index 000000000..c7b83cd5d --- /dev/null +++ b/applications/system/js_app/packages/create-fz-app/template/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "outDir": "dist", + "checkJs": true, + "module": "CommonJS", + "noLib": true, + "target": "ES2015", + }, + "files": [ + "./node_modules/@flipperdevices/fz-sdk/global.d.ts", + ], + "include": [ + "./**/*.ts", + "./**/*.js" + ], + "exclude": [ + "./node_modules/**/*", + "dist/**/*", + ], +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/.gitignore b/applications/system/js_app/packages/fz-sdk/.gitignore new file mode 100644 index 000000000..77f12ae2e --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/.gitignore @@ -0,0 +1 @@ +docs/ diff --git a/applications/system/js_app/packages/fz-sdk/README.md b/applications/system/js_app/packages/fz-sdk/README.md new file mode 100644 index 000000000..3234f68aa --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/README.md @@ -0,0 +1,31 @@ +# Flipper Zero JavaScript SDK +This package contains official tooling and typings for developing Flipper Zero +applications in JavaScript. + +## Getting started +Create your application using the interactive wizard: +```shell +npx @flipperdevices/create-fz-app@latest +``` + +Then, enter the directory with your application and launch it: +```shell +cd my-flip-app +npm start +``` + +You are free to use `pnpm` or `yarn` instead of `npm`. + +## Versioning +For each version of this package, the major and minor components match those of +the Flipper Zero JS SDK version that that package version targets. This version +follows semver. For example, apps compiled with SDK version `0.1.0` will be +compatible with SDK versions `0.1`...`1.0` (not including `1.0`). + +Every API has a version history reflected in its JSDoc comment. It is heavily +recommended to check SDK compatibility using a combination of +`sdkCompatibilityStatus`, `isSdkCompatible`, `assertSdkCompatibility` depending +on your use case. + +## Documentation +Check out the [JavaScript section in the Developer Documentation](https://developer.flipper.net/flipperzero/doxygen/js.html) diff --git a/applications/system/js_app/types/badusb/index.d.ts b/applications/system/js_app/packages/fz-sdk/badusb/index.d.ts similarity index 90% rename from applications/system/js_app/types/badusb/index.d.ts rename to applications/system/js_app/packages/fz-sdk/badusb/index.d.ts index 57c2662cd..3eca288c2 100644 --- a/applications/system/js_app/types/badusb/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/badusb/index.d.ts @@ -1,8 +1,10 @@ /** * @brief Special key codes that this module recognizes + * @version Added in JS SDK 0.1 */ export type ModifierKey = "CTRL" | "SHIFT" | "ALT" | "GUI"; +/** @version Added in JS SDK 0.1 */ export type MainKey = "DOWN" | "LEFT" | "RIGHT" | "UP" | @@ -31,6 +33,7 @@ export type MainKey = "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"; +/** @version Added in JS SDK 0.1 */ export type KeyCode = MainKey | ModifierKey | number; /** @@ -39,11 +42,13 @@ export type KeyCode = MainKey | ModifierKey | number; * Automatically unlocks USB profile, so qFlipper connection will be interrupted. * * @param settings USB device settings. Omit to select default parameters + * @version Added in JS SDK 0.1 */ export declare function setup(settings?: { vid: number, pid: number, mfrName?: string, prodName?: string, layoutPath?: string }): void; /** * @brief Tells whether the virtual USB HID device has successfully connected + * @version Added in JS SDK 0.1 */ export declare function isConnected(): boolean; @@ -52,6 +57,7 @@ export declare function isConnected(): boolean; * @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`). + * @version Added in JS SDK 0.1 */ export declare function press(...keys: KeyCode[]): void; @@ -60,6 +66,7 @@ export declare function press(...keys: KeyCode[]): void; * @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`). + * @version Added in JS SDK 0.1 */ export declare function hold(...keys: KeyCode[]): void; @@ -68,6 +75,7 @@ export declare function hold(...keys: KeyCode[]): void; * @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`). + * @version Added in JS SDK 0.1 */ export declare function release(...keys: KeyCode[]): void; @@ -75,6 +83,7 @@ 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 + * @version Added in JS SDK 0.1 */ export declare function print(string: string, delay?: number): void; @@ -83,6 +92,7 @@ export declare function print(string: string, delay?: number): void; * "Enter" after printing the string * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function println(string: string, delay?: number): void; @@ -90,6 +100,7 @@ export declare function println(string: string, delay?: number): void; * @brief Prints a string by Alt+Numpad method - works only on Windows! * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function altPrint(string: string, delay?: number): void; @@ -98,10 +109,12 @@ export declare function altPrint(string: string, delay?: number): void; * Presses "Enter" after printing the string * @param string The string to print * @param delay How many milliseconds to wait between key presses + * @version Added in JS SDK 0.1 */ export declare function altPrintln(string: string, delay?: number): void; /** * @brief Releases usb, optional, but allows to switch usb profile + * @version Added in JS SDK 0.1 */ export declare function quit(): void; diff --git a/applications/system/js_app/packages/fz-sdk/docs_readme.md b/applications/system/js_app/packages/fz-sdk/docs_readme.md new file mode 100644 index 000000000..f82f58ec7 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/docs_readme.md @@ -0,0 +1 @@ +# Welcome diff --git a/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts b/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts new file mode 100644 index 000000000..001518f87 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/event_loop/index.d.ts @@ -0,0 +1,182 @@ +/** + * Module for dealing with events + * + * ```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`. + * + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @ignore + */ +type Lit = undefined | null | {}; + +/** + * Subscription control interface + * @version Added in JS SDK 0.1 + */ +export interface Subscription { + /** + * Cancels the subscription, preventing any future events managed by the + * subscription from firing + * @version Added in JS SDK 0.1 + */ + cancel(): void; +} + +/** + * Opaque event source identifier + * @version Added in JS SDK 0.1 + */ +export type Contract = symbol & { "__tag__": "contract" }; +// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist. + +/** + * 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. + * @version Added in JS SDK 0.1 + */ +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 + * @version Added in JS SDK 0.1 + */ +export function subscribe(contract: Contract, callback: Callback, ...args: Args): Subscription; +/** + * Runs the event loop until it is stopped (potentially never) + * @version Added in JS SDK 0.1 + */ +export function run(): void | never; +/** + * Stops the event loop + * @version Added in JS SDK 0.1 + */ +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 + * @version Added in JS SDK 0.1 + */ +export function timer(mode: "oneshot" | "periodic", interval: number): Contract; + +/** + * Message queue + * @version Added in JS SDK 0.1 + */ +export declare class Queue { + /** + * Message event + * @version Added in JS SDK 0.1 + */ + input: Contract; + /** + * Sends a message to the queue + * @param message message to send + * @version Added in JS SDK 0.1 + */ + send(message: T): void; +} + +/** + * Creates a message queue + * @param length maximum queue capacity + * @version Added in JS SDK 0.1 + */ +export function queue(length: number): Queue; diff --git a/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts b/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts new file mode 100644 index 000000000..2dac4204b --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/flipper/index.d.ts @@ -0,0 +1,41 @@ +/** + * Module for querying device properties + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @brief Returns the device model + * @version Added in JS SDK 0.1 + */ +export declare function getModel(): string; + +/** + * @brief Returns the name of the virtual dolphin + * @version Added in JS SDK 0.1 + */ +export declare function getName(): string; + +/** + * @brief Returns the battery charge percentage + * @version Added in JS SDK 0.1 + */ +export declare function getBatteryCharge(): number; + +/** + * @warning Do **NOT** use this to check the presence or absence of features. If + * you do, I'm gonna be sad :( Instead, refer to `checkSdkFeatures` and + * other similar mechanisms. + * @note Original firmware reports `"flipperdevices"`. + * @version Added in JS SDK 0.1 + */ +export declare const firmwareVendor: string; + +/** + * @warning Do **NOT** use this to check the presence or absence of features. If + * you do, I'm gonna be sad :( Instead, refer to + * `checkSdkCompatibility` and other similar mechanisms. + * @note You're looking at JS SDK 0.1 + * @version Added in JS SDK 0.1 + */ +export declare const jsSdkVersion: [number, number]; diff --git a/applications/system/js_app/packages/fz-sdk/global.d.ts b/applications/system/js_app/packages/fz-sdk/global.d.ts new file mode 100644 index 000000000..953afc30d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/global.d.ts @@ -0,0 +1,367 @@ +/** + * Things from this module are automatically available to you without having to + * explicitly import anything. + * + * # SDK versioning and features + * + * ## Motivation + * It is very important that you check that features are implemented before you + * use them. By adding the necessary checks, you ensure that your users get a + * clear warning instead of a cryptic error message when running the script. + * + * This system has been designed in collaboration with our community in order to + * make things better for everybody involved. You can find out more in this + * discussion: https://github.com/flipperdevices/flipperzero-firmware/pull/3961 + * + * ## Community agreement + * Each interpreter implementation (aka "JS SDK", aka "JS API"), including + * those found in third-party firmware distributions, defines two markers for + * signaling what it supports: the **SDK version** and the + * **extra feature set**. + * + * The **SDK version** consists of two semver-like integer components: the major + * version and the minor version. Like semver, the major version is bumped when + * a breaking change is introduced (i.e. one that would require correction of + * apps by their developers), and the minor version is bumped when a new + * non-breaking feature is introduced. Because we have adopted TypeScript, + * the https://www.semver-ts.org/ standard is used to determine whether a change + * is breaking or not. The basis of `semver-ts` is the "no new red squiggles" + * rule. + * + * Every major version is associated with a set of **extra features** that are + * present in some firmware distributions but not others. Distributions may + * cross-port features between each other, until at some point they get ported + * into the upstream firmware distribution. With the next major release of the + * JS SDK, all extra features present in the upstream distribution are now + * declared **baseline features**, and thus no longer recognized as "extra + * features". + * + * Before using a feature, you must check that the interpreter that you're + * running on actually supports it. If you don't, the portability of your + * application will suffer. + * + * ## Implementation + * Use the following functions to check version compatibility: + * - `checkSdkCompatibility` when your script absolutely cannot function on an + * incompatible interpreter + * - `isSdkCompatible` when your script can leverage multiple interpreter + * editions to its advantage + * - `sdkCompatibilityStatus` when you need a detailed status on compatibility + * + * Use the following functions to check feature compatibility: + * - `checkSdkFeatures` when your script absolutely cannot function on an + * incompatible interpreter + * - `doesSdkSupport` when your script can leverage multiple interpreter + * editions to its advantage + * + * ## Automatic version enforcement + * The SDK will automatically insert a call to `checkSdkCompatibility` in the + * beginning of the resulting script. If you would like to disable this check + * and instead use other manual compatibility checking facilities, edit your + * `fz-sdk.config.json5`. + * + * # Standard library + * Standard library features are mostly unimplemented. This module defines, + * among other things, the features that _are_ implemented. + * + * @version Added in JS SDK 0.1 + * @module + */ + +/** + * @brief Checks compatibility between the script and the JS SDK that the + * firmware provides + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @returns Compatibility status: + * - `"compatible"` if the script and the JS SDK are compatible + * - `"firmwareTooOld"` if the expected major version is larger than the + * version of the firmware, or if the expected minor version is larger than + * the version of the firmware + * - `"firmwareTooNew"` if the expected major version is lower than the + * version of the firmware + * @version Added in JS SDK 0.1 + */ +declare function sdkCompatibilityStatus(expectedMajor: number, expectedMinor: number): + "compatible" | "firmwareTooOld" | "firmwareTooNew"; + +/** + * @brief Checks compatibility between the script and the JS SDK that the + * firmware provides in a boolean fashion + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @returns `true` if the two are compatible, `false` otherwise + * @version Added in JS SDK 0.1 + */ +declare function isSdkCompatible(expectedMajor: number, expectedMinor: number): boolean; + +/** + * @brief Asks the user whether to continue executing the script if the versions + * are not compatible. Does nothing if they are. + * + * @note You're looking at JS SDK v0.1 + * + * @param expectedMajor JS SDK major version expected by the script + * @param expectedMinor JS SDK minor version expected by the script + * @version Added in JS SDK 0.1 + */ +declare function checkSdkCompatibility(expectedMajor: number, expectedMinor: number): void | never; + +/** + * @brief Checks whether all of the specified extra features are supported by + * the interpreter. + * @warning This function will return `false` if a queried feature is now + * recognized as a baseline feature. For more info, consult the module + * documentation. + * @param features Array of named features to query + */ +declare function doesSdkSupport(features: string[]): boolean; + +/** + * @brief Checks whether all of the specified extra features are supported by + * the interpreter, asking the user if they want to continue running the + * script if they're not. + * @warning This function will act as if the feature is not implemented for + * features that are now recognized as baseline features. For more + * info, consult the module documentation. + * @param features Array of named features to query + */ +declare function checkSdkFeatures(features: string[]): void | never; + +/** + * @brief Pauses JavaScript execution for a while + * @param ms How many milliseconds to pause the execution for + * @version Added in JS SDK 0.1 + */ +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 + * @version Added in JS SDK 0.1 + */ +declare function print(...args: any[]): void; + +/** + * @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 + * @version Added in JS SDK 0.1 + */ +declare function load(path: string): any; + +/** + * @brief Loads a natively implemented module + * @param module The name of the module to load + * @version Added in JS SDK 0.1 + */ +declare function require(module: 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. + * + * @version Added in JS SDK 0.1 + */ +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 + * @version Added in JS SDK 0.1 + */ +declare class ArrayBuffer { + /** + * @brief The pointer to the byte buffer + * @note Like other `RawPointer` values, this value is essentially useless + * to JS code. + * @version Added in JS SDK 0.1 + */ + getPtr: RawPointer; + /** + * @brief The length of the buffer in bytes + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + byteLength: number; + /** + * @brief The length of the buffer in typed elements + * @version Added in JS SDK 0.1 + */ + length: number; + /** + * @brief The underlying `ArrayBuffer` + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + 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 + * @version Added in JS SDK 0.1 + */ + push(value: T): number; + /** + * @brief How many elements there are in the array + * @version Added in JS SDK 0.1 + */ + length: number; +} + +declare class String { + /** + * @brief How many characters there are in the string + * @version Added in JS SDK 0.1 + */ + length: number; + /** + * @brief Returns the character code at an index in the string + * @param index The index to consult + * @version Added in JS SDK 0.1 + */ + charCodeAt(index: number): number; + /** + * See `charCodeAt` + * @version Added in JS SDK 0.1 + */ + at(index: number): number; + /** + * @brief Return index of first occurrence of substr within the string or `-1` if not found + * @param substr The string to search for + * @param fromIndex The index to start searching from + * @version Added in JS SDK 0.1 + */ + indexOf(substr: string, fromIndex?: number): number; + /** + * @brief Return a substring between two indices + * @param start The index to start substring at + * @param end The index to end substring at + * @version Added in JS SDK 0.1 + */ + slice(start: number, end?: number): string; + /** + * @brief Return this string transformed to upper case + * @version Added in JS SDK 0.1 + */ + toUpperCase(): string; + /** + * @brief Return this string transformed to lower case + * @version Added in JS SDK 0.1 + */ + toLowerCase(): string; +} + +declare class Boolean { } + +declare class Function { } + +declare class Number { + /** + * @brief Converts this number to a string + * @param base Integer base (`2`...`16`), default: 10 + * @version Added in JS SDK 0.1 + */ + toString(base?: number): string; +} + +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/packages/fz-sdk/gpio/index.d.ts similarity index 59% rename from applications/system/js_app/types/gpio/index.d.ts rename to applications/system/js_app/packages/fz-sdk/gpio/index.d.ts index 18705f898..b484ebbf6 100644 --- a/applications/system/js_app/types/gpio/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/gpio/index.d.ts @@ -1,5 +1,37 @@ +/** + * Module for accessing the GPIO (General Purpose Input/Output) ports + * + * ```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); + * ``` + * + * @version Added in JS SDK 0.1 + * @module + */ + import type { Contract } from "../event_loop"; +/** + * @version Added in JS SDK 0.1 + */ export interface Mode { direction: "in" | "out"; outMode?: "push_pull" | "open_drain"; @@ -8,31 +40,39 @@ export interface Mode { pull?: "up" | "down"; } +/** + * @version Added in JS SDK 0.1 + */ export interface Pin { /** * Configures a pin. This may be done several times. * @param mode Pin configuration object + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ write(value: boolean): void; /** * Gets the input value of a pin if it's been configured with * `direction: "in"`, but not `inMode: "analog"`. + * @version Added in JS SDK 0.1 */ read(): boolean; /** * Gets the input voltage of a pin in millivolts if it's been configured * with `direction: "in"` and `inMode: "analog"` + * @version Added in JS SDK 0.1 */ - read_analog(): number; + readAnalog(): number; /** * Returns an `event_loop` event that can be used to listen to interrupts, * as configured by `init` + * @version Added in JS SDK 0.1 */ interrupt(): Contract; } @@ -41,5 +81,6 @@ export interface Pin { * 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`) + * @version Added in JS SDK 0.1 */ export function get(pin: string | number): Pin; diff --git a/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts new file mode 100644 index 000000000..5556e7fbb --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/byte_input.d.ts @@ -0,0 +1,41 @@ +/** + * Displays a byte input keyboard. + * + * Sample screenshot of the view + * + * ```js + * let eventLoop = require("event_loop"); + * let gui = require("gui"); + * let byteInputView = require("gui/byte_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 + * - `header`: Text displayed at the top of the screen + * - `length`: Length of data to edit + * - `defaultData`: Data to show by default + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + length: number, + defaultData: Uint8Array | ArrayBuffer, +} +declare class ByteInput extends View { + input: Contract; +} +declare class ByteInputFactory extends ViewFactory { } +declare const factory: ByteInputFactory; +export = factory; diff --git a/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts new file mode 100644 index 000000000..9bd0c3966 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/dialog.d.ts @@ -0,0 +1,45 @@ +/** + * 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 displayed in bold at the top of the screen + * - `text`: Text displayed in the middle of the string + * - `left`: Text for the left button + * - `center`: Text for the center button + * - `right`: Text for the right button + * + * @version Added in JS SDK 0.1 + * @module + */ + +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/packages/fz-sdk/gui/empty_screen.d.ts b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts new file mode 100644 index 000000000..49e591426 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/empty_screen.d.ts @@ -0,0 +1,32 @@ +/** + * 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. + * + * @version Added in JS SDK 0.1 + * @module + */ + +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/packages/fz-sdk/gui/file_picker.d.ts b/applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts new file mode 100644 index 000000000..9447f89a9 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/file_picker.d.ts @@ -0,0 +1,7 @@ +/** + * @brief Displays a file picker and returns the selected file, or undefined if cancelled + * @param basePath The path to start at + * @param extension The file extension(s) to show (like `.sub`, `.iso|.img`, `*`) + * @version Added in JS SDK 0.1 + */ +export declare function pickFile(basePath: string, extension: string): string | undefined; diff --git a/applications/system/js_app/packages/fz-sdk/gui/index.d.ts b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts new file mode 100644 index 000000000..3184a5718 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/index.d.ts @@ -0,0 +1,171 @@ +/** + * ```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(); + * ``` + * + * @version Added in JS SDK 0.1 + * @module + */ + +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; +} + +/** + * @version Added in JS SDK 0.1 + */ +declare class ViewDispatcher { + /** + * Event source for `sendCustom` events + * @version Added in JS SDK 0.1 + */ + custom: Contract; + /** + * Event source for navigation events (back key presses) + * @version Added in JS SDK 0.1 + */ + navigation: Contract; + /** + * Sends a number to the custom event handler + * @param event number to send + * @version Added in JS SDK 0.1 + */ + sendCustom(event: number): void; + /** + * Switches to a view + * @param assoc View-ViewDispatcher association as returned by `add` + * @version Added in JS SDK 0.1 + */ + 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"` + * @version Added in JS SDK 0.1 + */ + sendTo(direction: "front" | "back"): void; +} + +/** + * @version Added in JS SDK 0.1 + */ +export const viewDispatcher: ViewDispatcher; diff --git a/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts new file mode 100644 index 000000000..b8b10c43a --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/loading.d.ts @@ -0,0 +1,33 @@ +/** + * 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. + * + * @version Added in JS SDK 0.1 + * @module + */ + +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/packages/fz-sdk/gui/submenu.d.ts b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts new file mode 100644 index 000000000..31e08aab8 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/submenu.d.ts @@ -0,0 +1,39 @@ +/** + * 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`: Text displayed at the top of the screen in bold + * - `items`: Array of selectable textual items + * + * @version Added in JS SDK 0.1 + * @module + */ + +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/packages/fz-sdk/gui/text_box.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts new file mode 100644 index 000000000..a46ec73fa --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/text_box.d.ts @@ -0,0 +1,41 @@ +/** + * 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 in the text box + * - `font`: The font to display the text in (`"text"` or `"hex"`) + * - `focus`: The initial focus of the text box (`"start"` or `"end"`) + * + * @version Added in JS SDK 0.1 + * @module + */ + +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/packages/fz-sdk/gui/text_input.d.ts b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts new file mode 100644 index 000000000..5d64b038b --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/gui/text_input.d.ts @@ -0,0 +1,45 @@ +/** + * Displays a text input 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 + * - `header`: Text displayed at the top of the screen + * - `minLength`: Minimum allowed text length + * - `maxLength`: Maximum allowed text length + * - `defaultText`: Text to show by default + * - `defaultTextClear`: Whether to clear the default text on next character typed + * + * @version Added in JS SDK 0.1 + * @module + */ + +import type { View, ViewFactory } from "."; +import type { Contract } from "../event_loop"; + +type Props = { + header: string, + minLength: number, + maxLength: number, + defaultText: string, + defaultTextClear: boolean, +} +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/packages/fz-sdk/math/index.d.ts similarity index 52% rename from applications/system/js_app/types/math/index.d.ts rename to applications/system/js_app/packages/fz-sdk/math/index.d.ts index 4924eea7e..67c805db5 100644 --- a/applications/system/js_app/types/math/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/math/index.d.ts @@ -1,27 +1,60 @@ +/** + * Math operations + * @version Added in JS SDK 0.1 + * @module + */ + +/** @version Added in JS SDK 0.1 */ export function isEqual(a: number, b: number, tolerance: number): boolean; +/** @version Added in JS SDK 0.1 */ export function abs(n: number): number; +/** @version Added in JS SDK 0.1 */ export function acos(n: number): number; +/** @version Added in JS SDK 0.1 */ export function acosh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function asin(n: number): number; +/** @version Added in JS SDK 0.1 */ export function asinh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function atan(n: number): number; +/** @version Added in JS SDK 0.1 */ export function atan2(a: number, b: number): number; +/** @version Added in JS SDK 0.1 */ export function atanh(n: number): number; +/** @version Added in JS SDK 0.1 */ export function cbrt(n: number): number; +/** @version Added in JS SDK 0.1 */ export function ceil(n: number): number; +/** @version Added in JS SDK 0.1 */ export function clz32(n: number): number; +/** @version Added in JS SDK 0.1 */ export function cos(n: number): number; +/** @version Added in JS SDK 0.1 */ export function exp(n: number): number; +/** @version Added in JS SDK 0.1 */ export function floor(n: number): number; +/** @version Added in JS SDK 0.1 */ export function log(n: number): number; +/** @version Added in JS SDK 0.1 */ export function max(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function min(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function pow(n: number, m: number): number; +/** @version Added in JS SDK 0.1 */ export function random(): number; +/** @version Added in JS SDK 0.1 */ export function sign(n: number): number; +/** @version Added in JS SDK 0.1 */ export function sin(n: number): number; +/** @version Added in JS SDK 0.1 */ export function sqrt(n: number): number; +/** @version Added in JS SDK 0.1 */ export function trunc(n: number): number; +/** @version Added in JS SDK 0.1 */ declare const PI: number; +/** @version Added in JS SDK 0.1 */ declare const E: number; +/** @version Added in JS SDK 0.1 */ declare const EPSILON: number; diff --git a/applications/system/js_app/types/notification/index.d.ts b/applications/system/js_app/packages/fz-sdk/notification/index.d.ts similarity index 75% rename from applications/system/js_app/types/notification/index.d.ts rename to applications/system/js_app/packages/fz-sdk/notification/index.d.ts index 947daba21..2199a1479 100644 --- a/applications/system/js_app/types/notification/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/notification/index.d.ts @@ -1,6 +1,13 @@ +/** + * Module for using the color LED and vibration motor + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Signals success to the user via the color LED, speaker and vibration * motor + * @version Added in JS SDK 0.1 */ export declare function success(): void; @@ -10,11 +17,15 @@ export declare function success(): void; */ export declare function error(): void; +/** + * @version Added in JS SDK 0.1 + */ 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) + * @version Added in JS SDK 0.1 */ export declare function blink(color: Color, duration: "short" | "long"): void; diff --git a/applications/system/js_app/packages/fz-sdk/package.json b/applications/system/js_app/packages/fz-sdk/package.json new file mode 100644 index 000000000..4d18f3f20 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/package.json @@ -0,0 +1,27 @@ +{ + "name": "@flipperdevices/fz-sdk", + "version": "0.1.1", + "description": "Type declarations and documentation for native JS modules available on Flipper Zero", + "keywords": [ + "flipper", + "flipper zero", + "framework" + ], + "author": "Flipper Devices", + "license": "GPL-3.0-only", + "repository": { + "type": "git", + "url": "git+https://github.com/flipperdevices/flipperzero-firmware.git", + "directory": "applications/system/js_app/packages/fz-sdk" + }, + "type": "module", + "dependencies": { + "esbuild": "^0.24.0", + "esbuild-plugin-tsc": "^0.4.0", + "json5": "^2.2.3", + "typedoc": "^0.26.10", + "typedoc-material-theme": "^1.1.0", + "prompts": "^2.4.2", + "serialport": "^12.0.0" + } +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml b/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml new file mode 100644 index 000000000..45944a854 --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/pnpm-lock.yaml @@ -0,0 +1,896 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + prompts: + specifier: ^2.4.2 + version: 2.4.2 + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + esbuild: + specifier: ^0.24.0 + version: 0.24.0 + esbuild-plugin-tsc: + specifier: ^0.4.0 + version: 0.4.0(typescript@5.6.3) + json5: + specifier: ^2.2.3 + version: 2.2.3 + typedoc: + specifier: ^0.26.10 + version: 0.26.10(typescript@5.6.3) + typedoc-material-theme: + specifier: ^1.1.0 + version: 1.1.0(typedoc@0.26.10(typescript@5.6.3)) + +packages: + + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@material/material-color-utilities@0.2.7': + resolution: {integrity: sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==} + + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@shikijs/core@1.22.0': + resolution: {integrity: sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==} + + '@shikijs/engine-javascript@1.22.0': + resolution: {integrity: sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==} + + '@shikijs/engine-oniguruma@1.22.0': + resolution: {integrity: sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==} + + '@shikijs/types@1.22.0': + resolution: {integrity: sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==} + + '@shikijs/vscode-textmate@9.3.0': + resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild-plugin-tsc@0.4.0: + resolution: {integrity: sha512-q9gWIovt1nkwchMLc2zhyksaiHOv3kDK4b0AUol8lkMCRhJ1zavgfb2fad6BKp7FT9rh/OHmEBXVjczLoi/0yw==} + peerDependencies: + typescript: ^4.0.0 || ^5.0.0 + + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + + hast-util-to-html@9.0.3: + resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + oniguruma-to-js@0.4.3: + resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + regex@4.3.3: + resolution: {integrity: sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==} + + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + shiki@1.22.0: + resolution: {integrity: sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + typedoc-material-theme@1.1.0: + resolution: {integrity: sha512-LLWGVb8w+i+QGnsu/a0JKjcuzndFQt/UeGVOQz0HFFGGocROEHv5QYudIACrj+phL2LDwH05tJx0Ob3pYYH2UA==} + engines: {node: '>=18.0.0', npm: '>=8.6.0'} + peerDependencies: + typedoc: ^0.25.13 || ^0.26.3 + + typedoc@0.26.10: + resolution: {integrity: sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==} + engines: {node: '>= 18'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@esbuild/aix-ppc64@0.24.0': + optional: true + + '@esbuild/android-arm64@0.24.0': + optional: true + + '@esbuild/android-arm@0.24.0': + optional: true + + '@esbuild/android-x64@0.24.0': + optional: true + + '@esbuild/darwin-arm64@0.24.0': + optional: true + + '@esbuild/darwin-x64@0.24.0': + optional: true + + '@esbuild/freebsd-arm64@0.24.0': + optional: true + + '@esbuild/freebsd-x64@0.24.0': + optional: true + + '@esbuild/linux-arm64@0.24.0': + optional: true + + '@esbuild/linux-arm@0.24.0': + optional: true + + '@esbuild/linux-ia32@0.24.0': + optional: true + + '@esbuild/linux-loong64@0.24.0': + optional: true + + '@esbuild/linux-mips64el@0.24.0': + optional: true + + '@esbuild/linux-ppc64@0.24.0': + optional: true + + '@esbuild/linux-riscv64@0.24.0': + optional: true + + '@esbuild/linux-s390x@0.24.0': + optional: true + + '@esbuild/linux-x64@0.24.0': + optional: true + + '@esbuild/netbsd-x64@0.24.0': + optional: true + + '@esbuild/openbsd-arm64@0.24.0': + optional: true + + '@esbuild/openbsd-x64@0.24.0': + optional: true + + '@esbuild/sunos-x64@0.24.0': + optional: true + + '@esbuild/win32-arm64@0.24.0': + optional: true + + '@esbuild/win32-ia32@0.24.0': + optional: true + + '@esbuild/win32-x64@0.24.0': + optional: true + + '@material/material-color-utilities@0.2.7': {} + + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@shikijs/core@1.22.0': + dependencies: + '@shikijs/engine-javascript': 1.22.0 + '@shikijs/engine-oniguruma': 1.22.0 + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + + '@shikijs/engine-javascript@1.22.0': + dependencies: + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + oniguruma-to-js: 0.4.3 + + '@shikijs/engine-oniguruma@1.22.0': + dependencies: + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/types@1.22.0': + dependencies: + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@9.3.0': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.2.0': {} + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + entities@4.5.0: {} + + esbuild-plugin-tsc@0.4.0(typescript@5.6.3): + dependencies: + strip-comments: 2.0.1 + typescript: 5.6.3 + + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + + hast-util-to-html@9.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + html-void-elements@3.0.0: {} + + json5@2.2.3: {} + + kleur@3.0.3: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lunr@2.3.9: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + ms@2.1.2: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + oniguruma-to-js@0.4.3: + dependencies: + regex: 4.3.3 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + punycode.js@2.3.1: {} + + regex@4.3.3: {} + + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + shiki@1.22.0: + dependencies: + '@shikijs/core': 1.22.0 + '@shikijs/engine-javascript': 1.22.0 + '@shikijs/engine-oniguruma': 1.22.0 + '@shikijs/types': 1.22.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + sisteransi@1.0.5: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-comments@2.0.1: {} + + trim-lines@3.0.1: {} + + typedoc-material-theme@1.1.0(typedoc@0.26.10(typescript@5.6.3)): + dependencies: + '@material/material-color-utilities': 0.2.7 + typedoc: 0.26.10(typescript@5.6.3) + + typedoc@0.26.10(typescript@5.6.3): + dependencies: + lunr: 2.3.9 + markdown-it: 14.1.0 + minimatch: 9.0.5 + shiki: 1.22.0 + typescript: 5.6.3 + yaml: 2.6.0 + + typescript@5.6.3: {} + + uc.micro@2.1.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + yaml@2.6.0: {} + + zwitch@2.0.4: {} diff --git a/applications/system/js_app/packages/fz-sdk/sdk.js b/applications/system/js_app/packages/fz-sdk/sdk.js new file mode 100644 index 000000000..2eecf032d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/sdk.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { SerialPort } from "serialport"; +import prompts from "prompts"; +import esbuild from "esbuild"; +import json5 from "json5"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function build(config) { + await esbuild.build({ + entryPoints: ["./dist/index.js"], + outfile: config.output, + tsconfig: "./tsconfig.json", + format: "cjs", + bundle: true, + minify: config.minify, + external: [ + "@flipperdevices/fz-sdk/*" + ], + supported: { + "array-spread": false, + "arrow": false, + "async-await": false, + "async-generator": false, + "bigint": false, + "class": false, + "const-and-let": true, + "decorators": false, + "default-argument": false, + "destructuring": false, + "dynamic-import": false, + "exponent-operator": false, + "export-star-as": false, + "for-await": false, + "for-of": false, + "function-name-configurable": false, + "function-or-class-property-access": false, + "generator": false, + "hashbang": false, + "import-assertions": false, + "import-meta": false, + "inline-script": false, + "logical-assignment": false, + "nested-rest-binding": false, + "new-target": false, + "node-colon-prefix-import": false, + "node-colon-prefix-require": false, + "nullish-coalescing": false, + "object-accessors": false, + "object-extensions": false, + "object-rest-spread": false, + "optional-catch-binding": false, + "optional-chain": false, + "regexp-dot-all-flag": false, + "regexp-lookbehind-assertions": false, + "regexp-match-indices": false, + "regexp-named-capture-groups": false, + "regexp-set-notation": false, + "regexp-sticky-and-unicode-flags": false, + "regexp-unicode-property-escapes": false, + "rest-argument": false, + "template-literal": false, + "top-level-await": false, + "typeof-exotic-object-is-object": false, + "unicode-escapes": false, + "using": false, + }, + }); + + let outContents = fs.readFileSync(config.output, "utf8"); + outContents = "let exports = {};\n" + outContents; + + if (config.enforceSdkVersion) { + const version = json5.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version; + let [major, minor, _] = version.split("."); + outContents = `checkSdkCompatibility(${major}, ${minor});\n${outContents}`; + } + + fs.writeFileSync(config.output, outContents); +} + +async function upload(config) { + const appFile = fs.readFileSync(config.input, "utf8"); + const flippers = (await SerialPort.list()).filter(x => x.serialNumber?.startsWith("flip_")); + + if (!flippers) { + console.error("No Flippers found"); + process.exit(1); + } + + let portPath = flippers[0].path; + if (flippers.length > 1) { + port = (await prompts([{ + type: "select", + name: "port", + message: "Select Flipper to run the app on", + choices: flippers.map(x => ({ title: x.serialNumber.replace("flip_", ""), value: x.path })), + }])).port; + } + + console.log(`Connecting to Flipper at ${portPath}`); + let port = new SerialPort({ path: portPath, baudRate: 230400 }); + let received = ""; + let lastMatch = 0; + async function waitFor(string, timeoutMs) { + return new Promise((resolve, _reject) => { + let timeout = undefined; + if (timeoutMs) { + timeout = setTimeout(() => { + console.error("Error: timeout"); + process.exit(1); + }, timeoutMs); + } + setInterval(() => { + let idx = received.indexOf(string, lastMatch); + if (idx !== -1) { + lastMatch = idx; + if (timeoutMs) + clearTimeout(timeout); + resolve(); + } + }, 50); + }); + } + port.on("data", (data) => { + received += data.toString(); + }); + + await waitFor(">: ", 1000); + console.log("Uploading application file"); + port.write(`storage remove ${config.output}\x0d`); + port.drain(); + await waitFor(">: ", 1000); + port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`); + await waitFor("Ready", 1000); + port.write(appFile); + port.drain(); + await waitFor(">: ", 1000); + + console.log("Launching application"); + port.write(`js ${config.output}\x0d`); + port.drain(); + + await waitFor("Running", 1000); + process.stdout.write(received.slice(lastMatch)); + port.on("data", (data) => { + process.stdout.write(data.toString()); + }); + process.on("exit", () => { + port.write("\x03"); + }); + + await waitFor("Script done!", 0); + process.exit(0); +} + +(async () => { + const commands = { + "build": build, + "upload": upload, + }; + + const config = json5.parse(fs.readFileSync("./fz-sdk.config.json5", "utf8")); + const command = process.argv[2]; + + if (!Object.keys(commands).includes(command)) { + console.error(`Unknown command ${command}. Supported: ${Object.keys(commands).join(", ")}`); + process.exit(1); + } + + await commands[command](config[command]); +})(); diff --git a/applications/system/js_app/types/serial/index.d.ts b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts similarity index 92% rename from applications/system/js_app/types/serial/index.d.ts rename to applications/system/js_app/packages/fz-sdk/serial/index.d.ts index 680f9bd6e..3c249352e 100644 --- a/applications/system/js_app/types/serial/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/serial/index.d.ts @@ -1,3 +1,9 @@ +/** + * Module for accessing the serial port + * @version Added in JS SDK 0.1 + * @module + */ + /** * @brief Initializes the serial port * @@ -5,6 +11,7 @@ * * @param port The port to initialize (`"lpuart"` or `"start"`) * @param baudRate + * @version Added in JS SDK 0.1 */ export declare function setup(port: "lpuart" | "usart", baudRate: number): void; @@ -16,6 +23,7 @@ export declare function setup(port: "lpuart" | "usart", baudRate: number): void; * - Arrays of numbers will get sent as a sequence of bytes. * - `ArrayBuffer`s and `TypedArray`s will be sent as a sequence * of bytes. + * @version Added in JS SDK 0.1 */ export declare function write(value: string | number | number[] | ArrayBuffer | TypedArray): void; @@ -27,6 +35,7 @@ export declare function write(value: string | number | nu * unset, the function will wait forever. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function read(length: number, timeout?: number): string | undefined; @@ -42,6 +51,7 @@ export declare function read(length: number, timeout?: number): string | undefin * applies to characters, not entire strings. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function readln(timeout?: number): string; @@ -55,6 +65,7 @@ export declare function readln(timeout?: number): string; * wait forever. * @returns The received data interpreted as ASCII, or `undefined` if 0 bytes * were read. + * @version Added in JS SDK 0.1 */ export declare function readAny(timeout?: number): string | undefined; @@ -66,6 +77,7 @@ export declare function readAny(timeout?: number): string | undefined; * unset, the function will wait forever. * @returns The received data as an ArrayBuffer, or `undefined` if 0 bytes were * read. + * @version Added in JS SDK 0.1 */ export declare function readBytes(length: number, timeout?: number): ArrayBuffer; @@ -89,10 +101,12 @@ export declare function readBytes(length: number, timeout?: number): ArrayBuffer * @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. + * @version Added in JS SDK 0.1 */ export declare function expect(patterns: string | number[] | string[] | number[][], timeout?: number): number | undefined; /** * @brief Deinitializes the serial port, allowing multiple initializations per script run + * @version Added in JS SDK 0.1 */ export declare function end(): void; diff --git a/applications/system/js_app/types/storage/index.d.ts b/applications/system/js_app/packages/fz-sdk/storage/index.d.ts similarity index 83% rename from applications/system/js_app/types/storage/index.d.ts rename to applications/system/js_app/packages/fz-sdk/storage/index.d.ts index 0dd29e121..90d7a8254 100644 --- a/applications/system/js_app/types/storage/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/storage/index.d.ts @@ -1,8 +1,15 @@ +/** + * Module for accessing the filesystem + * @version Added in JS SDK 0.1 + * @module + */ + /** * File readability mode: * - `"r"`: read-only * - `"w"`: write-only * - `"rw"`: read-write + * @version Added in JS SDK 0.1 */ export type AccessMode = "r" | "w" | "rw"; @@ -13,53 +20,78 @@ export type AccessMode = "r" | "w" | "rw"; * - `"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 + * @version Added in JS SDK 0.1 */ export type OpenMode = "open_existing" | "open_always" | "open_append" | "create_new" | "create_always"; -/** Standard UNIX timestamp */ +/** + * Standard UNIX timestamp + * @version Added in JS SDK 0.1 + */ export type Timestamp = number; -/** File information structure */ +/** + * File information structure + * @version Added in JS SDK 0.1 + */ export declare class FileInfo { /** * Full path (e.g. "/ext/test", returned by `stat`) or file name * (e.g. "test", returned by `readDirectory`) + * @version Added in JS SDK 0.1 */ path: string; /** * Is the file a directory? + * @version Added in JS SDK 0.1 */ isDirectory: boolean; /** * File size in bytes, or 0 in the case of directories + * @version Added in JS SDK 0.1 */ size: number; /** * Time of last access as a UNIX timestamp + * @version Added in JS SDK 0.1 */ accessTime: Timestamp; } -/** Filesystem information structure */ +/** + * Filesystem information structure + * @version Added in JS SDK 0.1 + */ export declare class FsInfo { - /** Total size of the filesystem, in bytes */ + /** + * Total size of the filesystem, in bytes + * @version Added in JS SDK 0.1 + */ totalSpace: number; - /** Free space in the filesystem, in bytes */ + /** + * Free space in the filesystem, in bytes + * @version Added in JS SDK 0.1 + */ freeSpace: number; } // file operations -/** File class */ +/** + * File class + * @version Added in JS SDK 0.1 + */ 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 + * @version Added in JS SDK 0.1 */ close(): boolean; /** * Is the file currently open? + * @version Added in JS SDK 0.1 */ isOpen(): boolean; /** @@ -70,6 +102,7 @@ export declare class 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. + * @version Added in JS SDK 0.1 */ read(mode: T extends ArrayBuffer ? "binary" : "ascii", bytes: number): T; /** @@ -77,36 +110,43 @@ export declare class File { * @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 + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ seekAbsolute(bytes: number): boolean; /** * Gets the absolute position of the R/W pointer in bytes + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ truncate(): boolean; /** * Reads the total size of the file in bytes + * @version Added in JS SDK 0.1 */ size(): number; /** * Detects whether the R/W pointer has reached the end of the file + * @version Added in JS SDK 0.1 */ eof(): boolean; /** @@ -115,6 +155,7 @@ export declare class 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 + * @version Added in JS SDK 0.1 */ copyTo(dest: File, bytes: number): boolean; } @@ -126,12 +167,14 @@ export declare class File { * @param openMode `"open_existing"`, `"open_always"`, `"open_append"`, * `"create_new"` or `"create_always"`; see `OpenMode` * @returns a `File` on success, or `undefined` on failure + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ export declare function fileExists(path: string): boolean; @@ -142,17 +185,20 @@ export declare function fileExists(path: string): boolean; * @param path The path to the directory * @returns Array of `FileInfo` structures with directory entries, * or `undefined` on failure + * @version Added in JS SDK 0.1 */ export declare function readDirectory(path: string): FileInfo[] | undefined; /** * Detects whether a directory exists * @param path The path to the directory + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ export declare function makeDirectory(path: string): boolean; @@ -161,24 +207,28 @@ export declare function makeDirectory(path: string): boolean; /** * Detects whether a file or a directory exists * @param path The path to the file or directory + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ 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 + * @version Added in JS SDK 0.1 */ export declare function rmrf(path: string): boolean; /** @@ -187,6 +237,7 @@ export declare function rmrf(path: string): boolean; * @param newPath The new path that the file or directory will become accessible * under * @returns `true` on success, `false` on failure + * @version Added in JS SDK 0.1 */ export declare function rename(oldPath: string, newPath: string): boolean; /** @@ -194,11 +245,13 @@ export declare function rename(oldPath: string, newPath: string): boolean; * @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 + * @version Added in JS SDK 0.1 */ 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"`) + * @version Added in JS SDK 0.1 */ export declare function fsInfo(filesystem: string): FsInfo | undefined; /** @@ -218,6 +271,7 @@ export declare function fsInfo(filesystem: string): FsInfo | undefined; * @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. + * @version Added in JS SDK 0.1 */ export declare function nextAvailableFilename(dirPath: string, fileName: string, fileExt: string, maxLen: number): string; @@ -226,6 +280,7 @@ export declare function nextAvailableFilename(dirPath: string, fileName: string, /** * Determines whether the two paths are equivalent. Respects filesystem-defined * path equivalence rules. + * @version Added in JS SDK 0.1 */ export declare function arePathsEqual(path1: string, path2: string): boolean; /** @@ -233,5 +288,6 @@ export declare function arePathsEqual(path1: string, path2: string): boolean; * filesystem-defined path equivalence rules. * @param parentPath The parent path * @param childPath The child path + * @version Added in JS SDK 0.1 */ 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/packages/fz-sdk/tests/index.d.ts similarity index 88% rename from applications/system/js_app/types/tests/index.d.ts rename to applications/system/js_app/packages/fz-sdk/tests/index.d.ts index 8aaeec5e5..031588d4a 100644 --- a/applications/system/js_app/types/tests/index.d.ts +++ b/applications/system/js_app/packages/fz-sdk/tests/index.d.ts @@ -1,6 +1,8 @@ /** * Unit test module. Only available if the firmware has been configured with * `FIRMWARE_APP_SET=unit_tests`. + * @version Added in JS SDK 0.1 + * @module */ export function fail(message: string): never; diff --git a/applications/system/js_app/packages/fz-sdk/tsconfig.json b/applications/system/js_app/packages/fz-sdk/tsconfig.json new file mode 100644 index 000000000..cfb792e3f --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "CommonJS", + "noLib": true, + }, + "include": [ + "./**/*.d.ts" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/applications/system/js_app/packages/fz-sdk/typedoc.json b/applications/system/js_app/packages/fz-sdk/typedoc.json new file mode 100644 index 000000000..8b3befa6d --- /dev/null +++ b/applications/system/js_app/packages/fz-sdk/typedoc.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "name": "Flipper Zero JS API", + "excludePrivate": true, + "entryPointStrategy": "expand", + "entryPoints": [ + ".", + ], + "exclude": [ + "node_modules" + ], + "cleanOutputDir": true, + "out": "./docs", + "plugin": [ + "typedoc-material-theme", + ], + "readme": "./docs_readme.md", + "themeColor": "#ff8200", +} \ No newline at end of file diff --git a/applications/system/js_app/types/event_loop/index.d.ts b/applications/system/js_app/types/event_loop/index.d.ts deleted file mode 100644 index 49237782c..000000000 --- a/applications/system/js_app/types/event_loop/index.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index b1b1d474b..000000000 --- a/applications/system/js_app/types/flipper/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @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/gui/dialog.d.ts b/applications/system/js_app/types/gui/dialog.d.ts deleted file mode 100644 index 6d9c8d43b..000000000 --- a/applications/system/js_app/types/gui/dialog.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index c71e93b32..000000000 --- a/applications/system/js_app/types/gui/empty_screen.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -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/loading.d.ts b/applications/system/js_app/types/gui/loading.d.ts deleted file mode 100644 index 73a963349..000000000 --- a/applications/system/js_app/types/gui/loading.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 59d535864..000000000 --- a/applications/system/js_app/types/gui/submenu.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 3dbbac571..000000000 --- a/applications/system/js_app/types/gui/text_box.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index bdb5b4d4c..000000000 --- a/applications/system/js_app/types/gui/text_input.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { View, ViewFactory } from "."; -import type { Contract } from "../event_loop"; - -type Props = { - header: string, - minLength: number, - maxLength: number, - defaultText: string, - defaultTextClear: boolean, -} -declare class TextInput extends View { - input: Contract; -} -declare class TextInputFactory extends ViewFactory { } -declare const factory: TextInputFactory; -export = factory; diff --git a/assets/icons/Archive/file_10px.png b/assets/icons/Archive/file_10px.png new file mode 100644 index 000000000..cd38770f4 Binary files /dev/null and b/assets/icons/Archive/file_10px.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_0.png b/assets/icons/Settings/Alarm_47x39/frame_0.png new file mode 100644 index 000000000..564116036 Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_0.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_1.png b/assets/icons/Settings/Alarm_47x39/frame_1.png new file mode 100644 index 000000000..c5c58ea91 Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_1.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_2.png b/assets/icons/Settings/Alarm_47x39/frame_2.png new file mode 100644 index 000000000..b7d338f7d Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_2.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_3.png b/assets/icons/Settings/Alarm_47x39/frame_3.png new file mode 100644 index 000000000..067d55ddd Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_3.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_4.png b/assets/icons/Settings/Alarm_47x39/frame_4.png new file mode 100644 index 000000000..564116036 Binary files /dev/null and b/assets/icons/Settings/Alarm_47x39/frame_4.png differ diff --git a/assets/icons/Settings/Alarm_47x39/frame_rate b/assets/icons/Settings/Alarm_47x39/frame_rate new file mode 100644 index 000000000..0cfbf0888 --- /dev/null +++ b/assets/icons/Settings/Alarm_47x39/frame_rate @@ -0,0 +1 @@ +2 diff --git a/documentation/UnitTests.md b/documentation/UnitTests.md index 5c80e763d..9711c6ae1 100644 --- a/documentation/UnitTests.md +++ b/documentation/UnitTests.md @@ -20,7 +20,7 @@ To run the unit tests, follow these steps: 3. Launch the CLI session and run the `unit_tests` command. **NOTE:** To run a particular test (and skip all others), specify its name as the command argument. -See [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/debug/unit_tests/test_index.c) for the complete list of test names. +Test names match application names defined [here](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/applications/debug/unit_tests/application.fam). ## Adding unit tests @@ -28,7 +28,7 @@ See [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/blob/d #### Entry point -The common entry point for all tests is the [unit_tests](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests) app. Test-specific code is placed into an arbitrarily named subdirectory and is then called from the [test_index.c](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests/test_index.c) source file. +The common entry point for all tests is the [unit_tests](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/debug/unit_tests) app. Test-specific code is packaged as a `PLUGIN` app placed in a subdirectory of `tests` in the `unit_tests` mother-app and referenced in the common `application.fam`. Look at other tests for an example. #### Test assets diff --git a/documentation/images/byte_input.png b/documentation/images/byte_input.png new file mode 100644 index 000000000..9a46eb01d Binary files /dev/null and b/documentation/images/byte_input.png differ diff --git a/documentation/js/js_gpio.md b/documentation/js/js_gpio.md index 9791fb4eb..aa444bacd 100644 --- a/documentation/js/js_gpio.md +++ b/documentation/js/js_gpio.md @@ -61,7 +61,7 @@ Reads a digital value from a pin configured with `direction: "in"` and any #### Returns Boolean logic level -### `Pin.read_analog()` +### `Pin.readAnalog()` Reads an analog voltage level in millivolts from a pin configured with `direction: "in"` and `inMode: "analog"` diff --git a/documentation/js/js_math.md b/documentation/js/js_math.md index 12dae8fb3..ca16a9111 100644 --- a/documentation/js/js_math.md +++ b/documentation/js/js_math.md @@ -223,7 +223,7 @@ math.floor(45.05); // 45 math.floor(45.95); // 45 ``` -## is_equal +## isEqual Return true if the difference between numbers `a` and `b` is less than the specified parameter `e`. ### Parameters @@ -236,8 +236,8 @@ True if the difference between numbers `a` and `b` is less than the specified pa ### Example ```js -math.is_equal(1.4, 1.6, 0.2); // false -math.is_equal(3.556, 3.555, 0.01); // true +math.isEqual(1.4, 1.6, 0.2); // false +math.isEqual(3.556, 3.555, 0.01); // true ``` ## max diff --git a/furi/core/event_flag.c b/furi/core/event_flag.c index 19b28a500..721f4c2fa 100644 --- a/furi/core/event_flag.c +++ b/furi/core/event_flag.c @@ -5,11 +5,15 @@ #include #include +#include "event_loop_link_i.h" + #define FURI_EVENT_FLAG_MAX_BITS_EVENT_GROUPS 24U -#define FURI_EVENT_FLAG_INVALID_BITS (~((1UL << FURI_EVENT_FLAG_MAX_BITS_EVENT_GROUPS) - 1U)) +#define FURI_EVENT_FLAG_VALID_BITS ((1UL << FURI_EVENT_FLAG_MAX_BITS_EVENT_GROUPS) - 1U) +#define FURI_EVENT_FLAG_INVALID_BITS (~(FURI_EVENT_FLAG_VALID_BITS)) struct FuriEventFlag { StaticEventGroup_t container; + FuriEventLoopLink event_loop_link; }; // IMPORTANT: container MUST be the FIRST struct member @@ -27,6 +31,11 @@ FuriEventFlag* furi_event_flag_alloc(void) { void furi_event_flag_free(FuriEventFlag* instance) { furi_check(!FURI_IS_IRQ_MODE()); + + // Event Loop must be disconnected + furi_check(!instance->event_loop_link.item_in); + furi_check(!instance->event_loop_link.item_out); + vEventGroupDelete((EventGroupHandle_t)instance); free(instance); } @@ -39,6 +48,8 @@ uint32_t furi_event_flag_set(FuriEventFlag* instance, uint32_t flags) { uint32_t rflags; BaseType_t yield; + FURI_CRITICAL_ENTER(); + if(FURI_IS_IRQ_MODE()) { yield = pdFALSE; if(xEventGroupSetBitsFromISR(hEventGroup, (EventBits_t)flags, &yield) == pdFAIL) { @@ -48,11 +59,15 @@ uint32_t furi_event_flag_set(FuriEventFlag* instance, uint32_t flags) { portYIELD_FROM_ISR(yield); } } else { - vTaskSuspendAll(); rflags = xEventGroupSetBits(hEventGroup, (EventBits_t)flags); - (void)xTaskResumeAll(); } + if(rflags & flags) { + furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventIn); + } + + FURI_CRITICAL_EXIT(); + /* Return event flags after setting */ return rflags; } @@ -64,6 +79,7 @@ uint32_t furi_event_flag_clear(FuriEventFlag* instance, uint32_t flags) { EventGroupHandle_t hEventGroup = (EventGroupHandle_t)instance; uint32_t rflags; + FURI_CRITICAL_ENTER(); if(FURI_IS_IRQ_MODE()) { rflags = xEventGroupGetBitsFromISR(hEventGroup); @@ -79,6 +95,11 @@ uint32_t furi_event_flag_clear(FuriEventFlag* instance, uint32_t flags) { rflags = xEventGroupClearBits(hEventGroup, (EventBits_t)flags); } + if(rflags & flags) { + furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventOut); + } + FURI_CRITICAL_EXIT(); + /* Return event flags before clearing */ return rflags; } @@ -146,6 +167,36 @@ uint32_t furi_event_flag_wait( } } + if((rflags & FuriFlagError) == 0U) { + furi_event_loop_link_notify(&instance->event_loop_link, FuriEventLoopEventOut); + } + /* Return event flags before clearing */ return rflags; } + +static FuriEventLoopLink* furi_event_flag_event_loop_get_link(FuriEventLoopObject* object) { + FuriEventFlag* instance = object; + furi_assert(instance); + return &instance->event_loop_link; +} + +static bool + furi_event_flag_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { + FuriEventFlag* instance = object; + furi_assert(instance); + + if(event == FuriEventLoopEventIn) { + return (furi_event_flag_get(instance) & FURI_EVENT_FLAG_VALID_BITS); + } else if(event == FuriEventLoopEventOut) { + return (furi_event_flag_get(instance) & FURI_EVENT_FLAG_VALID_BITS) != + FURI_EVENT_FLAG_VALID_BITS; + } else { + furi_crash(); + } +} + +const FuriEventLoopContract furi_event_flag_event_loop_contract = { + .get_link = furi_event_flag_event_loop_get_link, + .get_level = furi_event_flag_event_loop_get_level, +}; diff --git a/furi/core/event_loop.c b/furi/core/event_loop.c index b622aa7a1..c0998ea90 100644 --- a/furi/core/event_loop.c +++ b/furi/core/event_loop.c @@ -101,36 +101,39 @@ void furi_event_loop_free(FuriEventLoop* instance) { } static inline FuriEventLoopProcessStatus - furi_event_loop_poll_process_level_event(FuriEventLoopItem* item) { - if(!item->contract->get_level(item->object, item->event)) { - return FuriEventLoopProcessStatusComplete; - } else if(item->callback(item->object, item->callback_context)) { - return FuriEventLoopProcessStatusIncomplete; - } else { - return FuriEventLoopProcessStatusAgain; - } + furi_event_loop_process_edge_event(FuriEventLoopItem* item) { + FuriEventLoopProcessStatus status = FuriEventLoopProcessStatusComplete; + item->callback(item->object, item->callback_context); + + return status; } static inline FuriEventLoopProcessStatus - furi_event_loop_poll_process_edge_event(FuriEventLoopItem* item) { - if(item->callback(item->object, item->callback_context)) { - return FuriEventLoopProcessStatusComplete; - } else { - return FuriEventLoopProcessStatusAgain; + furi_event_loop_process_level_event(FuriEventLoopItem* item) { + FuriEventLoopProcessStatus status = FuriEventLoopProcessStatusComplete; + if(item->contract->get_level(item->object, item->event)) { + item->callback(item->object, item->callback_context); + + if(item->contract->get_level(item->object, item->event)) { + status = FuriEventLoopProcessStatusIncomplete; + } } + + return status; } static inline FuriEventLoopProcessStatus - furi_event_loop_poll_process_event(FuriEventLoop* instance, FuriEventLoopItem* item) { + furi_event_loop_process_event(FuriEventLoop* instance, FuriEventLoopItem* item) { FuriEventLoopProcessStatus status; + if(item->event & FuriEventLoopEventFlagOnce) { furi_event_loop_unsubscribe(instance, item->object); } if(item->event & FuriEventLoopEventFlagEdge) { - status = furi_event_loop_poll_process_edge_event(item); + status = furi_event_loop_process_edge_event(item); } else { - status = furi_event_loop_poll_process_level_event(item); + status = furi_event_loop_process_level_event(item); } if(item->owner == NULL) { @@ -140,7 +143,7 @@ static inline FuriEventLoopProcessStatus return status; } -static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { +static inline FuriEventLoopItem* furi_event_loop_get_waiting_item(FuriEventLoop* instance) { FuriEventLoopItem* item = NULL; FURI_CRITICAL_ENTER(); @@ -152,27 +155,42 @@ static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { FURI_CRITICAL_EXIT(); + return item; +} + +static inline void furi_event_loop_sync_flags(FuriEventLoop* instance) { + FURI_CRITICAL_ENTER(); + + if(!WaitingList_empty_p(instance->waiting_list)) { + xTaskNotifyIndexed( + (TaskHandle_t)instance->thread_id, + FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, + FuriEventLoopFlagEvent, + eSetBits); + } + + FURI_CRITICAL_EXIT(); +} + +static void furi_event_loop_process_waiting_list(FuriEventLoop* instance) { + FuriEventLoopItem* item = furi_event_loop_get_waiting_item(instance); if(!item) return; - while(true) { - FuriEventLoopProcessStatus ret = furi_event_loop_poll_process_event(instance, item); + FuriEventLoopProcessStatus status = furi_event_loop_process_event(instance, item); - if(ret == FuriEventLoopProcessStatusComplete) { - // Event processing complete, break from loop - break; - } else if(ret == FuriEventLoopProcessStatusIncomplete) { - // Event processing incomplete more processing needed - } else if(ret == FuriEventLoopProcessStatusAgain) { //-V547 - furi_event_loop_item_notify(item); - break; - // Unsubscribed from inside the callback, delete item - } else if(ret == FuriEventLoopProcessStatusFreeLater) { //-V547 - furi_event_loop_item_free(item); - break; - } else { - furi_crash(); - } + if(status == FuriEventLoopProcessStatusComplete) { + // Event processing complete, do nothing + } else if(status == FuriEventLoopProcessStatusIncomplete) { + // Event processing incomplete, put item back in waiting list + furi_event_loop_item_notify(item); + } else if(status == FuriEventLoopProcessStatusFreeLater) { //-V547 + // Unsubscribed from inside the callback, delete item + furi_event_loop_item_free(item); + } else { + furi_crash(); } + + furi_event_loop_sync_flags(instance); } static void furi_event_loop_restore_flags(FuriEventLoop* instance, uint32_t flags) { @@ -239,14 +257,28 @@ void furi_event_loop_run(FuriEventLoop* instance) { } } +static void furi_event_loop_notify(FuriEventLoop* instance, FuriEventLoopFlag flag) { + if(FURI_IS_IRQ_MODE()) { + BaseType_t yield = pdFALSE; + + (void)xTaskNotifyIndexedFromISR( + (TaskHandle_t)instance->thread_id, + FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, + flag, + eSetBits, + &yield); + + portYIELD_FROM_ISR(yield); + + } else { + (void)xTaskNotifyIndexed( + (TaskHandle_t)instance->thread_id, FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, flag, eSetBits); + } +} + void furi_event_loop_stop(FuriEventLoop* instance) { furi_check(instance); - - xTaskNotifyIndexed( - (TaskHandle_t)instance->thread_id, - FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, - FuriEventLoopFlagStop, - eSetBits); + furi_event_loop_notify(instance, FuriEventLoopFlagStop); } /* @@ -268,11 +300,7 @@ void furi_event_loop_pend_callback( PendingQueue_push_front(instance->pending_queue, item); - xTaskNotifyIndexed( - (TaskHandle_t)instance->thread_id, - FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, - FuriEventLoopFlagPending, - eSetBits); + furi_event_loop_notify(instance, FuriEventLoopFlagPending); } /* @@ -328,6 +356,17 @@ static void furi_event_loop_object_subscribe( * Public specialized subscription API */ +void furi_event_loop_subscribe_event_flag( + FuriEventLoop* instance, + FuriEventFlag* event_flag, + FuriEventLoopEvent event, + FuriEventLoopEventCallback callback, + void* context) { + extern const FuriEventLoopContract furi_event_flag_event_loop_contract; + furi_event_loop_object_subscribe( + instance, event_flag, &furi_event_flag_event_loop_contract, event, callback, context); +} + void furi_event_loop_subscribe_message_queue( FuriEventLoop* instance, FuriMessageQueue* message_queue, @@ -491,11 +530,7 @@ static void furi_event_loop_item_notify(FuriEventLoopItem* instance) { FURI_CRITICAL_EXIT(); - xTaskNotifyIndexed( - (TaskHandle_t)owner->thread_id, - FURI_EVENT_LOOP_FLAG_NOTIFY_INDEX, - FuriEventLoopFlagEvent, - eSetBits); + furi_event_loop_notify(owner, FuriEventLoopFlagEvent); } static bool furi_event_loop_item_is_waiting(FuriEventLoopItem* instance) { diff --git a/furi/core/event_loop.h b/furi/core/event_loop.h index 6c5ba432c..d5e8710a6 100644 --- a/furi/core/event_loop.h +++ b/furi/core/event_loop.h @@ -11,6 +11,9 @@ * provide any compatibility with other event driven APIs. But * programming concepts are the same, except some runtime * limitations from our side. + * + * @warning Only ONE instance of FuriEventLoop per thread is possible. ALL FuriEventLoop + * funcitons MUST be called from the same thread that the instance was created in. */ #pragma once @@ -197,10 +200,29 @@ typedef void FuriEventLoopObject; * * @param object The object that triggered the event * @param context The context that was provided upon subscription - * - * @return true if event was processed, false if we need to delay processing */ -typedef bool (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); +typedef void (*FuriEventLoopEventCallback)(FuriEventLoopObject* object, void* context); + +/** Opaque event flag type */ +typedef struct FuriEventFlag FuriEventFlag; + +/** Subscribe to event flag events + * + * @warning you can only have one subscription for one event type. + * + * @param instance The Event Loop instance + * @param event_flag The event flag to add + * @param[in] event The Event Loop event to trigger on + * @param[in] callback The callback to call on event + * @param context The context for callback + */ + +void furi_event_loop_subscribe_event_flag( + FuriEventLoop* instance, + FuriEventFlag* event_flag, + FuriEventLoopEvent event, + FuriEventLoopEventCallback callback, + void* context); /** Opaque message queue type */ typedef struct FuriMessageQueue FuriMessageQueue; diff --git a/furi/core/event_loop_i.h b/furi/core/event_loop_i.h index 15efa8f86..7016e1e1b 100644 --- a/furi/core/event_loop_i.h +++ b/furi/core/event_loop_i.h @@ -59,7 +59,6 @@ typedef enum { typedef enum { FuriEventLoopProcessStatusComplete, FuriEventLoopProcessStatusIncomplete, - FuriEventLoopProcessStatusAgain, FuriEventLoopProcessStatusFreeLater, } FuriEventLoopProcessStatus; diff --git a/furi/core/event_loop_link_i.h b/furi/core/event_loop_link_i.h index 992ca6555..4b993390f 100644 --- a/furi/core/event_loop_link_i.h +++ b/furi/core/event_loop_link_i.h @@ -21,7 +21,7 @@ void furi_event_loop_link_notify(FuriEventLoopLink* instance, FuriEventLoopEvent typedef FuriEventLoopLink* (*FuriEventLoopContractGetLink)(FuriEventLoopObject* object); -typedef uint32_t ( +typedef bool ( *FuriEventLoopContractGetLevel)(FuriEventLoopObject* object, FuriEventLoopEvent event); typedef struct { diff --git a/furi/core/event_loop_timer.h b/furi/core/event_loop_timer.h index 9034043fa..50fb57389 100644 --- a/furi/core/event_loop_timer.h +++ b/furi/core/event_loop_timer.h @@ -1,6 +1,9 @@ /** * @file event_loop_timer.h * @brief Software timer functionality for FuriEventLoop. + * + * @warning ALL FuriEventLoopTimer functions MUST be called from the + * same thread that the owner FuriEventLoop instance was created in. */ #pragma once diff --git a/furi/core/message_queue.c b/furi/core/message_queue.c index bd0cec021..b0862e501 100644 --- a/furi/core/message_queue.c +++ b/furi/core/message_queue.c @@ -213,7 +213,7 @@ static FuriEventLoopLink* furi_message_queue_event_loop_get_link(FuriEventLoopOb return &instance->event_loop_link; } -static uint32_t +static bool furi_message_queue_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriMessageQueue* instance = object; furi_assert(instance); diff --git a/furi/core/mutex.c b/furi/core/mutex.c index f9848e1ba..edaba9e00 100644 --- a/furi/core/mutex.c +++ b/furi/core/mutex.c @@ -144,13 +144,13 @@ static FuriEventLoopLink* furi_mutex_event_loop_get_link(FuriEventLoopObject* ob return &instance->event_loop_link; } -static uint32_t +static bool furi_mutex_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriMutex* instance = object; furi_assert(instance); if(event == FuriEventLoopEventIn || event == FuriEventLoopEventOut) { - return furi_mutex_get_owner(instance) ? 0 : 1; + return !furi_mutex_get_owner(instance); } else { furi_crash(); } diff --git a/furi/core/semaphore.c b/furi/core/semaphore.c index 850169ad6..d05b9bf09 100644 --- a/furi/core/semaphore.c +++ b/furi/core/semaphore.c @@ -165,7 +165,7 @@ static FuriEventLoopLink* furi_semaphore_event_loop_get_link(FuriEventLoopObject return &instance->event_loop_link; } -static uint32_t +static bool furi_semaphore_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriSemaphore* instance = object; furi_assert(instance); diff --git a/furi/core/stream_buffer.c b/furi/core/stream_buffer.c index f35abec64..783b2d741 100644 --- a/furi/core/stream_buffer.c +++ b/furi/core/stream_buffer.c @@ -157,7 +157,7 @@ static FuriEventLoopLink* furi_stream_buffer_event_loop_get_link(FuriEventLoopOb return &stream_buffer->event_loop_link; } -static uint32_t +static bool furi_stream_buffer_event_loop_get_level(FuriEventLoopObject* object, FuriEventLoopEvent event) { FuriStreamBuffer* stream_buffer = object; furi_assert(stream_buffer); diff --git a/lib/flipper_application/flipper_application.c b/lib/flipper_application/flipper_application.c index 376e9c9ea..77bfa4387 100644 --- a/lib/flipper_application/flipper_application.c +++ b/lib/flipper_application/flipper_application.c @@ -242,9 +242,6 @@ static int32_t flipper_application_thread(void* context) { // wait until all notifications from RAM are completed NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION); - const NotificationSequence sequence_empty = { - NULL, - }; notification_message_block(notifications, &sequence_empty); furi_record_close(RECORD_NOTIFICATION); diff --git a/lib/mjs/mjs_builtin.c b/lib/mjs/mjs_builtin.c index afcf9ce6f..7a1d74ff1 100644 --- a/lib/mjs/mjs_builtin.c +++ b/lib/mjs/mjs_builtin.c @@ -146,10 +146,17 @@ void mjs_init_builtin(struct mjs* mjs, mjs_val_t obj) { // mjs_set(mjs, obj, "JSON", ~0, v); /* - * Populate Object.create() + * Populate Object */ v = mjs_mk_object(mjs); mjs_set(mjs, v, "create", ~0, mjs_mk_foreign_func(mjs, (mjs_func_ptr_t)mjs_op_create_object)); + mjs_set( + mjs, + v, + "defineProperty", + ~0, + mjs_mk_foreign_func( + mjs, (mjs_func_ptr_t)mjs_op_object_define_property)); // stub, do not use mjs_set(mjs, obj, "Object", ~0, v); /* diff --git a/lib/mjs/mjs_object.c b/lib/mjs/mjs_object.c index 60bacf514..cf14a499f 100644 --- a/lib/mjs/mjs_object.c +++ b/lib/mjs/mjs_object.c @@ -294,6 +294,11 @@ clean: mjs_return(mjs, ret); } +MJS_PRIVATE void mjs_op_object_define_property(struct mjs* mjs) { + // stub, do not use + mjs_return(mjs, MJS_UNDEFINED); +} + mjs_val_t mjs_struct_to_obj(struct mjs* mjs, const void* base, const struct mjs_c_struct_member* defs) { mjs_val_t obj; diff --git a/lib/mjs/mjs_object.h b/lib/mjs/mjs_object.h index 870486d06..101272e29 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); +/* + * Stub of `Object.defineProperty()` + */ +MJS_PRIVATE void mjs_op_object_define_property(struct mjs* mjs); + /* * Cell destructor for object arena */ diff --git a/lib/mjs/mjs_parser.c b/lib/mjs/mjs_parser.c index 503b16942..212804a86 100644 --- a/lib/mjs/mjs_parser.c +++ b/lib/mjs/mjs_parser.c @@ -76,7 +76,8 @@ static int s_assign_ops[] = { static int findtok(int* toks, int tok) { int i = 0; - while(tok != toks[i] && toks[i] != TOK_EOF) i++; + while(tok != toks[i] && toks[i] != TOK_EOF) + i++; return toks[i]; } @@ -87,7 +88,7 @@ static void emit_op(struct pstate* pstate, int tok) { } #define BINOP_STACK_FRAME_SIZE 16 -#define STACK_LIMIT 8192 +#define STACK_LIMIT 8192 // Intentionally left as macro rather than a function, to let the // compiler to inline calls and mimimize runtime stack usage. @@ -166,7 +167,8 @@ static mjs_err_t parse_statement_list(struct pstate* p, int et) { if(drop) emit_byte(p, OP_DROP); res = parse_statement(p); drop = 1; - while(p->tok.tok == TOK_SEMICOLON) pnext1(p); + while(p->tok.tok == TOK_SEMICOLON) + pnext1(p); } /* @@ -523,7 +525,11 @@ static mjs_err_t parse_expr(struct pstate* p) { static mjs_err_t parse_let(struct pstate* p) { mjs_err_t res = MJS_OK; LOG(LL_VERBOSE_DEBUG, ("[%.*s]", 10, p->tok.ptr)); - EXPECT(p, TOK_KEYWORD_LET); + if((p)->tok.tok != TOK_KEYWORD_VAR && (p)->tok.tok != TOK_KEYWORD_LET && + (p)->tok.tok != TOK_KEYWORD_CONST) + SYNTAX_ERROR(p); + else + pnext1(p); for(;;) { struct tok tmp = p->tok; EXPECT(p, TOK_IDENT); @@ -910,6 +916,8 @@ static mjs_err_t parse_statement(struct pstate* p) { pnext1(p); return MJS_OK; case TOK_KEYWORD_LET: + case TOK_KEYWORD_VAR: + case TOK_KEYWORD_CONST: return parse_let(p); case TOK_OPEN_CURLY: return parse_block(p, 1); @@ -939,7 +947,6 @@ static mjs_err_t parse_statement(struct pstate* p) { case TOK_KEYWORD_SWITCH: case TOK_KEYWORD_THROW: case TOK_KEYWORD_TRY: - case TOK_KEYWORD_VAR: case TOK_KEYWORD_VOID: case TOK_KEYWORD_WITH: mjs_set_errorf( diff --git a/lib/mjs/mjs_tok.c b/lib/mjs/mjs_tok.c index bdff5a86a..f89606d23 100644 --- a/lib/mjs/mjs_tok.c +++ b/lib/mjs/mjs_tok.c @@ -80,12 +80,13 @@ static int getnum(struct pstate* p) { } static int is_reserved_word_token(const char* s, int len) { - const char* reserved[] = {"break", "case", "catch", "continue", "debugger", "default", - "delete", "do", "else", "false", "finally", "for", - "function", "if", "in", "instanceof", "new", "null", - "return", "switch", "this", "throw", "true", "try", - "typeof", "var", "void", "while", "with", "let", - "undefined", NULL}; + const char* reserved[] = {"break", "case", "catch", "continue", "debugger", + "default", "delete", "do", "else", "false", + "finally", "for", "function", "if", "in", + "instanceof", "new", "null", "return", "switch", + "this", "throw", "true", "try", "typeof", + "var", "void", "while", "with", "let", + "const", "undefined", NULL}; int i; if(!mjs_is_alpha(s[0])) return 0; for(i = 0; reserved[i] != NULL; i++) { @@ -95,7 +96,8 @@ static int is_reserved_word_token(const char* s, int len) { } static int getident(struct pstate* p) { - while(mjs_is_ident(p->pos[0]) || mjs_is_digit(p->pos[0])) p->pos++; + while(mjs_is_ident(p->pos[0]) || mjs_is_digit(p->pos[0])) + p->pos++; p->tok.len = p->pos - p->tok.ptr; p->pos--; return TOK_IDENT; @@ -125,7 +127,8 @@ static void skip_spaces_and_comments(struct pstate* p) { p->pos++; } if(p->pos[0] == '/' && p->pos[1] == '/') { - while(p->pos[0] != '\0' && p->pos[0] != '\n') p->pos++; + while(p->pos[0] != '\0' && p->pos[0] != '\n') + p->pos++; } if(p->pos[0] == '/' && p->pos[1] == '*') { p->pos += 2; @@ -142,8 +145,8 @@ static void skip_spaces_and_comments(struct pstate* p) { } static int ptranslate(int tok) { -#define DT(a, b) ((a) << 8 | (b)) -#define TT(a, b, c) ((a) << 16 | (b) << 8 | (c)) +#define DT(a, b) ((a) << 8 | (b)) +#define TT(a, b, c) ((a) << 16 | (b) << 8 | (c)) #define QT(a, b, c, d) ((a) << 24 | (b) << 16 | (c) << 8 | (d)) /* Map token ID produced by mjs_tok.c to token ID produced by lemon */ /* clang-format off */ diff --git a/lib/mjs/mjs_tok.h b/lib/mjs/mjs_tok.h index 03d8fe6fa..5ff079492 100644 --- a/lib/mjs/mjs_tok.h +++ b/lib/mjs/mjs_tok.h @@ -125,6 +125,7 @@ enum { TOK_KEYWORD_WHILE, TOK_KEYWORD_WITH, TOK_KEYWORD_LET, + TOK_KEYWORD_CONST, TOK_KEYWORD_UNDEFINED, TOK_MAX }; diff --git a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c index 83875bb85..7d51f6c6e 100644 --- a/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c +++ b/lib/nfc/protocols/mf_ultralight/mf_ultralight_poller.c @@ -506,13 +506,13 @@ static NfcCommand mf_ultralight_poller_handler_read_pages(MfUltralightPoller* in } if(instance->error == MfUltralightErrorNone) { - if (start_page < instance->pages_total) { - FURI_LOG_D(TAG, "Read page %d success", start_page); - instance->data->page[start_page] = data.page[0]; - instance->pages_read++; - instance->data->pages_read = instance->pages_read; - } - + if(start_page < instance->pages_total) { + FURI_LOG_D(TAG, "Read page %d success", start_page); + instance->data->page[start_page] = data.page[0]; + instance->pages_read++; + instance->data->pages_read = instance->pages_read; + } + if(instance->pages_read == instance->pages_total) { instance->state = MfUltralightPollerStateReadCounters; } diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index be60c6a1c..b5d51a0dd 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.3,, +Version,+,78.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -1120,6 +1120,7 @@ Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObj Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* +Function,+,furi_event_loop_subscribe_event_flag,void,"FuriEventLoop*, FuriEventFlag*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" @@ -1376,6 +1377,7 @@ 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_alarm,_Bool,DateTime* Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,DateTime* Function,+,furi_hal_rtc_get_fault_data,uint32_t, @@ -1393,8 +1395,11 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag +Function,-,furi_hal_rtc_prepare_for_shutdown,void, Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag Function,+,furi_hal_rtc_reset_registers,void, +Function,-,furi_hal_rtc_set_alarm,void,"const DateTime*, _Bool" +Function,-,furi_hal_rtc_set_alarm_callback,void,"FuriHalRtcAlarmCallback, void*" Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,DateTime* Function,+,furi_hal_rtc_set_fault_data,void,uint32_t @@ -3114,6 +3119,7 @@ Variable,+,sequence_display_backlight_off,const NotificationSequence, Variable,+,sequence_display_backlight_off_delay_1000,const NotificationSequence, Variable,+,sequence_display_backlight_on,const NotificationSequence, Variable,+,sequence_double_vibro,const NotificationSequence, +Variable,+,sequence_empty,const NotificationSequence, Variable,+,sequence_error,const NotificationSequence, Variable,+,sequence_lcd_contrast_update,const NotificationSequence, Variable,+,sequence_not_charging,const NotificationSequence, diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index a749963b6..3e0a90630 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,77.3,, +Version,+,78.1,, 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,, @@ -1260,6 +1260,7 @@ Function,+,furi_event_loop_is_subscribed,_Bool,"FuriEventLoop*, FuriEventLoopObj Function,+,furi_event_loop_pend_callback,void,"FuriEventLoop*, FuriEventLoopPendingCallback, void*" Function,+,furi_event_loop_run,void,FuriEventLoop* Function,+,furi_event_loop_stop,void,FuriEventLoop* +Function,+,furi_event_loop_subscribe_event_flag,void,"FuriEventLoop*, FuriEventFlag*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_message_queue,void,"FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_mutex,void,"FuriEventLoop*, FuriMutex*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" Function,+,furi_event_loop_subscribe_semaphore,void,"FuriEventLoop*, FuriSemaphore*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*" @@ -1600,6 +1601,7 @@ Function,+,furi_hal_rfid_tim_read_pause,void, Function,+,furi_hal_rfid_tim_read_start,void,"float, float" Function,+,furi_hal_rfid_tim_read_stop,void, Function,-,furi_hal_rtc_deinit_early,void, +Function,-,furi_hal_rtc_get_alarm,_Bool,DateTime* Function,+,furi_hal_rtc_get_boot_mode,FuriHalRtcBootMode, Function,+,furi_hal_rtc_get_datetime,void,DateTime* Function,+,furi_hal_rtc_get_fault_data,uint32_t, @@ -1617,8 +1619,11 @@ Function,+,furi_hal_rtc_get_timestamp,uint32_t, Function,-,furi_hal_rtc_init,void, Function,-,furi_hal_rtc_init_early,void, Function,+,furi_hal_rtc_is_flag_set,_Bool,FuriHalRtcFlag +Function,-,furi_hal_rtc_prepare_for_shutdown,void, Function,+,furi_hal_rtc_reset_flag,void,FuriHalRtcFlag Function,+,furi_hal_rtc_reset_registers,void, +Function,-,furi_hal_rtc_set_alarm,void,"const DateTime*, _Bool" +Function,-,furi_hal_rtc_set_alarm_callback,void,"FuriHalRtcAlarmCallback, void*" Function,+,furi_hal_rtc_set_boot_mode,void,FuriHalRtcBootMode Function,+,furi_hal_rtc_set_datetime,void,DateTime* Function,+,furi_hal_rtc_set_fault_data,void,uint32_t @@ -4054,6 +4059,7 @@ Variable,+,sequence_display_backlight_off,const NotificationSequence, Variable,+,sequence_display_backlight_off_delay_1000,const NotificationSequence, Variable,+,sequence_display_backlight_on,const NotificationSequence, Variable,+,sequence_double_vibro,const NotificationSequence, +Variable,+,sequence_empty,const NotificationSequence, Variable,+,sequence_error,const NotificationSequence, Variable,+,sequence_lcd_contrast_update,const NotificationSequence, Variable,+,sequence_not_charging,const NotificationSequence, diff --git a/targets/f7/furi_hal/furi_hal_interrupt.c b/targets/f7/furi_hal/furi_hal_interrupt.c index cf10c8d33..27872570e 100644 --- a/targets/f7/furi_hal/furi_hal_interrupt.c +++ b/targets/f7/furi_hal/furi_hal_interrupt.c @@ -68,6 +68,9 @@ const IRQn_Type furi_hal_interrupt_irqn[FuriHalInterruptIdMax] = { // COMP [FuriHalInterruptIdCOMP] = COMP_IRQn, + // RTC + [FuriHalInterruptIdRtcAlarm] = RTC_Alarm_IRQn, + // HSEM [FuriHalInterruptIdHsem] = HSEM_IRQn, @@ -256,6 +259,10 @@ void DMA2_Channel7_IRQHandler(void) { furi_hal_interrupt_call(FuriHalInterruptIdDma2Ch7); } +void RTC_Alarm_IRQHandler(void) { + furi_hal_interrupt_call(FuriHalInterruptIdRtcAlarm); +} + void HSEM_IRQHandler(void) { furi_hal_interrupt_call(FuriHalInterruptIdHsem); } diff --git a/targets/f7/furi_hal/furi_hal_interrupt.h b/targets/f7/furi_hal/furi_hal_interrupt.h index 2326d3c0a..5ea830ced 100644 --- a/targets/f7/furi_hal/furi_hal_interrupt.h +++ b/targets/f7/furi_hal/furi_hal_interrupt.h @@ -42,6 +42,9 @@ typedef enum { // Comp FuriHalInterruptIdCOMP, + // RTC + FuriHalInterruptIdRtcAlarm, + // HSEM FuriHalInterruptIdHsem, diff --git a/targets/f7/furi_hal/furi_hal_power.c b/targets/f7/furi_hal/furi_hal_power.c index 37c6a8b1b..fe5c0cf17 100644 --- a/targets/f7/furi_hal/furi_hal_power.c +++ b/targets/f7/furi_hal/furi_hal_power.c @@ -326,6 +326,7 @@ void furi_hal_power_shutdown(void) { void furi_hal_power_off(void) { // Crutch: shutting down with ext 3V3 off is causing LSE to stop + furi_hal_rtc_prepare_for_shutdown(); furi_hal_power_enable_external_3_3v(); furi_hal_vibro_on(true); furi_delay_us(50000); diff --git a/targets/f7/furi_hal/furi_hal_rtc.c b/targets/f7/furi_hal/furi_hal_rtc.c index d5cda7476..ab592fb78 100644 --- a/targets/f7/furi_hal/furi_hal_rtc.c +++ b/targets/f7/furi_hal/furi_hal_rtc.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -42,6 +43,13 @@ typedef struct { _Static_assert(sizeof(SystemReg) == 4, "SystemReg size mismatch"); +typedef struct { + FuriHalRtcAlarmCallback alarm_callback; + void* alarm_callback_context; +} FuriHalRtc; + +static FuriHalRtc furi_hal_rtc = {}; + static const FuriHalSerialId furi_hal_rtc_log_devices[] = { [FuriHalRtcLogDeviceUsart] = FuriHalSerialIdUsart, [FuriHalRtcLogDeviceLpuart] = FuriHalSerialIdLpuart, @@ -60,6 +68,17 @@ static const uint32_t furi_hal_rtc_log_baud_rates[] = { [FuriHalRtcLogBaudRate1843200] = 1843200, }; +static void furi_hal_rtc_enter_init_mode(void) { + LL_RTC_EnableInitMode(RTC); + while(LL_RTC_IsActiveFlag_INIT(RTC) != 1) + ; +} + +static void furi_hal_rtc_exit_init_mode(void) { + LL_RTC_DisableInitMode(RTC); + furi_hal_rtc_sync_shadow(); +} + static void furi_hal_rtc_reset(void) { LL_RCC_ForceBackupDomainReset(); LL_RCC_ReleaseBackupDomainReset(); @@ -127,6 +146,36 @@ static void furi_hal_rtc_recover(void) { } } +static void furi_hal_rtc_alarm_handler(void* context) { + UNUSED(context); + + if(LL_RTC_IsActiveFlag_ALRA(RTC) != 0) { + /* Clear the Alarm interrupt pending bit */ + LL_RTC_ClearFlag_ALRA(RTC); + + /* Alarm callback */ + furi_check(furi_hal_rtc.alarm_callback); + furi_hal_rtc.alarm_callback(furi_hal_rtc.alarm_callback_context); + } + LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_17); +} + +static void furi_hal_rtc_set_alarm_out(bool enable) { + FURI_CRITICAL_ENTER(); + LL_RTC_DisableWriteProtection(RTC); + if(enable) { + LL_RTC_SetAlarmOutEvent(RTC, LL_RTC_ALARMOUT_ALMA); + LL_RTC_SetOutputPolarity(RTC, LL_RTC_OUTPUTPOLARITY_PIN_LOW); + LL_RTC_SetAlarmOutputType(RTC, LL_RTC_ALARM_OUTPUTTYPE_OPENDRAIN); + } else { + LL_RTC_SetAlarmOutEvent(RTC, LL_RTC_ALARMOUT_DISABLE); + LL_RTC_SetOutputPolarity(RTC, LL_RTC_OUTPUTPOLARITY_PIN_LOW); + LL_RTC_SetAlarmOutputType(RTC, LL_RTC_ALARM_OUTPUTTYPE_OPENDRAIN); + } + LL_RTC_EnableWriteProtection(RTC); + FURI_CRITICAL_EXIT(); +} + void furi_hal_rtc_init_early(void) { // Enable RTCAPB clock LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_RTCAPB); @@ -167,6 +216,11 @@ void furi_hal_rtc_init(void) { furi_hal_rtc_log_baud_rates[furi_hal_rtc_get_log_baud_rate()]); FURI_LOG_I(TAG, "Init OK"); + furi_hal_rtc_set_alarm_out(false); +} + +void furi_hal_rtc_prepare_for_shutdown(void) { + furi_hal_rtc_set_alarm_out(true); } void furi_hal_rtc_sync_shadow(void) { @@ -347,9 +401,7 @@ void furi_hal_rtc_set_datetime(DateTime* datetime) { LL_RTC_DisableWriteProtection(RTC); /* Enter Initialization mode and wait for INIT flag to be set */ - LL_RTC_EnableInitMode(RTC); - while(!LL_RTC_IsActiveFlag_INIT(RTC)) { - } + furi_hal_rtc_enter_init_mode(); /* Set time */ LL_RTC_TIME_Config( @@ -368,9 +420,7 @@ void furi_hal_rtc_set_datetime(DateTime* datetime) { __LL_RTC_CONVERT_BIN2BCD(datetime->year - 2000)); /* Exit Initialization mode */ - LL_RTC_DisableInitMode(RTC); - - furi_hal_rtc_sync_shadow(); + furi_hal_rtc_exit_init_mode(); /* Enable write protection */ LL_RTC_EnableWriteProtection(RTC); @@ -395,6 +445,82 @@ void furi_hal_rtc_get_datetime(DateTime* datetime) { datetime->weekday = __LL_RTC_CONVERT_BCD2BIN((date >> 24) & 0xFF); } +void furi_hal_rtc_set_alarm(const DateTime* datetime, bool enabled) { + furi_check(!FURI_IS_IRQ_MODE()); + + FURI_CRITICAL_ENTER(); + LL_RTC_DisableWriteProtection(RTC); + + if(datetime) { + LL_RTC_ALMA_ConfigTime( + RTC, + LL_RTC_ALMA_TIME_FORMAT_AM, + __LL_RTC_CONVERT_BIN2BCD(datetime->hour), + __LL_RTC_CONVERT_BIN2BCD(datetime->minute), + __LL_RTC_CONVERT_BIN2BCD(datetime->second)); + LL_RTC_ALMA_SetMask(RTC, LL_RTC_ALMA_MASK_DATEWEEKDAY); + } + + if(enabled) { + LL_RTC_ClearFlag_ALRA(RTC); + LL_RTC_ALMA_Enable(RTC); + } else { + LL_RTC_ALMA_Disable(RTC); + LL_RTC_ClearFlag_ALRA(RTC); + } + + LL_RTC_EnableWriteProtection(RTC); + FURI_CRITICAL_EXIT(); +} + +bool furi_hal_rtc_get_alarm(DateTime* datetime) { + furi_check(datetime); + + memset(datetime, 0, sizeof(DateTime)); + + datetime->hour = __LL_RTC_CONVERT_BCD2BIN(LL_RTC_ALMA_GetHour(RTC)); + datetime->minute = __LL_RTC_CONVERT_BCD2BIN(LL_RTC_ALMA_GetMinute(RTC)); + datetime->second = __LL_RTC_CONVERT_BCD2BIN(LL_RTC_ALMA_GetSecond(RTC)); + + return READ_BIT(RTC->CR, RTC_CR_ALRAE); +} + +void furi_hal_rtc_set_alarm_callback(FuriHalRtcAlarmCallback callback, void* context) { + FURI_CRITICAL_ENTER(); + LL_RTC_DisableWriteProtection(RTC); + if(callback) { + furi_check(!furi_hal_rtc.alarm_callback); + // Set our callbacks + furi_hal_rtc.alarm_callback = callback; + furi_hal_rtc.alarm_callback_context = context; + // Enable RTC ISR + furi_hal_interrupt_set_isr(FuriHalInterruptIdRtcAlarm, furi_hal_rtc_alarm_handler, NULL); + // Hello EXTI my old friend + // Chain: RTC->LINE-17->EXTI->NVIC->FuriHalInterruptIdRtcAlarm + LL_EXTI_EnableRisingTrig_0_31(LL_EXTI_LINE_17); + LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_17); + // Enable alarm interrupt + LL_RTC_EnableIT_ALRA(RTC); + // Force trigger + furi_hal_rtc_alarm_handler(NULL); + } else { + furi_check(furi_hal_rtc.alarm_callback); + // Cleanup EXTI flags and config + LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_17); + LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_17); + LL_EXTI_DisableRisingTrig_0_31(LL_EXTI_LINE_17); + // Cleanup NVIC flags and config + furi_hal_interrupt_set_isr(FuriHalInterruptIdRtcAlarm, NULL, NULL); + // Disable alarm interrupt + LL_RTC_DisableIT_ALRA(RTC); + + furi_hal_rtc.alarm_callback = NULL; + furi_hal_rtc.alarm_callback_context = NULL; + } + LL_RTC_EnableWriteProtection(RTC); + FURI_CRITICAL_EXIT(); +} + void furi_hal_rtc_set_fault_data(uint32_t value) { furi_hal_rtc_set_register(FuriHalRtcRegisterFaultData, value); } diff --git a/targets/f7/furi_hal/furi_hal_rtc.h b/targets/f7/furi_hal/furi_hal_rtc.h index c5eab12ec..9c48fac0b 100644 --- a/targets/f7/furi_hal/furi_hal_rtc.h +++ b/targets/f7/furi_hal/furi_hal_rtc.h @@ -98,6 +98,14 @@ void furi_hal_rtc_deinit_early(void); /** Initialize RTC subsystem */ void furi_hal_rtc_init(void); +/** Prepare system for shutdown + * + * This function must be called before system sent to transport mode(power off). + * FlipperZero implementation configures and enables ALARM output on pin PC13 + * (Back button). This allows the system to wake-up charger from transport mode. + */ +void furi_hal_rtc_prepare_for_shutdown(void); + /** Force sync shadow registers */ void furi_hal_rtc_sync_shadow(void); @@ -247,6 +255,38 @@ void furi_hal_rtc_set_datetime(DateTime* datetime); */ void furi_hal_rtc_get_datetime(DateTime* datetime); +/** Set alarm + * + * @param[in] datetime The date time to set or NULL if time change is not needed + * @param[in] enabled Indicates if alarm must be enabled or disabled + */ +void furi_hal_rtc_set_alarm(const DateTime* datetime, bool enabled); + +/** Get alarm + * + * @param datetime Pointer to DateTime object + * + * @return true if alarm was set, false otherwise + */ +bool furi_hal_rtc_get_alarm(DateTime* datetime); + +/** Furi HAL RTC alarm callback signature */ +typedef void (*FuriHalRtcAlarmCallback)(void* context); + +/** Set alarm callback + * + * Use it to subscribe to alarm trigger event. Setting alarm callback is + * independent from setting alarm. + * + * @warning Normally this callback will be delivered from the ISR, however we may + * deliver it while this function is called. This happens when + * the alarm has already triggered, but there was no ISR set. + * + * @param[in] callback The callback + * @param context The context + */ +void furi_hal_rtc_set_alarm_callback(FuriHalRtcAlarmCallback callback, void* context); + /** Set RTC Fault Data * * @param[in] value The value diff --git a/targets/furi_hal_include/furi_hal.h b/targets/furi_hal_include/furi_hal.h index cf483553f..284bd558a 100644 --- a/targets/furi_hal_include/furi_hal.h +++ b/targets/furi_hal_include/furi_hal.h @@ -46,13 +46,24 @@ struct STOP_EXTERNING_ME {}; extern "C" { #endif -/** Early FuriHal init, only essential subsystems */ +/** Early FuriHal init + * + * Init essential subsystems used in pre-DFU stage. + * This state can be undone with `furi_hal_deinit_early`. + * + */ void furi_hal_init_early(void); -/** Early FuriHal deinit */ +/** Early FuriHal deinit + * + * Undo `furi_hal_init_early`, prepare system for switch to another firmware/bootloader. + */ void furi_hal_deinit_early(void); -/** Init FuriHal */ +/** Init FuriHal + * + * Initialize the rest of the HAL, must be used after `furi_hal_init_early`. + */ void furi_hal_init(void); /** Jump to the void* diff --git a/tsconfig.json b/tsconfig.json index 2655a8b97..53f0a3625 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,13 @@ "checkJs": true, "module": "CommonJS", "typeRoots": [ - "./applications/system/js_app/types" + "./applications/system/js_app/packages/fz-sdk/" ], "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", + "./applications/system/js_app/packages/fz-sdk/global.d.ts", ] } \ No newline at end of file